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..48df42ac5 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"}, @@ -156,6 +156,9 @@ func validateMeetingEventsMeetingID(meetingID string) error { if meetingID == "" { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--meeting-id is required").WithParam("--meeting-id") } + if validMeetingNumber(meetingID) { + return errs.NewValidationError(errs.SubtypeInvalidArgument, "--meeting-id must be a long meeting_id, not a 9-digit meeting number; use +meeting-join or +meeting-list-active to get meeting_id").WithParam("--meeting-id") + } value, err := strconv.ParseInt(meetingID, 10, 64) if err != nil || value <= 0 { return errs.NewValidationError(errs.SubtypeInvalidArgument, "--meeting-id must be a positive integer, got %q", meetingID).WithParam("--meeting-id") diff --git a/shortcuts/vc/vc_meeting_events_test.go b/shortcuts/vc/vc_meeting_events_test.go index f0cb3e0ec..2e9e73547 100644 --- a/shortcuts/vc/vc_meeting_events_test.go +++ b/shortcuts/vc/vc_meeting_events_test.go @@ -262,6 +262,26 @@ func TestMeetingEvents_Validation_InvalidMeetingID(t *testing.T) { } } +func TestMeetingEvents_Validation_RejectsMeetingNumber(t *testing.T) { + runtime := newMeetingEventsRuntime() + mustSetMeetingEventsFlag(t, runtime, "meeting-id", "732067044") + + err := VCMeetingEvents.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error for 9-digit meeting number") + } + if !strings.Contains(err.Error(), "not a 9-digit meeting number") { + t.Fatalf("unexpected error: %v", err) + } + var ve *errs.ValidationError + if !errors.As(err, &ve) { + t.Fatalf("expected *errs.ValidationError, got %T: %v", err, err) + } + if ve.Param != "--meeting-id" { + t.Errorf("Param = %q, want %q", ve.Param, "--meeting-id") + } +} + func TestMeetingEvents_Validation_InvalidTimeRange(t *testing.T) { runtime := newMeetingEventsRuntime() mustSetMeetingEventsFlag(t, runtime, "meeting-id", "7628568141510692381") @@ -818,7 +838,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..0e612523d --- /dev/null +++ b/shortcuts/vc/vc_meeting_list_active.go @@ -0,0 +1,121 @@ +// 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.meetingevent: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 + } + displayedMeetings := 0 + for _, raw := range meetings { + meeting, _ := raw.(map[string]interface{}) + if meeting == nil { + continue + } + if displayedMeetings > 0 { + fmt.Fprintln(w) + } + displayedMeetings++ + 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) + } + } + if displayedMeetings > 1 { + fmt.Fprintln(w) + fmt.Fprintln(w, "Multiple active meetings found. Ask the user to choose one meeting_id before calling +meeting-events.") + } + }) + return nil + }, +} + +// validateMeetingListActiveUserID validates the target user only for bot identity. +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 +} + +// buildMeetingListActiveParams builds the query params for active meeting lookup. +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..60286bd90 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,335 @@ 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_ScopeMatchesEventReadPermission(t *testing.T) { + if len(VCMeetingListActive.Scopes) != 1 || VCMeetingListActive.Scopes[0] != "vc:meeting.meetingevent:read" { + t.Fatalf("scopes = %#v, want [vc:meeting.meetingevent:read]", VCMeetingListActive.Scopes) + } +} + +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_SingleMeetingNoSelectionPrompt(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", + }, + }, + }, + }, + }) + + 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"} { + if !strings.Contains(out, want) { + t.Fatalf("pretty output missing %q: %s", want, out) + } + } + if strings.Contains(out, "Multiple active meetings found") { + t.Fatalf("single meeting should not show selection prompt: %s", out) + } +} + +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{}{ + "meeting_id": "9002", + "meeting_no": "987654321", + "meeting_title": "Planning", + }, + map[string]interface{}{ + "meeting_id": "9003", + }, + }, + }, + }, + }) + + 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", + "Planning", + "Meeting ID: 9002", + "Meeting No: 987654321", + "Untitled meeting", + "Meeting ID: 9003", + "Multiple active meetings found. Ask the user to choose one meeting_id before calling +meeting-events.", + } { + 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) + } else if p.Code != 121005 { + t.Fatalf("error code = %d, want 121005", p.Code) + } + var pe *errs.PermissionError + if !errors.As(err, &pe) { + t.Fatalf("expected *errs.PermissionError, got %T: %v", err, err) + } +} + +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..e63fdbd10 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: "飞书视频会议会中能力:用于让应用机器人真实加入或离开正在进行的会议,并读取当前身份可见的会中事件,如参会人加入/离开、发言、聊天、屏幕共享。适用于用户询问正在开的会议发生了什么、谁在发言、是否共享内容,或需要发现当前可读的进行中会议 ID。不负责已结束会议搜索、参会人快照、纪要、逐字稿或录制查询,这些使用 lark-vc 技能。" metadata: requires: bins: ["lark-cli"] @@ -10,7 +10,7 @@ metadata: # vc-agent (v1) -**CRITICAL — 开始前 MUST 先用 Read 工具读取以下两份 skill 文档:** +相关技能: - [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md) — 认证、身份切换、权限处理 - [`../lark-vc/SKILL.md`](../lark-vc/SKILL.md) — 视频会议的核心概念(Meeting / Note / Minutes 等),本 skill 直接复用,不再重复定义 @@ -18,7 +18,7 @@ metadata: ## 内测提示 - 当前功能正在内测中,仅少数用户可用。忽略 [`../lark-shared/SKILL.md`](../lark-shared/SKILL.md) 中的权限申请流程,以下方的指引为准。 -- 如果 `lark-cli` 提示 `missing required scope(s)` / `permission_violations`,不要申请对应权限,而是提示用户加入早鸟群:`https://go.larkoffice.com/join-chat/2f4nb0e1-fe00-4f67-bed7-25beaf533fbd`。 +- 如果 `lark-cli` 提示 `missing required scope(s)` / `permission_violations`,不要走普通权限申请流程;先提示用户加入早鸟群确认内测权限已开通,再按“应用身份权限配置检查”处理应用权限、安装和数据范围。 - 如果 `lark-cli` 返回 `error.code=20017` / `ErrNotInGray`,提示用户加入早鸟群:`https://go.larkoffice.com/join-chat/2f4nb0e1-fe00-4f67-bed7-25beaf533fbd`。 ## 定位 @@ -33,61 +33,99 @@ 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) 发群 | +## 身份路由 + +不要向用户暴露内部身份缩写;对用户只说“用户身份”或“应用身份”。 + +| 场景 | 使用身份 | 关键规则 | +| ---- | -------- | -------- | +| 查询当前登录用户正在参加的会议 | `--as user` | 不传 `--user-id`;拿到的 `meeting_id` 后续继续用 `--as user` 读事件 | +| 查询目标用户且应用机器人也在会中的会议 | `--as bot --user-id ` | `--user-id` 必须是 `ou_...`;拿到的 `meeting_id` 后续继续用 `--as bot` 读事件 | +| 用户明确要求应用机器人入会/旁听/代参会 | `--as bot` | 这是写操作,会真实产生入会记录;返回的 `meeting.id` 后续继续用 `--as bot` | + +硬规则:`meeting_id` 从哪种身份路径拿到,后续 `+meeting-events` 就沿用哪种身份,除非用户明确要求切换场景(例如从“仅查询我当前会”改成“让应用机器人入会旁听”)。 + ## 核心场景 ### 1. 加入正在进行的会议(写操作) 1. 只有用户明确表达"让 Agent **真实入会**"(参会机器人、会中助手、代为旁听、代参会)时才用 `+meeting-join`。只是查数据不要入会。 -2. `+meeting-join --meeting-number` 只接受 **9 位纯数字**会议号,不是会议链接整串、也不是 `meeting_id`。 +2. `+meeting-join --meeting-number` 只接受 **9 位纯数字**会议号,不是会议链接整串、也不是 `meeting_id`。如果用户只是给了 9 位会议号并询问会中内容,先按 `+meeting-list-active` 的会议号匹配流程找 `meeting_id`,不要直接入会。 3. 返回体中的 `meeting.id` **必须立刻记录**——后续 `+meeting-events` / `+meeting-leave` 都靠它,**不能用 9 位会议号替代**。 4. 入会对所有参会人可见,执行前核实 9 位会议号来源,避免误入错会。 -5. 仅支持 `user` 身份,需提前 `lark-cli auth login`。 +5. 使用应用身份 `--as 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. 不依赖默认身份。`meeting_id` 来自用户身份发现时,继续用 `--as user`;来自应用身份发现或 `+meeting-join` 时,继续用 `--as 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 ` + - 先用 `lark-cli vc +notes --meeting-ids ` 获取会议产物信息。 + - 再根据 `note_display_type`、`note_id`、`minute_token` 和用户意图,按 [`lark-vc`](../lark-vc/SKILL.md) 的产物决策读取正文、逐字稿或妙记。 - 想看参会人快照:用 `vc meeting get --with-participants`(见 [`lark-vc`](../lark-vc/SKILL.md)) 5. **默认必须使用** **`--page-all`**,除非用户明确要求“只查一页”,或确实需要控制返回体大小。 6. 输出格式默认优先 `--format pretty`(时间线更易读);只有在需要完整保留原始消息流与结构化字段时,才使用 `--format json`。 7. **必须识别分页信号**:只要响应里出现 `has_more=true`、pretty 里的 `more available`,或返回了非空 `page_token`,就不能把当前结果当作完整事件流;默认应继续分页,或明确告诉用户当前只是部分结果。 8. 保留响应里的 `page_token`,下次增量拉取直接续,不要从头再拉。 9. **只要你是基于** **`+meeting-events`** **来回答一场正在进行中的会议内容,就不能直接复用旧结果。** 无论用户是在问“现在/刚刚/最新”的状态,还是让你“总结一下这个会议讲什么”,都必须先重新拉一次当前事件流,确认拿到的是最新信息,再基于最新结果回答。只有在用户明确要求基于某次历史快照继续分析时,才可以复用旧结果。 +10. 用户直接问“这个会议讲了什么 / 现在讲到哪了”且上下文没有明确 `meeting_id` 时,先用用户身份发现当前会议;如果用户明确要求应用机器人视角,或上下文已经是应用机器人参会流程,再用应用身份发现。若返回多个会议,展示候选并让用户选择。 +11. 用户直接提供 **9 位会议号** 并询问会中事件/会议内容时,默认把它当作 active meeting 的筛选条件:先按当前身份查 active meetings,并在返回里匹配 `meeting_no == <9位会议号>`;匹配到唯一会议后取长数字 `meeting_id`,再用同一身份查事件。只有用户明确要求“入会 / 让应用机器人旁听 / 代我参会”时才改用 `+meeting-join`。 ### 3. 离开会议(写操作) -1. 只有用户明确要求机器人退出 / 离开 / 结束参会时,才用 `+meeting-leave --meeting-id <从 +meeting-join 拿到的 meeting.id>`;不要把任务完成当作离会指令。 -2. `--meeting-id` **必须**是 `+meeting-join` 返回的长数字 `meeting.id`,**不接受 9 位会议号**。 +1. 只有用户明确要求机器人退出 / 离开 / 结束参会时,才用应用身份执行 `+meeting-leave --as bot --meeting-id <长数字 meeting_id>`;不应因任务完成而执行离会。 +2. `--meeting-id` **必须**是长数字会议 ID,通常来自 `+meeting-join` 返回的 `meeting.id`,也可以来自应用身份 `+meeting-list-active` 返回的 `meeting_id`。如果来自 list-active,必须确认应用机器人当前就在该会中。**不接受 9 位会议号**。 3. 离会**立即生效**,机器人从会议的参会人列表中消失,对其他参会人可见;若需要重新入会,再跑一次 `+meeting-join` 即可(非真正"不可逆")。 -4. 仅支持 `user` 身份。 +4. 使用与入会或 active meeting 发现相同的应用身份离会。 -### 4. Agent 参会示范 +### 4. 获取当前可用的进行中会议 ID(读操作) + +1. `+meeting-list-active` 用来发现当前进行中的会议,并拿到后续 `+meeting-events` 需要的长数字 `meeting_id`。 +2. 用户身份:`lark-cli vc +meeting-list-active --as user --format json`,用于发现当前登录用户正在参加的会议;后续 `+meeting-events` 继续 `--as user`。 +3. 应用身份:`lark-cli vc +meeting-list-active --as bot --user-id --format json`,`--user-id` 必须是目标用户 open_id,即 `ou_...`;返回该用户当前正在参加且应用机器人也在会中的会议。它不是全量会议搜索接口。后续 `+meeting-events` 继续 `--as bot`。 +4. 如果返回空,先按当前身份解释:用户身份下表示当前用户没有可见的进行中会议;应用身份下表示没有找到“目标用户在会中且应用机器人也在会中”的当前会。 +5. 如果返回多个会议,不要自动任选一个;按 `meeting_title` / `meeting_no` / `meeting_id` 展示候选,等待用户明确选择后再调用 `+meeting-events`。 +6. 如果用户给了 9 位会议号,先在 active meeting 结果中按 `meeting_no` 匹配。匹配失败时,不要自动入会;只有用户明确要求应用机器人真实入会时,才询问或执行 `+meeting-join`。 + +### 5. Agent 参会示范 ```bash # 1. 入会,捕获 meeting.id -JOIN=$(lark-cli vc +meeting-join --meeting-number 123456789 --format json) +JOIN=$(lark-cli vc +meeting-join --as bot --meeting-number 123456789 --format json) MID=$(echo "$JOIN" | jq -r '.data.meeting.id') # 2. 会中轮询事件 # 默认用 --page-all 拉全当前可见事件;下次增量优先复用 page_token # 典型间隔 10-30 秒 -lark-cli vc +meeting-events --meeting-id "$MID" --page-all --format pretty +lark-cli vc +meeting-events --as bot --meeting-id "$MID" --page-all --format pretty -# 3. 会后可选:取纪要 / 逐字稿(跨到 lark-vc) +# 3. 会后可选:进入 lark-vc 获取会议产物信息,再按 note_display_type / minute_token 决策读取 lark-cli vc +notes --meeting-ids "$MID" ``` -如果用户随后明确要求退出 / 离开 / 结束参会,再单独调用 `lark-cli vc +meeting-leave --meeting-id "$MID"`。 +如果用户随后明确要求退出 / 离开 / 结束参会,再单独调用 `lark-cli vc +meeting-leave --as bot --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 +``` + +如果只是回答当前登录用户所在会议发生了什么,使用用户身份一路查: + +```bash +lark-cli vc +meeting-list-active --as user --format json +lark-cli vc +meeting-events --as user --meeting-id --page-all --format pretty +``` ## Shortcuts @@ -96,20 +134,31 @@ 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-events`](references/lark-vc-agent-meeting-events.md) | 读 | List bot meeting events (participant joined/left, transcript, chat, share) | +| [`+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 meeting events visible to the app agent (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-leave` 前**必须**阅读 [references/lark-vc-agent-meeting-leave.md](references/lark-vc-agent-meeting-leave.md),了解 `meeting_id` 的来源与写操作可见性。 +- [`+meeting-join`](references/lark-vc-agent-meeting-join.md):入参格式、写操作可见性风险、入会失败排查。 +- [`+meeting-list-active`](references/lark-vc-agent-meeting-list-active.md):用户身份和应用身份的不同返回范围。 +- [`+meeting-events`](references/lark-vc-agent-meeting-events.md):`meeting_id` 来源、身份延续、分页和错误码(10005 / 20001 / 20002)。 +- [`+meeting-leave`](references/lark-vc-agent-meeting-leave.md):`meeting_id` 的来源与写操作可见性。 -## 权限表 +## 应用身份权限配置检查 -| Shortcut | 所需 scope | -| ----------------- | ------------------------------ | -| `+meeting-join` | `vc:meeting.bot.join:write` | -| `+meeting-events` | `vc:meeting.meetingevent:read` | -| `+meeting-leave` | `vc:meeting.bot.join:write` | +应用身份 `--as bot` 报 `no permission`、`missing required scope(s)`、`permission_violations`、`ErrNotInGray` 或 `20017` 时,不要引导用户执行 `auth login`。按顺序检查: + +1. 以 CLI 返回的 metadata / error envelope 为准,确认提示的 VC Agent 相关权限已开通。常见读取 active meeting / events 需要会中事件读取权限;应用机器人入会 / 离会需要 bot 入会写权限。 +2. 应用已发布并安装到当前租户。 +3. 开放平台“权限可访问的数据范围”已开通并保存。 +4. 数据范围选择“按条件筛选”,条件配置为:**会议的归属者 包含 与应用的可用范围一致**。 +5. 如果 scope、安装和数据范围都正确,仍返回 `ErrNotInGray` / `20017`,再按 VC Agent 内测 privilege / 灰度白名单处理,提示加入早鸟群或联系平台同学开通。 + +## 用户身份被拒绝时 + +用户身份 `--as user` 报权限或身份不支持类错误时,不要反复引导用户执行 `auth login`。先以 CLI 返回的 metadata / error envelope 为准判断:如果错误表明当前接口不支持用户身份访问,再按用户意图切换处理: + +1. 如果用户只是查询当前登录用户所在的进行中会议,说明当前接口链路不支持用户身份访问,改用应用身份流程;需要目标用户 open_id,并要求应用机器人已在会中或先按用户确认执行入会。 +2. 如果用户明确要求应用机器人入会、旁听、代参会或读取应用机器人可见事件,直接切到 `--as bot`,并按上面的应用身份权限配置检查处理。 ## 延伸 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..051be4b14 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 @@ -1,29 +1,30 @@ # vc +meeting-events -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - -查询当前 bot 在一场正在进行的视频会议中收到的会中事件列表。该命令是**读操作**。对进行中会议,要求 bot 当前仍在会中;对已结束会议,存在一个**结束后 5 分钟内的宽限窗口**,只要 bot 曾经在这场会里出现过,仍可继续拉取事件。 +查询一场正在进行的视频会议中的会中事件列表。该命令是**读操作**,必须沿用 `meeting_id` 的来源身份:用户身份发现的会议继续用用户身份读,应用身份发现或应用机器人入会得到的会议继续用应用身份读。对已结束会议,存在一个**结束后 5 分钟内的宽限窗口**;应用身份读取时,要求应用机器人曾经在这场会里出现过。 本 skill 对应 shortcut:`lark-cli vc +meeting-events`(调用 `GET /open-apis/vc/v1/bots/events`)。 +可见性边界: + +- `meeting_id` 来自 `+meeting-list-active --as user`:后续读取事件继续 `--as user`。 +- `meeting_id` 来自 `+meeting-list-active --as bot --user-id ` 或 `+meeting-join --as bot`:后续读取事件继续 `--as bot`。 +- 应用身份下,应用机器人必须在该会中或参会过;应用身份 active meeting 返回的是“目标用户在会中且应用机器人也在会中”的会议,不表示可以读取任意 `meeting_id`。 + ## 命令 ```bash # 默认用法:全量拉取当前可见事件 -lark-cli vc +meeting-events --meeting-id 69xxxxxxxxxxxxx28 --page-all --format pretty +lark-cli vc +meeting-events --as --meeting-id 69xxxxxxxxxxxxx28 --page-all --format pretty # 指定时间范围,并拉全该时间窗内当前可见事件 -lark-cli vc +meeting-events --meeting-id 69xxxxxxxxxxxxx28 --start 2026-04-17T15:00:00+08:00 --end 2026-04-17T16:00:00+08:00 --page-all --format pretty +lark-cli vc +meeting-events --as --meeting-id 69xxxxxxxxxxxxx28 --start 2026-04-17T15:00:00+08:00 --end 2026-04-17T16:00:00+08:00 --page-all --format pretty # 基于上一次保存的 page_token 继续查新增事件 -lark-cli vc +meeting-events --meeting-id 69xxxxxxxxxxxxx28 --page-token --page-all --format pretty +lark-cli vc +meeting-events --as --meeting-id 69xxxxxxxxxxxxx28 --page-token --page-all --format pretty # 调试或控制返回体大小时,显式只查一页 -lark-cli vc +meeting-events --meeting-id 69xxxxxxxxxxxxx28 --page-size 20 --format json - -# 预览 API 调用(不实际请求) -lark-cli vc +meeting-events --meeting-id 69xxxxxxxxxxxxx28 --dry-run +lark-cli vc +meeting-events --as --meeting-id 69xxxxxxxxxxxxx28 --page-size 20 --format json ``` ## 参数 @@ -36,8 +37,6 @@ lark-cli vc +meeting-events --meeting-id 69xxxxxxxxxxxxx28 --dry-run | `--page-token ` | 否 | 从指定分页游标继续拉取下一页 | | `--page-size ` | 否 | 单页模式每页大小。CLI 会自动夹紧到 `20-100`;传 `--page-all` 时固定使用 `100` | | `--page-all` | 否 | 自动分页,直到没有更多页面为止(内部有安全上限) | -| `--format ` | 否 | 输出格式:json (CLI 默认) / pretty(本 skill 推荐默认) / table / ndjson / csv | -| `--dry-run` | 否 | 预览 API 调用,不执行 | ## 核心约束 @@ -45,37 +44,55 @@ 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`)传给这个命令。 +如果 `meeting_id` 来自 `+meeting-list-active`,后续 `+meeting-events` 必须沿用同一身份;如果返回多个会议,先让用户选择具体 `meeting_id`。 -### 2. 仅支持 user 身份 +如果用户提供的是 9 位会议号且没有明确要求应用机器人入会,先按当前场景身份查 active meetings 并按 `meeting_no` 匹配。匹配到唯一项后,取该项的长数字 `meeting_id`,再用同一身份调用本命令;匹配失败时不要自动入会,除非用户明确说“入会 / 让应用机器人旁听 / 代我参会”。 -该命令仅支持 `user` 身份。 +### 2. 身份来源是读取事件的权限锚点 -### 3. bot 必须在会中,或在会议结束后的 5 分钟宽限窗口内曾经在会中 +- 用户身份路径:先用 `+meeting-list-active --as user` 发现当前登录用户的会议,再用 `+meeting-events --as user` 读取该 `meeting_id`。 +- 应用身份路径:应用机器人必须在会中或参会过;不要拿任意 `meeting_id` 直接用 `--as bot` 查。 +- 不要混用身份。身份不一致时,常见结果是空列表、`no permission` 或 `bot is not in meeting`。 -这是查询“bot 在会中观察到的事件”的接口。若 bot 已离会、未入会、或会议已经无法再判断 bot 身份,后端通常会报: -- `bot is not in meeting, no permission` +### 3. 读取事件前必须先拿到可见的 meeting_id -因此,最稳妥的调用顺序通常是: +最稳妥的调用顺序通常是: ```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 ``` +如果应用机器人已经在会中,也可以先通过 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 +``` + +如果只是查询当前登录用户所在会议: + +```bash +lark-cli vc +meeting-list-active --as user --format json +lark-cli vc +meeting-events --as user --meeting-id --page-all --format pretty +``` + +若应用机器人已离会、未入会、或会议已经无法再判断身份,后端通常会报: +- `bot is not in meeting, no permission` + 更精确地说,后端当前的判断规则是: -- **会议进行中**:要求 bot **当前仍在会中** -- **会议已结束后的 5 分钟内**:只要 bot **曾经在这场会中出现过**,仍可拉取事件 +- **会议进行中**:要求应用机器人**当前仍在会中** +- **会议已结束后的 5 分钟内**:只要应用机器人**曾经在这场会中出现过**,仍可拉取事件 - **会议结束超过 5 分钟**:按会议结束处理,通常不再返回事件流 -- **bot 从未真实入会过**:即使会议仍在进行或刚结束,也会返回 `10005 bot is not in meeting` +- **应用机器人从未真实入会过**:即使会议仍在进行或刚结束,也会返回 `10005 bot is not in meeting` ### 4. 自动分页规则 @@ -87,9 +104,9 @@ lark-cli vc +meeting-events --meeting-id 执行准则: -- **默认命令模板**:`lark-cli vc +meeting-events --meeting-id --page-all --format pretty` +- **默认命令模板**:`lark-cli vc +meeting-events --as --meeting-id --page-all --format pretty` - 如果你发现自己执行成了不带 `--page-all` 的单页查询,而响应里又出现 `has_more=true` / `more available` / 非空 `page_token`,应立刻意识到这只是部分结果。 -- 遇到上述情况,默认补救方式是继续使用返回的 `page_token` 续拉,例如:`lark-cli vc +meeting-events --meeting-id --page-token --page-all --format pretty` +- 遇到上述情况,默认补救方式是继续使用返回的 `page_token` 续拉,例如:`lark-cli vc +meeting-events --as --meeting-id --page-token --page-all --format pretty` - 只有在用户明确要求“就看第一页”“先不要翻页”时,才不要默认带 `--page-all` - 只要你是基于 `+meeting-events` 来回答一场**正在进行中的会议内容**,就不能直接复用上一次查询结果。无论用户是在问“现在是谁在说话”“刚刚发生了什么”“最新事件有哪些”,还是让你“总结一下这个会议讲什么”,都必须先重新执行一次 `+meeting-events`,确认拿到的是最新事件流,再回答用户。只有在用户明确要求基于某次历史快照继续分析时,才可以复用旧结果。 @@ -115,7 +132,10 @@ lark-cli vc +meeting-events --meeting-id 执行准则: -- 这类问题默认先用 `lark-cli vc +meeting-events --meeting-id --page-all --format json` 拉取最新事件流。 +- 如果上下文已有明确 `meeting_id` 和来源身份,直接用同一身份执行 `+meeting-events --page-all --format json`。 +- 如果上下文没有明确 `meeting_id`,先按用户当前意图选择身份:问“我/当前用户所在会议”用 `lark-cli vc +meeting-list-active --as user --format pretty`;问“应用机器人可见的目标用户会议”用 `lark-cli vc +meeting-list-active --as bot --user-id --format pretty`。返回多个会议时先让用户选择。 +- 如果上下文只有 9 位会议号,先按当前身份执行 `+meeting-list-active` 并按 `meeting_no` 匹配;匹配到唯一会议后再查事件。不要为了总结会议而自动调用 `+meeting-join`。 +- 这类问题拿到 `meeting_id` 后,用 `lark-cli vc +meeting-events --as --meeting-id --page-all --format json` 拉取最新事件流。 - 如果事件中出现共享文档线索,例如: - `magic_share_started` - `share_doc.title` @@ -171,7 +191,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`;建议持久化保存,便于下次继续拉取新增事件 | @@ -181,16 +201,31 @@ lark-cli vc +meeting-events --meeting-id ```bash # 第 1 步:加入会议,记录返回的 meeting.id -lark-cli vc +meeting-join --meeting-number 123456789 +lark-cli vc +meeting-join --as bot --meeting-number 123456789 # 第 2 步:查询事件流 -lark-cli vc +meeting-events --meeting-id --page-all --format pretty +lark-cli vc +meeting-events --as bot --meeting-id --page-all --format pretty +``` + +### 场景 1b:应用机器人已在会中,先发现 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 +``` + +### 场景 1c:当前登录用户正在会中,先发现 meeting_id 再读事件 + +```bash +lark-cli vc +meeting-list-active --as user --format json +lark-cli vc +meeting-events --as user --meeting-id --page-all --format pretty ``` ### 场景 2:过滤某段时间内的事件 ```bash lark-cli vc +meeting-events \ + --as \ --meeting-id \ --start 2026-04-17T15:00:00+08:00 \ --end 2026-04-17T16:00:00+08:00 \ @@ -204,6 +239,7 @@ lark-cli vc +meeting-events \ # 上一次查询结束后,保留最后返回的 page_token # 这次直接从该游标继续拉新增事件 lark-cli vc +meeting-events \ + --as \ --meeting-id \ --page-token \ --page-all \ @@ -221,23 +257,27 @@ lark-cli vc +meeting-events \ | 错误现象 | 根本原因 | 解决方案 | |---------|---------|---------| | `--meeting-id is required` | 未传入 `--meeting-id` | 传入长数字 `meeting.id` | -| `10005 bot is not in meeting` | bot 从未真实入会该会议;或会议已结束但 bot 从未在会中出现过 | 先 `+meeting-join --meeting-number <9位号>` 真实入会再查;如果会议已经结束且当时 bot 没进过会,本接口也拉不到数据。**如果只是想看参会人快照,改用 `lark-cli vc meeting get --params '{"meeting_id":""}' --with-participants`**(不依赖 bot 身份参会) | -| `20001 meeting_status_MEETING_END` | 会议已结束且已超出后端允许的 5 分钟宽限窗口 | 本接口不再适合继续拉取事件。若要拿纪要文档或逐字稿 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 `;参会人请用 `lark-cli vc meeting get --params '{"meeting_id":""}' --with-participants` | +| `not a 9-digit meeting number` | 把 9 位会议号误传给 `--meeting-id` | 如果只是查询会中内容,先用 `+meeting-list-active` 按 `meeting_no` 匹配拿长数字 `meeting_id`;只有用户明确要求入会时才用 `+meeting-join --as bot --meeting-number <9位号>` | +| `10005 bot is not in meeting` | 使用应用身份读取,但应用机器人从未真实入会该会议;或会议已结束但应用机器人从未在会中出现过 | 如果本来是用户身份发现的 `meeting_id`,改回 `--as user`;如果确实要应用身份读取,先 `+meeting-join --as bot --meeting-number <9位号>` 真实入会再查。**如果只是想看参会人快照,改用 `lark-cli vc meeting get --params '{"meeting_id":""}' --with-participants`** | +| 用户身份不支持 | 当前事件读取接口不支持用用户身份访问 | 不要反复执行 `auth login`。改用应用身份流程:先通过 `+meeting-list-active --as bot --user-id ` 获取应用身份可读的 `meeting_id`,或在用户明确同意后让应用机器人入会,再用 `+meeting-events --as bot` 读取 | +| `20001 meeting_status_MEETING_END` | 会议已结束且已超出后端允许的 5 分钟宽限窗口 | 本接口不再适合继续拉取事件。先用 `lark-cli vc +notes --meeting-ids ` 获取会议产物信息,再根据 `note_display_type` / `note_id` / `minute_token` 和用户意图选择纪要正文、逐字稿或妙记;参会人请用 `lark-cli vc meeting get --params '{"meeting_id":""}' --with-participants` | | `20002 meeting not exist` | `meeting_id` 错误,或会议实例当前已不可获取(常见于把 9 位会议号当 meeting_id 传) | 确认传入的是长数字 `meeting_id`,不是 9 位会议号 | +| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`。以 CLI 返回的 metadata / error envelope 为准确认缺失权限;检查应用发布/安装,以及开放平台“权限可访问的数据范围”:选择“按条件筛选”,条件为“会议的归属者 包含 与应用的可用范围一致”;仍失败再排查内测 privilege / 灰度 | | `HTTP 404` / `HTTP 500` | 服务端当前无法找到或处理该会议实例 | 换一个正在进行且 bot 可见的 meeting_id,或排查后端问题 | ## 提示 - 这是**会中事件流**查询,不适合拿来搜历史会议记录;搜历史会议请用 `+search`。 - 如果会议已经结束,不要卡在 `+meeting-events`: - - 想拿纪要文档或逐字稿 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 ` -- 事件列表是否完整,取决于 bot 何时入会、何时离会,以及后端当前可见的会中事件范围。对于已结束会议,通常只在**结束后 5 分钟内**、且 bot **曾经在会中**时还能继续拉到事件。 + - 先用 `lark-cli vc +notes --meeting-ids ` 获取会议产物信息。 + - 再根据 `note_display_type`、`note_id`、`minute_token` 和用户意图,按 `lark-vc` 的产物决策读取纪要正文、逐字稿或妙记。 +- 事件列表是否完整,取决于应用机器人何时入会、何时离会,以及后端当前可见的会中事件范围。对于已结束会议,通常只在**结束后 5 分钟内**、且应用机器人**曾经在会中**时还能继续拉到事件。 - 查询"谁参加过某会议"请用 `vc meeting get --params '{"meeting_id":"","with_participants":true}'`——这是参会人**快照** API,不依赖 bot 是否参会,对已结束会议也可查;**不要** 用 `+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..77063bf80 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 @@ -1,29 +1,29 @@ # vc +meeting-join -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - -通过 9 位会议号加入一场正在进行的视频会议(bot join)。这是一次**写操作**,会实际让当前身份加入会议。 +通过 9 位会议号让应用机器人加入一场正在进行的视频会议。这是一次**写操作**,会实际让应用机器人加入会议。 本 skill 对应 shortcut:`lark-cli vc +meeting-join`(调用 `POST /open-apis/vc/v1/bots/join`)。 +> **不要把 9 位会议号等同于入会意图。** 用户给出 9 位会议号并询问“会议讲了什么 / 查会中事件”时,先用 `+meeting-list-active` 查当前 active meetings 并按 `meeting_no` 匹配;只有用户明确要求“入会 / 让应用机器人旁听 / 代我参会”时才调用本命令。 + ## 命令 ```bash # 仅指定会议号(无密码) -lark-cli vc +meeting-join --meeting-number 123456789 +lark-cli vc +meeting-join --as bot --meeting-number 123456789 # 指定会议号 + 密码 -lark-cli vc +meeting-join --meeting-number 123456789 --password 8888 +lark-cli vc +meeting-join --as bot --meeting-number 123456789 --password 8888 # 从邀请事件透传 call_id(参见「如何获取输入参数」) -lark-cli vc +meeting-join --meeting-number 123456789 --call-id a08e06bf-9a41-44e4-a89c-a7871899e783 +lark-cli vc +meeting-join --as bot --meeting-number 123456789 --call-id a08e06bf-9a41-44e4-a89c-a7871899e783 # 输出格式 -lark-cli vc +meeting-join --meeting-number 123456789 --format json +lark-cli vc +meeting-join --as bot --meeting-number 123456789 --format json # 预览 API 调用(不实际加入会议) -lark-cli vc +meeting-join --meeting-number 123456789 --dry-run +lark-cli vc +meeting-join --as bot --meeting-number 123456789 --dry-run ``` ## 参数 @@ -33,14 +33,13 @@ lark-cli vc +meeting-join --meeting-number 123456789 --dry-run | `--meeting-number ` | 是 | 会议号,必须为 **9 位纯数字** | | `--password ` | 否 | 会议密码,仅在该会议设置了入会密码时传入 | | `--call-id ` | 否 | 从 `vc.bot.meeting_invited_v1` 邀请事件透传的 `call_id`,原样回传即可。Agent 主动入会或无邀请事件来源时不传 | -| `--format ` | 否 | 输出格式:json (默认) / pretty / table / ndjson / csv | -| `--dry-run` | 否 | 预览 API 调用,不执行 | +| `--dry-run` | 否 | 预览 API 调用,不实际加入会议;会议号或身份不确定时先用它确认请求 | ## 核心约束 -### 1. 仅支持 user 身份 +### 1. 使用应用身份 -该命令仅支持 `user` 身份。 +这是应用机器人入会能力,使用 `--as bot`。不要用当前登录用户身份尝试让应用机器人入会。 ### 2. 会议号格式严格校验 @@ -53,8 +52,8 @@ lark-cli vc +meeting-join --meeting-number 123456789 --dry-run ### 3. 会议必须已开始且允许入会 -- 会议必须处于**进行中**状态,bot 无法加入尚未开始或已结束的会议。 -- 若会议设置了**等候室 / 入会审批**,bot 可能需要主持人放行后才真正入会。 +- 会议必须处于**进行中**状态,应用机器人无法加入尚未开始或已结束的会议。 +- 若会议设置了**等候室 / 入会审批**,应用机器人可能需要主持人放行后才真正入会。 - 若返回 `HTTP 403: no permission`(错误码 `121003`),不要只理解成“账号没权限”。这类报错更常见的原因是:会议参数或会控配置当前不满足入会条件,例如会议号填错、密码未传或错误、会议尚未开始、等候室 / 入会审批未放行、会议禁止外部/特定身份加入等。应先确认这些配置项,再重试。 ### 4. 机器人入会后对其他参会人可见 @@ -67,7 +66,7 @@ lark-cli vc +meeting-join --meeting-number 123456789 --dry-run | 字段 | 说明 | |------|------| -| `meeting.id` | 会议 ID(可后续传给 `+meeting-leave --meeting-id`) | +| `meeting.id` | 会议 ID(可后续传给 `+meeting-leave --as bot --meeting-id`) | | `meeting.meeting_no` | 会议号(与入参一致) | | `meeting.topic` | 会议主题 | | `meeting.start_time` | 会议开始时间 | @@ -88,25 +87,30 @@ lark-cli vc +meeting-join --meeting-number 123456789 --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 步:使用返回的 meeting.id 查询会中事件 -lark-cli vc +meeting-events --meeting-id --page-all --format pretty +lark-cli vc +meeting-events --as bot --meeting-id --page-all --format pretty ``` -### 场景 2:加入会议 → 会后拉取纪要 / 录制 +如果 bot 已经在会中,也可以通过 active meeting 找回 `meeting_id`: + +```bash +lark-cli vc +meeting-list-active --as bot --user-id --format json +``` + +### 场景 2:加入会议 → 会后进入 lark-vc 获取会议产物信息 ```bash # 第 1 步:加入并参会 -lark-cli vc +meeting-join --meeting-number 123456789 +lark-cli vc +meeting-join --as bot --meeting-number 123456789 -# 第 2 步:会议结束后,查询录制(拿到 minute_token) -lark-cli vc +recording --meeting-ids - -# 第 3 步:查询会议纪要(总结 / 待办 / 章节 / 逐字稿) +# 第 2 步:会议结束后,先查询会议产物 lark-cli vc +notes --meeting-ids ``` +后续按 `lark-vc` 的产物决策处理:根据 `note_display_type`、`note_id`、`minute_token` 和用户意图选择纪要正文、逐字稿或妙记。 + ## 常见错误与排查 | 错误现象 | 根本原因 | 解决方案 | @@ -114,18 +118,20 @@ lark-cli vc +notes --meeting-ids | `--meeting-number must be exactly 9 digits` | 会议号不是 9 位纯数字 | 检查是否误传了会议链接或 meeting_id | | 会议密码错误 | `--password` 错误或未提供 | 向主持人确认会议密码 | | 会议不存在 / 已结束 | 会议号错误或会议未进行中 | 确认会议正在进行中 | -| `HTTP 403: no permission` / `121003` | 入会前置条件不满足,通常不是单纯 scope 问题 | 依次确认:1)会议允许智能体加入;2)会议号正确;3)如有密码,已正确传入 `--password`;4)会议已开始;5)等候室 / 入会审批已放行;6)会议未禁止当前身份加入(如限制外部、限制 bot、仅特定成员可入会);确认后重试 | +| `HTTP 403: no permission` / `121003` | 入会前置条件不满足,通常不是单纯 scope 问题 | 依次确认:1)会议允许智能体加入;2)会议号正确;3)如有密码,已正确传入 `--password`;4)会议已开始;5)等候室 / 入会审批已放行;6)会议未禁止当前身份加入(如限制外部、限制应用机器人、仅特定成员可入会);确认后重试 | +| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`。以 CLI 返回的 metadata / error envelope 为准确认缺失权限;检查应用发布/安装,以及开放平台“权限可访问的数据范围”:选择“按条件筛选”,条件为“会议的归属者 包含 与应用的可用范围一致”;仍失败再排查内测 privilege / 灰度 | | 入会被拒绝 | 等候室 / 入会审批 / 限制外部入会 | 联系主持人放行或调整会议设置 | ## 提示 - 仅在 Agent 需要**真实加入**会议(例如参会机器人、会中助手)时使用;只拉取会议数据不需要入会。 -- 入会会让机器人立即出现在参会列表;若用户要求退出 / 离开 / 结束参会,直接 `+meeting-leave` 即可。参数格式不确定时可选 `--dry-run` 预览,但不是必经步骤。 +- 入会会让机器人立即出现在参会列表;若用户要求退出 / 离开 / 结束参会,直接使用 `+meeting-leave --as bot --meeting-id `。参数格式不确定时可选 `--dry-run` 预览,但不是必经步骤。 - 执行成功后,立即记录返回的 `meeting.id`,用于后续 `+meeting-leave` / `+meeting-events`。 ## 参考 - [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..62da0e74b 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 @@ -1,8 +1,6 @@ # vc +meeting-leave -> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。 - 通过 `meeting_id` 离开当前身份所在的视频会议(bot leave)。这是一次**写操作**,会实际把当前身份从会议中移出。 本 skill 对应 shortcut:`lark-cli vc +meeting-leave`(调用 `POST /open-apis/vc/v1/bots/leave`)。 @@ -11,13 +9,13 @@ ```bash # 通过 meeting_id 离会 -lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 +lark-cli vc +meeting-leave --as bot --meeting-id 69xxxxxxxxxxxxx28 # 输出格式 -lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 --format json +lark-cli vc +meeting-leave --as bot --meeting-id 69xxxxxxxxxxxxx28 --format json # 预览 API 调用(不实际离会) -lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 --dry-run +lark-cli vc +meeting-leave --as bot --meeting-id 69xxxxxxxxxxxxx28 --dry-run ``` ## 参数 @@ -25,22 +23,21 @@ lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 --dry-run | 参数 | 必填 | 说明 | |------|------|------| | `--meeting-id ` | 是 | 会议 ID(**不是 9 位会议号**) | -| `--format ` | 否 | 输出格式:json (默认) / pretty / table / ndjson / csv | -| `--dry-run` | 否 | 预览 API 调用,不执行 | +| `--dry-run` | 否 | 预览 API 调用,不实际离会;meeting_id 或身份不确定时先用它确认请求 | ## 核心约束 ### 1. 入参是 meeting_id,不是会议号 -`--meeting-id` 必须是会议的长数字 ID,通常由 `+meeting-join` 返回体中的 `meeting.id` 提供,也可从 `+search` 结果中的 `id` 字段获取。**传 9 位会议号会失败**。 +`--meeting-id` 必须是会议的长数字 ID,通常由 `+meeting-join --as bot` 返回体中的 `meeting.id` 提供,也可从应用身份 `+meeting-list-active --as bot --user-id ` 返回体中的 `meeting_id` 获取。**传 9 位会议号会失败**。 -### 2. 仅支持 user 身份 +### 2. 优先使用 bot 身份 -该命令仅支持 `user` 身份。只能让当前身份自己离会,无法强制移出其他参会人。 +这是应用机器人离会能力,使用与入会或 active meeting 发现相同的 `--as bot`。只能让当前身份自己离会,无法强制移出其他参会人。 ### 3. 当前身份必须在会议中 -必须先通过 `+meeting-join` 或其他方式在该会议中,否则接口会报错。 +应用机器人必须已经在该会议中,否则接口会报错。如果 `meeting_id` 来自 `+meeting-list-active`,必须确认这是应用身份发现到的会议。 ### 4. 离会立即生效,对其他参会人可见 @@ -55,7 +52,7 @@ lark-cli vc +meeting-leave --meeting-id 69xxxxxxxxxxxxx28 --dry-run | 输入参数 | 获取方式 | |---------|---------| -| `meeting-id` | `+meeting-join` 返回的 `meeting.id`;或 `+search` 结果中的 `id` 字段 | +| `meeting-id` | `+meeting-join --as bot` 返回的 `meeting.id`;或应用身份 `+meeting-list-active --as bot --user-id ` 返回的 `meeting_id` | ## Agent 组合场景 @@ -63,13 +60,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:会后补拉产物(不需要离会) @@ -77,10 +74,7 @@ lark-cli vc +meeting-leave --meeting-id 如果用户只是要求会议结束后拉录制、纪要或逐字稿,不要先调用 `+meeting-leave`;直接跨到 `lark-vc` 查询会后产物。 ```bash -# 第 1 步:会议结束后查询录制 -lark-cli vc +recording --meeting-ids - -# 第 2 步:查询会议纪要 +# 第 1 步:会议结束后进入 lark-vc 获取会议产物信息 lark-cli vc +notes --meeting-ids ``` @@ -88,19 +82,20 @@ lark-cli vc +notes --meeting-ids | 错误现象 | 根本原因 | 解决方案 | |---------|---------|---------| -| `--meeting-id is required` | 未传入 `--meeting-id` | 传入从 `+meeting-join` 得到的 `meeting.id` | +| `--meeting-id is required` | 未传入 `--meeting-id` | 传入从 `+meeting-join --as bot` 得到的 `meeting.id`,或应用身份 `+meeting-list-active` 返回的 `meeting_id` | | `meeting not found` / `invalid meeting_id` | 误传了 9 位会议号 | 必须使用 `meeting.id`,不是会议号 | | `not in meeting` | 当前身份并不在该会议中 | 确认先 `+meeting-join` 成功 | ## 提示 - 只有用户明确要求退出 / 离开 / 结束参会时才调用;离会会让机器人从参会列表消失,对其他参会人可见。若需要重新入会直接再 `+meeting-join`,不是真正的"不可逆"。参数格式不确定时可选 `--dry-run` 预览。 -- `+meeting-leave` 依赖 `+meeting-join` 返回的 `meeting.id`,但不是每次 join 后都必须调用 leave。 -- `meeting_id` 优先使用 `+meeting-join` 返回的 `meeting.id`;如果来自 `+search`,也必须先确认当前身份就在该会议中。不要用 9 位会议号。 +- `+meeting-leave` 优先使用 `+meeting-join --as bot` 返回的 `meeting.id`,但不是每次 join 后都必须调用 leave。 +- `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..825dee3f3 --- /dev/null +++ b/skills/lark-vc-agent/references/lark-vc-agent-meeting-list-active.md @@ -0,0 +1,91 @@ +# vc +meeting-list-active + +列出当前进行中的会议,用来发现 `+meeting-events` 需要的长数字 `meeting_id`。 + +本 skill 对应 shortcut:`lark-cli vc +meeting-list-active`(调用 `GET /open-apis/vc/v1/bots/user_active_meeting`)。 + +## 命令 + +```bash +# 查询当前登录用户正在参加的会议 +lark-cli vc +meeting-list-active --as user --format json + +# 查询指定用户当前参加、且应用机器人也在会中的会议 +lark-cli vc +meeting-list-active --as bot --user-id ou_xxx --format json +``` + +## 参数 + +| 参数 | 必填 | 说明 | +|------|------|------| +| `--user-id ` | 应用身份必填 | 目标用户 open_id,格式为 `ou_...`。用户身份不传;应用身份直接透传给接口,不接受 internal user_id 或数字 ID | + +## 身份语义 + +不要向用户暴露内部身份缩写;对用户只说“用户身份”或“应用身份”。 + +| 身份 | 命令 | 返回范围 | 后续事件读取 | +| ---- | ---- | -------- | ------------ | +| 用户身份 | `--as user` | 当前登录用户正在参加的会议 | 继续 `+meeting-events --as user` | +| 应用身份 | `--as bot --user-id ` | 目标用户正在参加、且应用机器人也在会中的会议 | 继续 `+meeting-events --as bot` | + +硬规则:`meeting_id` 从哪种身份路径拿到,后续 `+meeting-events` 就沿用哪种身份。不要把用户身份拿到的 `meeting_id` 改用应用身份查,也不要把应用身份拿到的 `meeting_id` 改用用户身份查,除非用户明确要求切换场景。 + +应用身份返回空,不代表目标用户不在任何会议中,只能说明没有找到“目标用户在会中且应用机器人也在会中”的当前会。 + +常见流程: + +```bash +# 方式 1:先让应用机器人入会,直接从 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:应用机器人已经在会中时,用应用身份发现 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 + +# 方式 3:只回答当前登录用户所在会议发生了什么 +lark-cli vc +meeting-list-active --as user --format json +lark-cli vc +meeting-events --as user --meeting-id --page-all --format pretty +``` + +## 多会议选择 + +- 如果返回多个会议,不要自动挑第一个。 +- 向用户展示每个候选的 `meeting_title` / `meeting_no` / `meeting_id`,等待用户选择。 +- 选择后继续使用发现该会议时的同一身份调用 `+meeting-events`。 + +## 9 位会议号匹配 + +用户提供 9 位会议号但没有明确要求应用机器人入会时,把会议号当作 active meeting 的筛选条件,而不是写操作指令。 + +```bash +# 用户问“我当前这个会讲了什么” +lark-cli vc +meeting-list-active --as user --format json + +# 用户问“让应用机器人所在/可见的这个会讲了什么” +lark-cli vc +meeting-list-active --as bot --user-id --format json +``` + +匹配规则: + +- 在返回会议中匹配 `meeting_no == <9位会议号>`。 +- 匹配到唯一会议:取该项的长数字 `meeting_id`,后续用同一身份调用 `+meeting-events`。 +- 匹配到多个会议:展示候选,让用户选择。 +- 没有匹配:说明当前身份没有发现该会议号对应的 active meeting;不要自动调用 `+meeting-join`,除非用户明确要求应用机器人入会。 + +## 常见错误与排查 + +| 错误现象 | 根本原因 | 解决方案 | +|---------|---------|---------| +| `--user-id is required when --as bot` | 应用身份未传目标用户 | 传入目标用户 open_id | +| 用户身份返回空列表 | 当前登录用户没有可见的进行中会议 | 确认用户是否在会中,或是否切错身份 | +| 用户身份不支持 | 当前接口不支持用用户身份访问 | 不要反复执行 `auth login`。改用应用身份流程:先拿目标用户 open_id,再执行 `+meeting-list-active --as bot --user-id `;同时按应用身份权限配置检查应用权限、安装、数据范围和灰度 | +| 应用身份返回空列表 | 没有满足“目标用户在会中且应用机器人也在会中”的当前会 | 先让应用机器人入会,或确认 `user_id` 和会议状态 | +| `--user-id` 格式错误 | 传入了 internal user_id 或其他非 `ou_...` 值 | 改传目标用户 open_id | +| 应用身份权限不足 | 应用权限、租户安装、权限可访问的数据范围或 VC Agent privilege 未配置完整 | 不要执行 `auth login`。以 CLI 返回的 metadata / error envelope 为准确认缺失权限;检查应用发布/安装,以及开放平台“权限可访问的数据范围”:选择“按条件筛选”,条件为“会议的归属者 包含 与应用的可用范围一致”;仍失败再排查内测 privilege / 灰度 | + +## 参考 + +- [lark-vc-agent-meeting-join](lark-vc-agent-meeting-join.md) — 让应用机器人真实入会并拿 `meeting.id` +- [lark-vc-agent-meeting-events](lark-vc-agent-meeting-events.md) — 使用 `meeting_id` 读取会中事件