From ca1d166fd9593b34c118e734af3ceccf03275fd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E6=B8=AF?= Date: Fri, 17 Apr 2026 19:15:28 +0800 Subject: [PATCH 1/2] feat: support event share info Change-Id: I4876df38effe44de04e587ac18ace7e230c9fa3a --- skills/lark-calendar/SKILL.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/skills/lark-calendar/SKILL.md b/skills/lark-calendar/SKILL.md index 13b571d77..982a1c183 100644 --- a/skills/lark-calendar/SKILL.md +++ b/skills/lark-calendar/SKILL.md @@ -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 [flags] # 调用 API - `instance_view` — 查询日程视图 - `patch` — 更新日程 - `search` — 搜索日程 + - `share_info` — 获取日程分享链接 ### freebusys @@ -137,6 +139,7 @@ lark-cli calendar [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` | **注意(强制性):** From 2c6b11830d05c0aed23f82cabb81774de7134435 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=83=A1=E6=B8=AF?= Date: Mon, 20 Apr 2026 17:14:05 +0800 Subject: [PATCH 2/2] fix: return detail err info for calendar --- shortcuts/calendar/calendar_agenda.go | 1 + shortcuts/calendar/calendar_create.go | 3 + shortcuts/calendar/calendar_freebusy.go | 1 + shortcuts/calendar/calendar_test.go | 333 ++++++++++++++++++++++++ shortcuts/calendar/errors.go | 66 +++++ 5 files changed, 404 insertions(+) create mode 100644 shortcuts/calendar/errors.go diff --git a/shortcuts/calendar/calendar_agenda.go b/shortcuts/calendar/calendar_agenda.go index 85839deb9..c972bdb9e 100644 --- a/shortcuts/calendar/calendar_agenda.go +++ b/shortcuts/calendar/calendar_agenda.go @@ -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) } diff --git a/shortcuts/calendar/calendar_create.go b/shortcuts/calendar/calendar_create.go index 7c824cf11..d089d595a 100644 --- a/shortcuts/calendar/calendar_create.go +++ b/shortcuts/calendar/calendar_create.go @@ -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) } diff --git a/shortcuts/calendar/calendar_freebusy.go b/shortcuts/calendar/calendar_freebusy.go index d1463ec65..1de1a67e2 100644 --- a/shortcuts/calendar/calendar_freebusy.go +++ b/shortcuts/calendar/calendar_freebusy.go @@ -102,6 +102,7 @@ var CalendarFreebusy = common.Shortcut{ "user_id": userId, "need_rsvp_status": true, }) + err = wrapPredefinedError(err) if err != nil { return err } diff --git a/shortcuts/calendar/calendar_test.go b/shortcuts/calendar/calendar_test.go index 4f64983bc..8fc8794fa 100644 --- a/shortcuts/calendar/calendar_test.go +++ b/shortcuts/calendar/calendar_test.go @@ -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 // --------------------------------------------------------------------------- diff --git a/shortcuts/calendar/errors.go b/shortcuts/calendar/errors.go new file mode 100644 index 000000000..e6d1e905f --- /dev/null +++ b/shortcuts/calendar/errors.go @@ -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 +}