From fc2761d16b4a3548ff4dccfcfc601d28bc91c0fe Mon Sep 17 00:00:00 2001 From: calendar-assistant Date: Wed, 22 Jul 2026 14:36:01 +0800 Subject: [PATCH] feat(calendar): auto-add bot self as attendee and note user-only search (#1991) When creating an event as a bot, resolve the bot's own open_id via /bot/v3/info and add it to the attendee list, mirroring how a user is auto-joined to their own events; warn and proceed without it if the lookup fails. Also note in the +create skill doc that the user-search API is user-only, so resolving a name to open_id needs --as user. --- shortcuts/calendar/calendar_create.go | 30 +++- shortcuts/calendar/calendar_test.go | 130 ++++++++++++++++++ .../references/lark-calendar-create.md | 1 + 3 files changed, 155 insertions(+), 6 deletions(-) diff --git a/shortcuts/calendar/calendar_create.go b/shortcuts/calendar/calendar_create.go index effb5f677..2599ebd2e 100644 --- a/shortcuts/calendar/calendar_create.go +++ b/shortcuts/calendar/calendar_create.go @@ -67,6 +67,25 @@ func parseAttendees(attendeesStr string, currentUserId string) ([]map[string]str return attendees, nil } +// selfAttendeeId resolves the open_id of the identity running the command so it +// can be auto-added to the attendee list, mirroring how a human user is joined +// to their own events. For a user it comes from config; for a bot it is fetched +// from /bot/v3/info. If the bot lookup fails, we warn and return "" so the event +// is still created with the explicitly requested attendees. +func selfAttendeeId(runtime *common.RuntimeContext) string { + if !runtime.IsBot() { + return runtime.UserOpenId() + } + info, err := runtime.BotInfo() + if err != nil { + fmt.Fprintf(runtime.IO().ErrOut, + "[calendar +create] warning: could not resolve bot identity to add it as an attendee (%v); proceeding without the bot\n", + err) + return "" + } + return info.OpenID +} + func attendeesIncludeRoom(attendees []map[string]string) bool { for _, attendee := range attendees { if attendee["type"] == "resource" || attendee["room_id"] != "" { @@ -176,7 +195,9 @@ var CalendarCreate = common.Shortcut{ eventData := buildEventData(runtime, startTs, endTs) attendeesStr := runtime.Str("attendee-ids") if attendeesStr != "" { - // Note: dry-run doesn't network resolve the current user's open_id. + // Note: dry-run doesn't network resolve the running identity's own + // open_id (user from config, bot from /bot/v3/info), so the auto-joined + // self attendee is not shown here. attendees, err := parseAttendees(attendeesStr, "") if err != nil { return common.NewDryRunAPI().Set("error", err.Error()) @@ -228,11 +249,8 @@ var CalendarCreate = common.Shortcut{ // Add attendees if specified if attendeesStr := runtime.Str("attendee-ids"); attendeesStr != "" { - currentUserId := "" - if !runtime.IsBot() { - currentUserId = runtime.UserOpenId() - } - attendees, err := parseAttendees(attendeesStr, currentUserId) + selfId := selfAttendeeId(runtime) + attendees, err := parseAttendees(attendeesStr, selfId) if err != nil { return withParam(err, "--attendee-ids") } diff --git a/shortcuts/calendar/calendar_test.go b/shortcuts/calendar/calendar_test.go index b086357e1..b6dec52f4 100644 --- a/shortcuts/calendar/calendar_test.go +++ b/shortcuts/calendar/calendar_test.go @@ -251,6 +251,136 @@ func TestCreate_WithAttendees_Success(t *testing.T) { } } +func TestCreate_WithAttendees_AsBot_AddsBotSelf(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/bot/v3/info", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "bot": map[string]interface{}{ + "open_id": "ou_botself", + "app_name": "Test Bot", + }, + }, + }) + 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_bot", + "summary": "Bot Sync", + "start_time": map[string]interface{}{ + "timestamp": "1742515200", + }, + "end_time": map[string]interface{}{ + "timestamp": "1742518800", + }, + }, + }, + }, + }) + attendeesStub := &httpmock.Stub{ + Method: "POST", + URL: "/events/evt_bot/attendees", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{}, + }, + } + reg.Register(attendeesStub) + + err := mountAndRun(t, CalendarCreate, []string{ + "+create", + "--summary", "Bot Sync", + "--start", "2025-03-21T00:00:00+08:00", + "--end", "2025-03-21T01:00:00+08:00", + "--calendar-id", "cal_test123", + "--attendee-ids", "ou_user1", + "--as", "bot", + }, f, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if attendeesStub.CapturedBody == nil { + t.Fatal("attendees API was not called") + } + if !bytes.Contains(attendeesStub.CapturedBody, []byte("ou_botself")) { + t.Fatalf("expected bot open_id ou_botself in attendees request, got: %s", attendeesStub.CapturedBody) + } + if !bytes.Contains(attendeesStub.CapturedBody, []byte("ou_user1")) { + t.Fatalf("expected requested attendee ou_user1 in attendees request, got: %s", attendeesStub.CapturedBody) + } +} + +func TestCreate_WithAttendees_AsBot_BotInfoFails_ProceedsWithoutBot(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/bot/v3/info", + Body: map[string]interface{}{ + "code": 99991663, "msg": "app ticket invalid", + }, + }) + 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_nobot", + "summary": "Bot Sync", + "start_time": map[string]interface{}{ + "timestamp": "1742515200", + }, + "end_time": map[string]interface{}{ + "timestamp": "1742518800", + }, + }, + }, + }, + }) + attendeesStub := &httpmock.Stub{ + Method: "POST", + URL: "/events/evt_nobot/attendees", + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{}, + }, + } + reg.Register(attendeesStub) + + err := mountAndRun(t, CalendarCreate, []string{ + "+create", + "--summary", "Bot Sync", + "--start", "2025-03-21T00:00:00+08:00", + "--end", "2025-03-21T01:00:00+08:00", + "--calendar-id", "cal_test123", + "--attendee-ids", "ou_user1", + "--as", "bot", + }, f, nil) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if attendeesStub.CapturedBody == nil { + t.Fatal("attendees API was not called") + } + if !bytes.Contains(attendeesStub.CapturedBody, []byte("ou_user1")) { + t.Fatalf("expected requested attendee ou_user1 in attendees request, got: %s", attendeesStub.CapturedBody) + } + if bytes.Contains(attendeesStub.CapturedBody, []byte("ou_botself")) { + t.Fatalf("bot open_id should be absent when /bot/v3/info fails, got: %s", attendeesStub.CapturedBody) + } +} + func TestCreate_WithAttendees_APIError_RollsBack(t *testing.T) { f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) diff --git a/skills/lark-calendar/references/lark-calendar-create.md b/skills/lark-calendar/references/lark-calendar-create.md index 8c280c4b5..b0e772b1a 100644 --- a/skills/lark-calendar/references/lark-calendar-create.md +++ b/skills/lark-calendar/references/lark-calendar-create.md @@ -44,6 +44,7 @@ lark-cli calendar +create --summary "..." --start "..." --end "..." \ > 自动设置 `reminders: [{"minutes": 5}]`,默认日程开始前 5 分钟提醒。 > 自动设置 `vchat: {"vc_type": "vc"}`,默认日程包含飞书视频会议。如需其他视频会议类型或不含视频会议,请使用完整 API 命令。 > 失败保护:若添加参会人失败(如 open_id 错误),CLI 会自动删除刚创建的空日程(回滚,不通知参会人)。 +> 搜索用户接口不支持 bot 身份,需用 `--as user` 进行搜索。 > 审批会议室:`+create` 不暴露低频字段 `attendees[].approval_reason`。如果会议室要求审批,请使用用户身份先创建日程,再用完整 API `calendar event.attendees create --as user` 添加会议室并传 `approval_reason`。 ## 高级用法(完整 API 命令)