From dd95f5eb4ade86385bf5b0ee67abe502c16ac146 Mon Sep 17 00:00:00 2001 From: houzhicong Date: Mon, 15 Jun 2026 14:51:18 +0800 Subject: [PATCH] feat: support vc agent active meetings --- shortcuts/vc/shortcuts.go | 1 + shortcuts/vc/vc_meeting_events.go | 2 +- shortcuts/vc/vc_meeting_events_test.go | 2 +- shortcuts/vc/vc_meeting_join.go | 2 +- shortcuts/vc/vc_meeting_leave.go | 2 +- shortcuts/vc/vc_meeting_list_active.go | 113 ++++++++ shortcuts/vc/vc_meeting_test.go | 266 ++++++++++++++++++ skills/lark-vc-agent/SKILL.md | 37 ++- .../lark-vc-agent-meeting-events.md | 48 +++- .../references/lark-vc-agent-meeting-join.md | 11 +- .../references/lark-vc-agent-meeting-leave.md | 15 +- .../lark-vc-agent-meeting-list-active.md | 70 +++++ 12 files changed, 534 insertions(+), 35 deletions(-) create mode 100644 shortcuts/vc/vc_meeting_list_active.go create mode 100644 skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md diff --git a/shortcuts/vc/shortcuts.go b/shortcuts/vc/shortcuts.go index 34b9ec246..444644476 100644 --- a/shortcuts/vc/shortcuts.go +++ b/shortcuts/vc/shortcuts.go @@ -13,6 +13,7 @@ func Shortcuts() []common.Shortcut { VCRecording, VCMeetingJoin, VCMeetingLeave, + VCMeetingListActive, VCMeetingEvents, } } diff --git a/shortcuts/vc/vc_meeting_events.go b/shortcuts/vc/vc_meeting_events.go index 204c10a2a..ebd308e03 100644 --- a/shortcuts/vc/vc_meeting_events.go +++ b/shortcuts/vc/vc_meeting_events.go @@ -48,7 +48,7 @@ var VCMeetingEvents = common.Shortcut{ Description: "List bot meeting events by meeting ID", Risk: "read", Scopes: []string{"vc:meeting.meetingevent:read"}, - AuthTypes: []string{"user"}, + AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "meeting-id", Required: true, Desc: "meeting ID to query"}, diff --git a/shortcuts/vc/vc_meeting_events_test.go b/shortcuts/vc/vc_meeting_events_test.go index f0cb3e0ec..b920e0acb 100644 --- a/shortcuts/vc/vc_meeting_events_test.go +++ b/shortcuts/vc/vc_meeting_events_test.go @@ -818,7 +818,7 @@ func TestVCShortcuts_RegistersMeetingAgentCommands(t *testing.T) { for _, shortcut := range got { commands = append(commands, shortcut.Command) } - want := []string{"+search", "+notes", "+recording", "+meeting-join", "+meeting-leave", "+meeting-events"} + want := []string{"+search", "+notes", "+recording", "+meeting-join", "+meeting-leave", "+meeting-list-active", "+meeting-events"} if !reflect.DeepEqual(commands, want) { t.Fatalf("shortcut commands = %#v, want %#v", commands, want) } diff --git a/shortcuts/vc/vc_meeting_join.go b/shortcuts/vc/vc_meeting_join.go index f2cbb9dfd..7ac2037bd 100644 --- a/shortcuts/vc/vc_meeting_join.go +++ b/shortcuts/vc/vc_meeting_join.go @@ -28,7 +28,7 @@ var VCMeetingJoin = common.Shortcut{ Description: "Join a meeting by meeting number (bot join)", Risk: "write", Scopes: []string{"vc:meeting.bot.join:write"}, - AuthTypes: []string{"user"}, + AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "meeting-number", Required: true, Desc: "meeting number to join"}, diff --git a/shortcuts/vc/vc_meeting_leave.go b/shortcuts/vc/vc_meeting_leave.go index 5aa040114..959c7c90a 100644 --- a/shortcuts/vc/vc_meeting_leave.go +++ b/shortcuts/vc/vc_meeting_leave.go @@ -20,7 +20,7 @@ var VCMeetingLeave = common.Shortcut{ Description: "Leave a meeting by meeting ID", Risk: "write", Scopes: []string{"vc:meeting.bot.join:write"}, - AuthTypes: []string{"user"}, + AuthTypes: []string{"user", "bot"}, HasFormat: true, Flags: []common.Flag{ {Name: "meeting-id", Required: true, Desc: "meeting ID to leave"}, diff --git a/shortcuts/vc/vc_meeting_list_active.go b/shortcuts/vc/vc_meeting_list_active.go new file mode 100644 index 000000000..e49f9a050 --- /dev/null +++ b/shortcuts/vc/vc_meeting_list_active.go @@ -0,0 +1,113 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package vc + +import ( + "context" + "fmt" + "io" + "net/http" + "strings" + + "github.com/larksuite/cli/errs" + "github.com/larksuite/cli/internal/output" + "github.com/larksuite/cli/shortcuts/common" +) + +const vcMeetingListActiveAPIPath = "/open-apis/vc/v1/bots/user_active_meeting" + +// VCMeetingListActive lists meetings the current or target user is actively in. +var VCMeetingListActive = common.Shortcut{ + Service: "vc", + Command: "+meeting-list-active", + Description: "List active meetings for the current identity or target user", + Risk: "read", + Scopes: []string{"vc:meeting.active:read"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: []common.Flag{ + {Name: "user-id", Desc: "target user ID when using bot identity"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + return validateMeetingListActiveUserID(runtime) + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + params, err := buildMeetingListActiveParams(runtime) + if err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } + dryRun := common.NewDryRunAPI().GET(vcMeetingListActiveAPIPath) + if len(params) > 0 { + dryRun.Params(params) + } + return dryRun + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + params, err := buildMeetingListActiveParams(runtime) + if err != nil { + return err + } + data, err := runtime.CallAPITyped(http.MethodGet, vcMeetingListActiveAPIPath, params, nil) + if err != nil { + return err + } + if data == nil { + data = map[string]interface{}{} + } + meetings := common.GetSlice(data, "meetings") + runtime.OutFormat(data, &output.Meta{Count: len(meetings)}, func(w io.Writer) { + if len(meetings) == 0 { + fmt.Fprintln(w, "No active meetings.") + return + } + for i, raw := range meetings { + meeting, _ := raw.(map[string]interface{}) + if meeting == nil { + continue + } + if i > 0 { + fmt.Fprintln(w) + } + title := common.GetString(meeting, "meeting_title") + if title == "" { + title = "Untitled meeting" + } + fmt.Fprintf(w, "%s\n", title) + if id := common.GetString(meeting, "meeting_id"); id != "" { + fmt.Fprintf(w, " Meeting ID: %s\n", id) + } + if no := common.GetString(meeting, "meeting_no"); no != "" { + fmt.Fprintf(w, " Meeting No: %s\n", no) + } + } + }) + return nil + }, +} + +func validateMeetingListActiveUserID(runtime *common.RuntimeContext) error { + if !runtime.IsBot() { + return nil + } + userID := strings.TrimSpace(runtime.Str("user-id")) + if userID == "" { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id is required when --as bot").WithParam("--user-id") + } + if _, err := common.ValidateUserIDTyped("--user-id", userID); err != nil { + return err + } + return nil +} + +func buildMeetingListActiveParams(runtime *common.RuntimeContext) (map[string]interface{}, error) { + if err := validateMeetingListActiveUserID(runtime); err != nil { + return nil, err + } + params := map[string]interface{}{} + if runtime.IsBot() { + userID := strings.TrimSpace(runtime.Str("user-id")) + params["user_id"] = userID + } + return params, nil +} diff --git a/shortcuts/vc/vc_meeting_test.go b/shortcuts/vc/vc_meeting_test.go index f68842936..8d229650e 100644 --- a/shortcuts/vc/vc_meeting_test.go +++ b/shortcuts/vc/vc_meeting_test.go @@ -8,6 +8,7 @@ import ( "context" "encoding/json" "errors" + "net/http" "strings" "testing" @@ -15,6 +16,7 @@ import ( "github.com/larksuite/cli/errs" "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/internal/core" "github.com/larksuite/cli/internal/httpmock" "github.com/larksuite/cli/shortcuts/common" ) @@ -589,6 +591,270 @@ func TestMeetingLeave_Execute_APIError(t *testing.T) { } } +func TestMeetingListActive_DryRun_UserIdentity(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--dry-run", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + out := stdout.String() + if !strings.Contains(out, "/open-apis/vc/v1/bots/user_active_meeting") { + t.Errorf("dry-run should include API path, got: %s", out) + } + if strings.Contains(out, "user_id") { + t.Errorf("user identity should not send user_id by default, got: %s", out) + } +} + +func TestMeetingListActive_DryRun_UserIdentityIgnoresUserID(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--dry-run", "--as", "user", "--user-id", "not-open-id", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if strings.Contains(stdout.String(), "user_id") { + t.Errorf("user identity should not send user_id, got: %s", stdout.String()) + } +} + +func TestMeetingListActive_Execute_UserIdentityIgnoresInvalidUserID(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + var gotUserID string + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/vc/v1/bots/user_active_meeting", + OnMatch: func(req *http.Request) { + gotUserID = req.URL.Query().Get("user_id") + }, + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{"meetings": []interface{}{}}, + }, + }) + + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--as", "user", "--user-id", "not-open-id", "--format", "json", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotUserID != "" { + t.Fatalf("user identity should not send user_id, got %q", gotUserID) + } +} + +func TestMeetingListActive_Validate_BotRequiresUserID(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingListActive, []string{"+meeting-list-active", "--as", "bot"}, f, nil) + if err == nil { + t.Fatal("expected error when --as bot omits --user-id") + } + assertMeetingListActiveUserIDValidationError(t, err) +} + +func TestMeetingListActive_Validate_UserIDOpenIDFormat(t *testing.T) { + f, _, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--as", "bot", "--user-id", "300", + }, f, nil) + if err == nil { + t.Fatal("expected error for non-open_id user-id") + } + assertMeetingListActiveUserIDValidationError(t, err) +} + +func TestMeetingListActive_Execute_BotPassesUserID(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + + var gotUserID string + stub := &httpmock.Stub{ + Method: "GET", + URL: "/open-apis/vc/v1/bots/user_active_meeting", + OnMatch: func(req *http.Request) { + gotUserID = req.URL.Query().Get("user_id") + }, + Body: map[string]interface{}{ + "code": 0, "msg": "ok", + "data": map[string]interface{}{ + "meetings": []interface{}{ + map[string]interface{}{ + "meeting_id": "9001", + "meeting_no": "123456789", + "meeting_title": "Standup", + }, + }, + }, + }, + } + reg.Register(stub) + + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--user-id", "ou_300", + "--format", "json", "--as", "bot", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if gotUserID != "ou_300" { + t.Fatalf("user_id query = %q, want ou_300", gotUserID) + } + + var resp map[string]any + if err := json.Unmarshal(stdout.Bytes(), &resp); err != nil { + t.Fatalf("failed to parse stdout: %v", err) + } + data, _ := resp["data"].(map[string]any) + meetings, _ := data["meetings"].([]any) + if len(meetings) != 1 { + t.Fatalf("meetings = %d, want 1 (envelope: %s)", len(meetings), stdout.String()) + } +} + +func TestMeetingListActive_DryRun_BotValidationErrorEnvelope(t *testing.T) { + cmd := &cobra.Command{Use: "+meeting-list-active"} + cmd.Flags().String("user-id", "", "") + runtime := common.TestNewRuntimeContextWithIdentity(cmd, defaultConfig(), core.AsBot) + + dry := VCMeetingListActive.DryRun(context.Background(), runtime) + if dry == nil { + t.Fatal("DryRun returned nil") + } + raw, err := json.Marshal(dry) + if err != nil { + t.Fatalf("failed to marshal dry-run output: %v", err) + } + got := string(raw) + if !strings.Contains(got, "--user-id") { + t.Fatalf("dry-run error = %q, want user-id validation", got) + } +} + +func TestMeetingListActive_DryRun_BotSendsUserID(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--dry-run", "--as", "bot", "--user-id", "ou_300", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "user_id") || !strings.Contains(stdout.String(), "ou_300") { + t.Fatalf("dry-run should include user_id=ou_300, got: %s", stdout.String()) + } +} + +func TestMeetingListActive_Execute_ValidationError(t *testing.T) { + cmd := &cobra.Command{Use: "+meeting-list-active"} + cmd.Flags().String("user-id", "", "") + runtime := common.TestNewRuntimeContextWithIdentity(cmd, defaultConfig(), core.AsBot) + + err := VCMeetingListActive.Execute(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error") + } + assertMeetingListActiveUserIDValidationError(t, err) +} + +func TestMeetingListActive_ExecutePretty_Empty(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/vc/v1/bots/user_active_meeting", + Body: map[string]interface{}{"code": 0, "msg": "ok"}, + }) + + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--format", "pretty", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !strings.Contains(stdout.String(), "No active meetings.") { + t.Fatalf("pretty output = %q, want empty-state message", stdout.String()) + } +} + +func TestMeetingListActive_ExecutePretty_MultipleMeetings(t *testing.T) { + f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/vc/v1/bots/user_active_meeting", + Body: map[string]interface{}{ + "code": 0, + "data": map[string]interface{}{ + "meetings": []interface{}{ + map[string]interface{}{ + "meeting_id": "9001", + "meeting_no": "123456789", + "meeting_title": "Standup", + }, + "ignored", + map[string]interface{}{}, + }, + }, + }, + }) + + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--format", "pretty", "--as", "user", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + out := stdout.String() + for _, want := range []string{"Standup", "Meeting ID: 9001", "Meeting No: 123456789", "Untitled meeting"} { + if !strings.Contains(out, want) { + t.Fatalf("pretty output missing %q: %s", want, out) + } + } +} + +func TestMeetingListActive_Execute_APIError(t *testing.T) { + f, _, _, reg := cmdutil.TestFactory(t, defaultConfig()) + reg.Register(&httpmock.Stub{ + Method: "GET", + URL: "/open-apis/vc/v1/bots/user_active_meeting", + Body: map[string]interface{}{"code": 121005, "msg": "no permission"}, + }) + + err := mountAndRun(t, VCMeetingListActive, []string{ + "+meeting-list-active", "--format", "json", "--as", "user", + }, f, nil) + if err == nil { + t.Fatal("expected API error") + } + if p, ok := errs.ProblemOf(err); !ok || p.Category != errs.CategoryAuthorization { + t.Fatalf("error problem = (%+v, %t), want authorization problem", p, ok) + } else if p.Subtype != errs.SubtypePermissionDenied { + t.Fatalf("error subtype = %q, want %q", p.Subtype, errs.SubtypePermissionDenied) + } +} + +func assertMeetingListActiveUserIDValidationError(t *testing.T, err error) { + t.Helper() + p, ok := errs.ProblemOf(err) + if !ok { + t.Fatalf("expected typed problem, got %T: %v", err, err) + } + if p.Category != errs.CategoryValidation { + t.Errorf("Category = %q, want %q", p.Category, errs.CategoryValidation) + } + if p.Subtype != errs.SubtypeInvalidArgument { + t.Errorf("Subtype = %q, want %q", p.Subtype, errs.SubtypeInvalidArgument) + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("expected *errs.ValidationError, got %T: %v", err, err) + } + if ve.Param != "--user-id" { + t.Errorf("Param = %q, want %q", ve.Param, "--user-id") + } +} + // --------------------------------------------------------------------------- // Typed error lock assertions // --------------------------------------------------------------------------- diff --git a/skills/lark-vc-agent/SKILL.md b/skills/lark-vc-agent/SKILL.md index 6d54da3a1..8704c55c6 100644 --- a/skills/lark-vc-agent/SKILL.md +++ b/skills/lark-vc-agent/SKILL.md @@ -1,7 +1,7 @@ --- name: lark-vc-agent version: 1.0.0 -description: "飞书视频会议:让机器人代当前用户加入/离开正在进行的会议,并读取会议期间的实时事件(参会人加入与离开、发言、聊天、屏幕共享等)。1. 用户提供 9 位会议号、要求代为入会或离会时使用 +meeting-join / +meeting-leave——会真实产生入会/离会记录。2. 会议进行中用户想知道“谁加入了”“谁离开了”“谁在发言”“有人共享屏幕吗”等会中动态时,机器人入会后用 +meeting-events 读取事件时间线。3. 典型场景:参会机器人、会中助手、代为旁听、代为参会。前提:机器人只能读到它自己参会过且仍在进行中的会议的事件;查询已结束会议的参会名单、纪要或逐字稿请使用 lark-vc 技能。" +description: "飞书视频会议:让机器人代当前用户加入/离开正在进行的会议,并读取会议期间的实时事件(参会人加入与离开、发言、聊天、屏幕共享等)。1. 用户提供 9 位会议号、要求代为入会或离会时使用 +meeting-join / +meeting-leave——会真实产生入会/离会记录。2. 会议进行中用户想知道“谁加入了”“谁离开了”“谁在发言”“有人共享屏幕吗”等会中动态时,用 +meeting-events 读取事件时间线:UAT 要求当前 user 在会中;TAT 要求 bot 在会中,meeting_id 来自 +meeting-join 返回或 +meeting-list-active。3. 典型场景:参会机器人、会中助手、代为旁听、代为参会。查询已结束会议的参会名单、纪要或逐字稿请使用 lark-vc 技能。" metadata: requires: bins: ["lark-cli"] @@ -33,7 +33,8 @@ metadata: | 用户意图示例 | 应路由到 | | ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | | "帮我入会 123456789"、"代我参会"、"让机器人进会旁听" | **本 skill** `+meeting-join` | -| "会议现在还开着,谁刚加入了"、"会议里谁在发言"、"有人共享屏幕吗"(**进行中会议**,且**机器人已入会**) | **本 skill** `+meeting-events` | +| "会议现在还开着,谁刚加入了"、"会议里谁在发言"、"有人共享屏幕吗"(**进行中会议**) | **本 skill** `+meeting-events` | +| "我/某个用户现在在哪个会里"、"给我找当前可拉事件的 meeting_id" | **本 skill** `+meeting-list-active` | | "退出会议"、"让机器人离开" | **本 skill** `+meeting-leave` | | "昨天那场会有谁参加过"、"搜昨天的会"、"查纪要/逐字稿/录制" | [`lark-vc`](../lark-vc/SKILL.md) | | "帮我参会,结束后把纪要发到群" 等跨阶段场景 | 按序编排:本 skill(入会 → 读事件)→ 会议结束后用 [`lark-vc`](../lark-vc/SKILL.md) / [`lark-minutes`](../lark-minutes/SKILL.md) 拉纪要 → [`lark-im`](../lark-im/SKILL.md) 发群 | @@ -46,14 +47,17 @@ metadata: 2. `+meeting-join --meeting-number` 只接受 **9 位纯数字**会议号,不是会议链接整串、也不是 `meeting_id`。 3. 返回体中的 `meeting.id` **必须立刻记录**——后续 `+meeting-events` / `+meeting-leave` 都靠它,**不能用 9 位会议号替代**。 4. 入会对所有参会人可见,执行前核实 9 位会议号来源,避免误入错会。 -5. 仅支持 `user` 身份,需提前 `lark-cli auth login`。 +5. 优先使用 `--as bot` 执行真实 bot 入会;若误用 `--as user` 且服务端返回“user access token is not supported”,切到 bot 身份重试。 6. 若入会失败,优先查看 `+meeting-join` reference 的错误排查段落,重点确认会议号、密码、会议状态、等候室 / 审批以及会议是否禁止当前身份加入。 ### 2. 感知会中事件(读操作) 1. 用户要看"会议里正在发生什么"(参会人加入/离开、聊天、转写、屏幕共享)时,用 `+meeting-events`。 2. 输入是 **`meeting_id`**(长数字 ID),不是 9 位会议号。 -3. Bot 必须**真实参会过**(先 `+meeting-join`),否则事件流通常不可见。具体的状态边界、结束后宽限窗口与错误码(如 `10005 / 20001 / 20002`)请查看 `+meeting-events` reference。 +3. 身份语义必须区分清楚: + - UAT / `--as user`:当前 user 在会中即可读取该会事件。 + - TAT / `--as bot`:bot 必须在会中或参会过;meeting_id 优先来自 `+meeting-join` 返回的 `meeting.id`,也可以先用 `+meeting-list-active --as bot --user-id ` 找到 bot 也在的当前会。 + - 不要把 TAT 理解成“任意 meeting_id 都能读”。如果 bot 没进过该会,事件流不可见。 4. **不能做会后复盘**,**不能替代参会人快照查询**。如果会议已结束: - 想拿纪要文档或逐字稿文档 token:用 `lark-cli vc +notes --meeting-ids ` - 想拿 AI 产物(summary / todos / chapters)或导出逐字稿文件:先用 `lark-cli vc +recording --meeting-ids ` 拿 `minute_token`,再用 `lark-cli vc +notes --minute-tokens ` @@ -66,12 +70,19 @@ metadata: ### 3. 离开会议(写操作) -1. 只有用户明确要求机器人退出 / 离开 / 结束参会时,才用 `+meeting-leave --meeting-id <从 +meeting-join 拿到的 meeting.id>`;不要把任务完成当作离会指令。 +1. 只有用户明确要求机器人退出 / 离开 / 结束参会时,才用 `+meeting-leave --meeting-id <从 +meeting-join 或 +meeting-list-active 拿到的 meeting.id>`;不应因任务完成而执行离会。 2. `--meeting-id` **必须**是 `+meeting-join` 返回的长数字 `meeting.id`,**不接受 9 位会议号**。 3. 离会**立即生效**,机器人从会议的参会人列表中消失,对其他参会人可见;若需要重新入会,再跑一次 `+meeting-join` 即可(非真正"不可逆")。 -4. 仅支持 `user` 身份。 +4. 优先使用与入会相同的 bot 身份离会。 -### 4. Agent 参会示范 +### 4. 获取当前可用的进行中会议 ID(读操作) + +1. `+meeting-list-active` 用来发现当前进行中的会议,并拿到后续 `+meeting-events` 需要的长数字 `meeting_id`。 +2. UAT / `--as user`:不需要传 `--user-id`,返回当前登录用户正在参加的会议。 +3. TAT / `--as bot`:必须传 `--user-id `,即 `ou_...`;返回该用户当前正在参加且 bot 也在会中的会议。它不是全量会议搜索接口。 +4. 如果 TAT 返回空,说明没有找到“目标用户在会中且 bot 也在会中”的当前会;先让 bot 通过 `+meeting-join` 入会,或确认传入的 `user_id` 和会议状态。 + +### 5. Agent 参会示范 ```bash # 1. 入会,捕获 meeting.id @@ -89,6 +100,13 @@ lark-cli vc +notes --meeting-ids "$MID" 如果用户随后明确要求退出 / 离开 / 结束参会,再单独调用 `lark-cli vc +meeting-leave --meeting-id "$MID"`。 +如果已经知道目标用户 `open_id`,且 bot 已在会中,也可以先发现当前会: + +```bash +lark-cli vc +meeting-list-active --as bot --user-id --format json +lark-cli vc +meeting-events --as bot --meeting-id --page-all --format pretty +``` + ## Shortcuts Shortcut 是对常用操作的高级封装(`lark-cli vc + [flags]`)。 @@ -96,11 +114,13 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc + [flags]`)。 | Shortcut | 类型 | 说明 | | --------------------------------------------------------------- | -- | -------------------------------------------------------------------------- | | [`+meeting-join`](references/lark-vc-agent-meeting-join.md) | 写 | Join an in-progress meeting by 9-digit meeting number | +| [`+meeting-list-active`](references/lark-vc-agent-meeting-list-active.md) | 读 | List active meetings and discover meeting_id for event reads | | [`+meeting-events`](references/lark-vc-agent-meeting-events.md) | 读 | List bot meeting events (participant joined/left, transcript, chat, share) | | [`+meeting-leave`](references/lark-vc-agent-meeting-leave.md) | 写 | Leave a meeting by meeting\_id | - 使用 `+meeting-join` 前**必须**阅读 [references/lark-vc-agent-meeting-join.md](references/lark-vc-agent-meeting-join.md),了解入参格式与写操作可见性风险。 -- 使用 `+meeting-events` 前**必须**阅读 [references/lark-vc-agent-meeting-events.md](references/lark-vc-agent-meeting-events.md),了解 `meeting_id` 来源、分页、错误码(10005 / 20001 / 20002)与 "bot 仍在会中" 硬约束。 +- 使用 `+meeting-list-active` 前**必须**阅读 [references/lark-vc-agent-meeting-list-active.md](references/lark-vc-agent-meeting-list-active.md),了解 UAT/TAT 的 `user_id` 和 bot-in-meeting 过滤语义。 +- 使用 `+meeting-events` 前**必须**阅读 [references/lark-vc-agent-meeting-events.md](references/lark-vc-agent-meeting-events.md),了解 `meeting_id` 来源、分页、错误码(10005 / 20001 / 20002)与 UAT/TAT 身份边界。 - 使用 `+meeting-leave` 前**必须**阅读 [references/lark-vc-agent-meeting-leave.md](references/lark-vc-agent-meeting-leave.md),了解 `meeting_id` 的来源与写操作可见性。 ## 权限表 @@ -108,6 +128,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc + [flags]`)。 | Shortcut | 所需 scope | | ----------------- | ------------------------------ | | `+meeting-join` | `vc:meeting.bot.join:write` | +| `+meeting-list-active` | `vc:meeting.active:read` | | `+meeting-events` | `vc:meeting.meetingevent:read` | | `+meeting-leave` | `vc:meeting.bot.join:write` | diff --git a/skills/lark-vc-agent/references/lark-vc-agent-meeting-events.md b/skills/lark-vc-agent/references/lark-vc-agent-meeting-events.md index 273c91e9f..5ea1dd2e6 100644 --- a/skills/lark-vc-agent/references/lark-vc-agent-meeting-events.md +++ b/skills/lark-vc-agent/references/lark-vc-agent-meeting-events.md @@ -3,10 +3,15 @@ > **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 -查询当前 bot 在一场正在进行的视频会议中收到的会中事件列表。该命令是**读操作**。对进行中会议,要求 bot 当前仍在会中;对已结束会议,存在一个**结束后 5 分钟内的宽限窗口**,只要 bot 曾经在这场会里出现过,仍可继续拉取事件。 +查询一场正在进行的视频会议中的会中事件列表。该命令是**读操作**。UAT / `--as user` 要求当前 user 在会中;TAT / `--as bot` 要求 bot 在会中或参会过。对已结束会议,存在一个**结束后 5 分钟内的宽限窗口**,只要对应身份曾经在这场会里出现过,仍可继续拉取事件。 本 skill 对应 shortcut:`lark-cli vc +meeting-events`(调用 `GET /open-apis/vc/v1/bots/events`)。 +身份边界: + +- UAT / `--as user`:当前 user 在该会中即可读取事件。 +- TAT / `--as bot`:bot 必须在该会中或参会过;`meeting_id` 通常来自 `+meeting-join` 返回的 `meeting.id`,也可以通过 `+meeting-list-active --as bot --user-id ` 发现。TAT 不表示可以读取任意 meeting_id。 + ## 命令 ```bash @@ -45,31 +50,38 @@ lark-cli vc +meeting-events --meeting-id 69xxxxxxxxxxxxx28 --dry-run `--meeting-id` 必须是会议的长数字 ID。它通常来自: - `+meeting-join` 返回体中的 `meeting.id` +- `+meeting-list-active` 返回体中的 `meeting_id` - `+search` 结果中的 `id` **不要**把 9 位会议号(`--meeting-number`)传给这个命令。 -### 2. 仅支持 user 身份 +### 2. 支持 UAT 和 TAT,但权限锚点不同 -该命令仅支持 `user` 身份。 +- UAT / `--as user`:当前 user 在会中即可。 +- TAT / `--as bot`:bot 必须在会中或参会过;不要拿任意 meeting_id 直接查。 -### 3. bot 必须在会中,或在会议结束后的 5 分钟宽限窗口内曾经在会中 +### 3. 读取事件前必须先拿到可见的 meeting_id -这是查询“bot 在会中观察到的事件”的接口。若 bot 已离会、未入会、或会议已经无法再判断 bot 身份,后端通常会报: -- `bot is not in meeting, no permission` - -因此,最稳妥的调用顺序通常是: +TAT 场景最稳妥的调用顺序通常是: ```bash -# 先入会 -lark-cli vc +meeting-join --meeting-number 123456789 - -# 记录返回的 meeting.id +# 方式 1:先入会,直接记录返回的 meeting.id +lark-cli vc +meeting-join --as bot --meeting-number 123456789 # 再查询事件 -lark-cli vc +meeting-events --meeting-id +lark-cli vc +meeting-events --as bot --meeting-id ``` +如果 bot 已经在会中,也可以先通过 active meeting 找会: + +```bash +lark-cli vc +meeting-list-active --as bot --user-id --format json +lark-cli vc +meeting-events --as bot --meeting-id --page-all --format pretty +``` + +若对应身份已离会、未入会、或会议已经无法再判断身份,后端通常会报: +- `bot is not in meeting, no permission` + 更精确地说,后端当前的判断规则是: - **会议进行中**:要求 bot **当前仍在会中** @@ -171,7 +183,7 @@ lark-cli vc +meeting-events --meeting-id | 输入参数 | 获取方式 | |---------|---------| -| `meeting-id` | `+meeting-join` 返回的 `meeting.id`;或 `+search` 结果中的 `id` | +| `meeting-id` | `+meeting-join` 返回的 `meeting.id`;或 `+meeting-list-active` 返回的 `meeting_id`;或 `+search` 结果中的 `id` | | `start` / `end` | 用户给出的时间范围;如未给出则默认取全量可见事件 | | `page-token` | 上一页或上一次查询结果中保存的 `page_token`;建议持久化保存,便于下次继续拉取新增事件 | @@ -187,6 +199,13 @@ lark-cli vc +meeting-join --meeting-number 123456789 lark-cli vc +meeting-events --meeting-id --page-all --format pretty ``` +### 场景 1b:TAT 下 bot 已在会中,先发现 meeting_id 再读事件 + +```bash +lark-cli vc +meeting-list-active --as bot --user-id --format json +lark-cli vc +meeting-events --as bot --meeting-id --page-all --format pretty +``` + ### 场景 2:过滤某段时间内的事件 ```bash @@ -238,6 +257,7 @@ lark-cli vc +meeting-events \ ## 参考 - [lark-vc-agent-meeting-join](lark-vc-agent-meeting-join.md) — 先真实入会 +- [lark-vc-agent-meeting-list-active](lark-vc-agent-meeting-list-active.md) — 发现当前可读事件的进行中会议 ID - [lark-vc-agent-meeting-leave](lark-vc-agent-meeting-leave.md) — 用户明确要求时离会 - [lark-vc-search](../../lark-vc/references/lark-vc-search.md) — 搜索历史会议(获取 meeting_id) - [lark-vc-recording](../../lark-vc/references/lark-vc-recording.md) — 查询 minute_token diff --git a/skills/lark-vc-agent/references/lark-vc-agent-meeting-join.md b/skills/lark-vc-agent/references/lark-vc-agent-meeting-join.md index 2bb49b9e1..db7191f67 100644 --- a/skills/lark-vc-agent/references/lark-vc-agent-meeting-join.md +++ b/skills/lark-vc-agent/references/lark-vc-agent-meeting-join.md @@ -38,9 +38,9 @@ lark-cli vc +meeting-join --meeting-number 123456789 --dry-run ## 核心约束 -### 1. 仅支持 user 身份 +### 1. 优先使用 bot 身份 -该命令仅支持 `user` 身份。 +这是 bot 入会能力,优先使用 `--as bot`。如果误用 `--as user` 且服务端返回“user access token is not supported”,切到 bot 身份重试。 ### 2. 会议号格式严格校验 @@ -94,6 +94,12 @@ lark-cli vc +meeting-join --meeting-number 123456789 lark-cli vc +meeting-events --meeting-id --page-all --format pretty ``` +如果 bot 已经在会中,也可以通过 active meeting 找回 `meeting_id`: + +```bash +lark-cli vc +meeting-list-active --as bot --user-id --format json +``` + ### 场景 2:加入会议 → 会后拉取纪要 / 录制 ```bash @@ -126,6 +132,7 @@ lark-cli vc +notes --meeting-ids ## 参考 - [lark-vc-agent-meeting-leave](lark-vc-agent-meeting-leave.md) — 对应的离会命令 +- [lark-vc-agent-meeting-list-active](lark-vc-agent-meeting-list-active.md) — 发现当前可读事件的进行中会议 ID - [lark-vc-agent-meeting-events](lark-vc-agent-meeting-events.md) — 会中事件流 - [lark-vc-search](../../lark-vc/references/lark-vc-search.md) — 搜索历史会议记录 - [lark-vc-recording](../../lark-vc/references/lark-vc-recording.md) — 查询 minute_token diff --git a/skills/lark-vc-agent/references/lark-vc-agent-meeting-leave.md b/skills/lark-vc-agent/references/lark-vc-agent-meeting-leave.md index 998e31e7e..d8c98f820 100644 --- a/skills/lark-vc-agent/references/lark-vc-agent-meeting-leave.md +++ b/skills/lark-vc-agent/references/lark-vc-agent-meeting-leave.md @@ -32,11 +32,11 @@ lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 --dry-run ### 1. 入参是 meeting_id,不是会议号 -`--meeting-id` 必须是会议的长数字 ID,通常由 `+meeting-join` 返回体中的 `meeting.id` 提供,也可从 `+search` 结果中的 `id` 字段获取。**传 9 位会议号会失败**。 +`--meeting-id` 必须是会议的长数字 ID,通常由 `+meeting-join` 返回体中的 `meeting.id` 提供,也可从 `+meeting-list-active` 返回体中的 `meeting_id` 获取。**传 9 位会议号会失败**。 -### 2. 仅支持 user 身份 +### 2. 优先使用 bot 身份 -该命令仅支持 `user` 身份。只能让当前身份自己离会,无法强制移出其他参会人。 +这是 bot 离会能力,优先使用与入会相同的 `--as bot`。只能让当前身份自己离会,无法强制移出其他参会人。 ### 3. 当前身份必须在会议中 @@ -55,7 +55,7 @@ lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 --dry-run | 输入参数 | 获取方式 | |---------|---------| -| `meeting-id` | `+meeting-join` 返回的 `meeting.id`;或 `+search` 结果中的 `id` 字段 | +| `meeting-id` | `+meeting-join` 返回的 `meeting.id`;或 `+meeting-list-active` 返回的 `meeting_id` | ## Agent 组合场景 @@ -63,13 +63,13 @@ lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 --dry-run ```bash # 第 1 步:加入会议,记录 meeting.id -lark-cli vc +meeting-join --meeting-number 123456789 +lark-cli vc +meeting-join --as bot --meeting-number 123456789 # 第 2 步:在会中处理用户请求(如监听发言、记录信息等) # ... # 第 3 步:仅在用户明确要求退出 / 离开 / 结束参会时,使用上一步记录的 meeting.id 离会 -lark-cli vc +meeting-leave --meeting-id +lark-cli vc +meeting-leave --as bot --meeting-id ``` ### 场景 2:会后补拉产物(不需要离会) @@ -96,11 +96,12 @@ lark-cli vc +notes --meeting-ids - 只有用户明确要求退出 / 离开 / 结束参会时才调用;离会会让机器人从参会列表消失,对其他参会人可见。若需要重新入会直接再 `+meeting-join`,不是真正的"不可逆"。参数格式不确定时可选 `--dry-run` 预览。 - `+meeting-leave` 依赖 `+meeting-join` 返回的 `meeting.id`,但不是每次 join 后都必须调用 leave。 -- `meeting_id` 优先使用 `+meeting-join` 返回的 `meeting.id`;如果来自 `+search`,也必须先确认当前身份就在该会议中。不要用 9 位会议号。 +- `meeting_id` 优先使用 `+meeting-join` 返回的 `meeting.id`;如果来自 `+meeting-list-active`,也必须确认当前身份就在该会议中。不要用 9 位会议号。 ## 参考 - [lark-vc-agent-meeting-join](lark-vc-agent-meeting-join.md) — 对应的入会命令 +- [lark-vc-agent-meeting-list-active](lark-vc-agent-meeting-list-active.md) — 发现当前可读事件的进行中会议 ID - [lark-vc-agent-meeting-events](lark-vc-agent-meeting-events.md) — 会中事件流 - [lark-vc-search](../../lark-vc/references/lark-vc-search.md) — 搜索历史会议(获取 meeting_id) - [lark-vc-recording](../../lark-vc/references/lark-vc-recording.md) — 查询 minute_token diff --git a/skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md b/skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md new file mode 100644 index 000000000..fb620e840 --- /dev/null +++ b/skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md @@ -0,0 +1,70 @@ +# vc +meeting-list-active + +> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、身份切换和权限处理。 + +列出当前进行中的会议,用来发现 `+meeting-events` 需要的长数字 `meeting_id`。 + +本 skill 对应 shortcut:`lark-cli vc +meeting-list-active`(调用 `GET /open-apis/vc/v1/bots/user_active_meeting`)。 + +## 命令 + +```bash +# UAT:查询当前登录用户正在参加的会议 +lark-cli vc +meeting-list-active --as user --format pretty + +# TAT:查询指定 open_id 当前参加、且 bot 也在会中的会议 +lark-cli vc +meeting-list-active --as bot --user-id ou_xxx --format json + +# 预览 API 调用 +lark-cli vc +meeting-list-active --as bot --user-id ou_xxx --dry-run +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--user-id ` | TAT 必填,UAT 不需要 | 目标用户 open_id,格式为 `ou_...`。CLI 直接透传给接口,不接受 internal user_id 或数字 ID | +| `--format ` | 否 | 输出格式:json / pretty / table / ndjson / csv | +| `--dry-run` | 否 | 预览 API 调用,不执行 | + +## 身份语义 + +### UAT / `--as user` + +- 不传 `--user-id`。 +- 返回当前登录用户正在参加的会议。 +- 后续可直接用返回的 `meeting_id` 调用: + +```bash +lark-cli vc +meeting-events --as user --meeting-id --page-all --format pretty +``` + +### TAT / `--as bot` + +- 必须传 `--user-id `,即 `ou_...`。 +- 返回该用户当前正在参加、且 bot 也在会中的会议。 +- 如果返回空,不代表用户不在任何会议中,只能说明没有找到“用户在会中且 bot 也在会中”的当前会。 +- TAT 读取事件仍然要求 bot 在会中。常见流程是: + +```bash +# 方式 1:先让 bot 入会,直接从 join 响应拿 meeting.id +lark-cli vc +meeting-join --as bot --meeting-number 123456789 --format json +lark-cli vc +meeting-events --as bot --meeting-id --page-all --format pretty + +# 方式 2:bot 已经在会中时,用 active meeting 发现 meeting_id +lark-cli vc +meeting-list-active --as bot --user-id --format json +lark-cli vc +meeting-events --as bot --meeting-id --page-all --format pretty +``` + +## 常见错误与排查 + +| 错误现象 | 根本原因 | 解决方案 | +|---------|---------|---------| +| `--user-id is required when --as bot` | TAT 未传目标用户 | 传入目标用户 open_id | +| 返回空列表 | 没有满足“目标用户在会中且 bot 也在会中”的当前会 | 先让 bot 入会,或确认 `user_id` 和会议状态 | +| `--user-id` 格式错误 | 传入了 internal user_id 或其他非 `ou_...` 值 | 改传目标用户 open_id | + +## 参考 + +- [lark-vc-agent-meeting-join](lark-vc-agent-meeting-join.md) — 让 bot 真实入会并拿 `meeting.id` +- [lark-vc-agent-meeting-events](lark-vc-agent-meeting-events.md) — 使用 `meeting_id` 读取会中事件