mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 01:51:27 +08:00
feat: support event share link and error details (#583)
* feat: support event share info Change-Id: I4876df38effe44de04e587ac18ace7e230c9fa3a * fix: return detail err info for calendar
This commit is contained in:
@@ -54,6 +54,7 @@ func fetchInstanceViewRange(ctx context.Context, runtime *common.RuntimeContext,
|
||||
"start_time": fmt.Sprintf("%d", startTime),
|
||||
"end_time": fmt.Sprintf("%d", endTime),
|
||||
}, nil)
|
||||
err = wrapPredefinedError(err)
|
||||
if err != nil {
|
||||
return nil, output.Errorf(output.ExitAPI, "api_error", "API call failed: %s", err)
|
||||
}
|
||||
|
||||
@@ -194,6 +194,7 @@ var CalendarCreate = common.Shortcut{
|
||||
data, err := runtime.CallAPI("POST",
|
||||
fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events", validate.EncodePathSegment(calendarId)),
|
||||
nil, eventData)
|
||||
err = wrapPredefinedError(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -221,11 +222,13 @@ var CalendarCreate = common.Shortcut{
|
||||
"attendees": attendees,
|
||||
"need_notification": true,
|
||||
})
|
||||
err = wrapPredefinedError(err)
|
||||
if err != nil {
|
||||
// Rollback: delete the event
|
||||
_, rollbackErr := runtime.RawAPI("DELETE",
|
||||
fmt.Sprintf("/open-apis/calendar/v4/calendars/%s/events/%s", validate.EncodePathSegment(calendarId), validate.EncodePathSegment(eventId)),
|
||||
map[string]interface{}{"need_notification": false}, nil)
|
||||
rollbackErr = wrapPredefinedError(rollbackErr)
|
||||
if rollbackErr != nil {
|
||||
return output.Errorf(output.ExitAPI, "api_error", "failed to add attendees: %v; rollback also failed, orphan event_id=%s needs manual cleanup", rollbackErr, eventId)
|
||||
}
|
||||
|
||||
@@ -102,6 +102,7 @@ var CalendarFreebusy = common.Shortcut{
|
||||
"user_id": userId,
|
||||
"need_rsvp_status": true,
|
||||
})
|
||||
err = wrapPredefinedError(err)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -375,6 +375,238 @@ func TestCreate_NoEventIdReturned(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_CreateEvent_InvalidParamsWithDetail(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/calendar/v4/calendars/cal_test123/events",
|
||||
Body: map[string]interface{}{
|
||||
"code": errCodeInvalidParamsWithDetail,
|
||||
"msg": "invalid params",
|
||||
"error": map[string]interface{}{
|
||||
"details": []interface{}{
|
||||
map[string]interface{}{"value": "end_time should be later than start_time"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarCreate, []string{
|
||||
"+create",
|
||||
"--summary", "Bad Time",
|
||||
"--start", "2025-03-21T10:00:00+08:00",
|
||||
"--end", "2025-03-21T11:00:00+08:00",
|
||||
"--calendar-id", "cal_test123",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 190014, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected *output.ExitError, got %T", err)
|
||||
}
|
||||
if exitErr.Detail.Code != errCodeInvalidParamsWithDetail {
|
||||
t.Errorf("expected code %d, got %d", errCodeInvalidParamsWithDetail, exitErr.Detail.Code)
|
||||
}
|
||||
if !strings.Contains(exitErr.Detail.Message, "end_time should be later than start_time") {
|
||||
t.Errorf("expected detail value in message, got %q", exitErr.Detail.Message)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_CreateEvent_InvalidParamsWithoutDetailValue(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/calendar/v4/calendars/cal_test123/events",
|
||||
Body: map[string]interface{}{
|
||||
"code": errCodeInvalidParamsWithDetail,
|
||||
"msg": "invalid params",
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarCreate, []string{
|
||||
"+create",
|
||||
"--summary", "Bad Time",
|
||||
"--start", "2025-03-21T10:00:00+08:00",
|
||||
"--end", "2025-03-21T11:00:00+08:00",
|
||||
"--calendar-id", "cal_test123",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 190014, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected *output.ExitError, got %T", err)
|
||||
}
|
||||
if exitErr.Detail.Code != errCodeInvalidParamsWithDetail {
|
||||
t.Errorf("expected code %d, got %d", errCodeInvalidParamsWithDetail, exitErr.Detail.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_CreateEvent_InvalidParams_ErrorNotMap(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/calendar/v4/calendars/cal_test123/events",
|
||||
RawBody: []byte(`{"code":190014,"msg":"invalid params","error":"just a string"}`),
|
||||
ContentType: "text/plain",
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarCreate, []string{
|
||||
"+create",
|
||||
"--summary", "Bad Time",
|
||||
"--start", "2025-03-21T10:00:00+08:00",
|
||||
"--end", "2025-03-21T11:00:00+08:00",
|
||||
"--calendar-id", "cal_test123",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 190014, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected *output.ExitError, got %T", err)
|
||||
}
|
||||
if exitErr.Detail.Code != errCodeInvalidParamsWithDetail {
|
||||
t.Errorf("expected code %d, got %d", errCodeInvalidParamsWithDetail, exitErr.Detail.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_CreateEvent_InvalidParams_NoDetailsKey(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/calendar/v4/calendars/cal_test123/events",
|
||||
Body: map[string]interface{}{
|
||||
"code": errCodeInvalidParamsWithDetail,
|
||||
"msg": "invalid params",
|
||||
"error": map[string]interface{}{
|
||||
"other_key": "no details here",
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarCreate, []string{
|
||||
"+create",
|
||||
"--summary", "Bad Time",
|
||||
"--start", "2025-03-21T10:00:00+08:00",
|
||||
"--end", "2025-03-21T11:00:00+08:00",
|
||||
"--calendar-id", "cal_test123",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 190014, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected *output.ExitError, got %T", err)
|
||||
}
|
||||
if exitErr.Detail.Code != errCodeInvalidParamsWithDetail {
|
||||
t.Errorf("expected code %d, got %d", errCodeInvalidParamsWithDetail, exitErr.Detail.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_CreateEvent_InvalidParams_DetailItemNotMap(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/calendar/v4/calendars/cal_test123/events",
|
||||
Body: map[string]interface{}{
|
||||
"code": errCodeInvalidParamsWithDetail,
|
||||
"msg": "invalid params",
|
||||
"error": map[string]interface{}{
|
||||
"details": []interface{}{nil},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarCreate, []string{
|
||||
"+create",
|
||||
"--summary", "Bad Time",
|
||||
"--start", "2025-03-21T10:00:00+08:00",
|
||||
"--end", "2025-03-21T11:00:00+08:00",
|
||||
"--calendar-id", "cal_test123",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 190014, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected *output.ExitError, got %T", err)
|
||||
}
|
||||
if exitErr.Detail.Code != errCodeInvalidParamsWithDetail {
|
||||
t.Errorf("expected code %d, got %d", errCodeInvalidParamsWithDetail, exitErr.Detail.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCreate_WithAttendees_InvalidParamsWithDetail_RollsBack(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/calendar/v4/calendars/cal_test123/events",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"event": map[string]interface{}{
|
||||
"event_id": "evt_190014",
|
||||
"summary": "Bad Attendees",
|
||||
"start_time": map[string]interface{}{"timestamp": "1742515200"},
|
||||
"end_time": map[string]interface{}{"timestamp": "1742518800"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/events/evt_190014/attendees",
|
||||
Body: map[string]interface{}{
|
||||
"code": errCodeInvalidParamsWithDetail,
|
||||
"msg": "invalid params",
|
||||
"error": map[string]interface{}{
|
||||
"details": []interface{}{
|
||||
map[string]interface{}{"value": "invalid attendee open_id"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "DELETE",
|
||||
URL: "/events/evt_190014",
|
||||
Body: map[string]interface{}{"code": 0, "msg": "ok"},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarCreate, []string{
|
||||
"+create",
|
||||
"--summary", "Bad Attendees",
|
||||
"--start", "2025-03-21T00:00:00+08:00",
|
||||
"--end", "2025-03-21T01:00:00+08:00",
|
||||
"--calendar-id", "cal_test123",
|
||||
"--attendee-ids", "ou_invalid",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for invalid attendees with 190014, got nil")
|
||||
}
|
||||
if !strings.Contains(err.Error(), "invalid attendee open_id") {
|
||||
t.Errorf("expected detail value in error, got: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarAgenda tests
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -645,6 +877,67 @@ func TestAgenda_ExplicitCalendarId(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenda_InvalidParamsWithDetail(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/events/instance_view",
|
||||
Body: map[string]interface{}{
|
||||
"code": errCodeInvalidParamsWithDetail,
|
||||
"msg": "invalid params",
|
||||
"error": map[string]interface{}{
|
||||
"details": []interface{}{
|
||||
map[string]interface{}{"value": "start_time is required"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarAgenda, []string{
|
||||
"+agenda",
|
||||
"--start", "2025-03-21",
|
||||
"--end", "2025-03-21",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 190014, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected *output.ExitError, got %T", err)
|
||||
}
|
||||
if exitErr.Detail.Code != errCodeInvalidParamsWithDetail {
|
||||
t.Errorf("expected code %d, got %d", errCodeInvalidParamsWithDetail, exitErr.Detail.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAgenda_NonExitError_Passthrough(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/events/instance_view",
|
||||
RawBody: []byte("this is not json"),
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarAgenda, []string{
|
||||
"+agenda",
|
||||
"--start", "2025-03-21",
|
||||
"--end", "2025-03-21",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for non-JSON response, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if errors.As(err, &exitErr) && exitErr.Detail != nil && exitErr.Detail.Code != 0 {
|
||||
t.Fatalf("expected non-API error passthrough, got API error code %d", exitErr.Detail.Code)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarFreebusy tests
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -725,6 +1018,46 @@ func TestFreebusy_APIError(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestFreebusy_InvalidParamsWithDetail(t *testing.T) {
|
||||
f, _, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "POST",
|
||||
URL: "/open-apis/calendar/v4/freebusy/list",
|
||||
Body: map[string]interface{}{
|
||||
"code": errCodeInvalidParamsWithDetail,
|
||||
"msg": "invalid params",
|
||||
"error": map[string]interface{}{
|
||||
"details": []interface{}{
|
||||
map[string]interface{}{"value": "user_id is invalid"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, CalendarFreebusy, []string{
|
||||
"+freebusy",
|
||||
"--start", "2025-03-21",
|
||||
"--end", "2025-03-21",
|
||||
"--user-id", "ou_someone",
|
||||
"--as", "bot",
|
||||
}, f, nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error for 190014, got nil")
|
||||
}
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) {
|
||||
t.Fatalf("expected *output.ExitError, got %T", err)
|
||||
}
|
||||
if exitErr.Detail.Code != errCodeInvalidParamsWithDetail {
|
||||
t.Errorf("expected code %d, got %d", errCodeInvalidParamsWithDetail, exitErr.Detail.Code)
|
||||
}
|
||||
if !strings.Contains(exitErr.Detail.Message, "user_id is invalid") {
|
||||
t.Errorf("expected detail value in message, got %q", exitErr.Detail.Message)
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// CalendarSuggestion tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
66
shortcuts/calendar/errors.go
Normal file
66
shortcuts/calendar/errors.go
Normal file
@@ -0,0 +1,66 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package calendar
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/larksuite/cli/internal/output"
|
||||
)
|
||||
|
||||
const (
|
||||
errCodeInvalidParamsWithDetail = 190014
|
||||
)
|
||||
|
||||
// getErrorDetailValue extracts the first detail value from the output.ErrDetail.
|
||||
// It assumes Detail is a map containing a "details" array of objects with "value" string fields.
|
||||
// For example: {"details": [{"value": "error message 1"}, {"value": "error message 2"}]}
|
||||
// Returns an empty string if the structure doesn't match or the array is empty.
|
||||
func getErrorDetailValue(e *output.ErrDetail) string {
|
||||
if e == nil || e.Detail == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
errMap, ok := e.Detail.(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
details, ok := errMap["details"].([]interface{})
|
||||
if !ok || len(details) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
detailObj, ok := details[0].(map[string]interface{})
|
||||
if !ok {
|
||||
return ""
|
||||
}
|
||||
|
||||
val, _ := detailObj["value"].(string)
|
||||
return val
|
||||
}
|
||||
|
||||
// wrapPredefinedError wraps an error into *output.ExitError if it matches predefined error codes.
|
||||
// Currently handles error code 190014 (invalid params with detail), extracting the detail value into the message.
|
||||
// If the error is nil or doesn't match predefined codes, returns the original error.
|
||||
func wrapPredefinedError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var exitErr *output.ExitError
|
||||
if !errors.As(err, &exitErr) || exitErr.Detail == nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if exitErr.Detail.Code == errCodeInvalidParamsWithDetail {
|
||||
if val := getErrorDetailValue(exitErr.Detail); val != "" {
|
||||
fullMsg := fmt.Sprintf("%s: %s", exitErr.Detail.Message, val)
|
||||
return output.ErrAPI(exitErr.Detail.Code, fullMsg, exitErr.Detail.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
@@ -18,6 +18,7 @@ metadata:
|
||||
- **查询过去时间的会议**:如果用户明确查询过去时间的会议(如“昨天的会议”、“上周的会议”),**优先使用 [`../lark-vc/SKILL.md`](../lark-vc/SKILL.md) 搜索会议记录**。因为会议数据不仅包含从日程发起的视频会议,还包含即时会议,仅查询日程数据会导致结果不全。
|
||||
- **查询日历/日程或未来时间的会议**:如果用户明确表达的是“日历”、“日程”,或者涉及**未来时间**的安排,则属于本技能(lark-calendar)的业务域,请继续使用本技能处理。
|
||||
|
||||
**CRITICAL — 验证与同步延迟:在涉及删除日程(delete)或修改日程(patch)之后,如果需要进行二次查询验证操作结果,MUST 等待至少 2 秒后再进行查询,以防止因数据同步延迟导致查不到最新数据。注意:不要向用户提及你等待了这 2 秒钟的事情。**
|
||||
|
||||
**时间与日期推断规范:**
|
||||
为确保准确性,在涉及时间推断时,请严格遵循以下规则:
|
||||
@@ -112,6 +113,7 @@ lark-cli calendar <resource> <method> [flags] # 调用 API
|
||||
- `instance_view` — 查询日程视图
|
||||
- `patch` — 更新日程
|
||||
- `search` — 搜索日程
|
||||
- `share_info` — 获取日程分享链接
|
||||
|
||||
### freebusys
|
||||
|
||||
@@ -137,6 +139,7 @@ lark-cli calendar <resource> <method> [flags] # 调用 API
|
||||
| `events.instance_view` | `calendar:calendar.event:read` |
|
||||
| `events.patch` | `calendar:calendar.event:update` |
|
||||
| `events.search` | `calendar:calendar.event:read` |
|
||||
| `events.share_info` | `calendar:calendar.event:read` |
|
||||
| `freebusys.list` | `calendar:calendar.free_busy:read` |
|
||||
|
||||
**注意(强制性):**
|
||||
|
||||
Reference in New Issue
Block a user