diff --git a/shortcuts/vc/shortcuts.go b/shortcuts/vc/shortcuts.go index 444644476..8183d2f47 100644 --- a/shortcuts/vc/shortcuts.go +++ b/shortcuts/vc/shortcuts.go @@ -15,5 +15,6 @@ func Shortcuts() []common.Shortcut { VCMeetingLeave, VCMeetingListActive, VCMeetingEvents, + VCMeetingMessageSend, } } diff --git a/shortcuts/vc/vc_meeting_events_test.go b/shortcuts/vc/vc_meeting_events_test.go index 2e9e73547..78718bdb0 100644 --- a/shortcuts/vc/vc_meeting_events_test.go +++ b/shortcuts/vc/vc_meeting_events_test.go @@ -838,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-list-active", "+meeting-events"} + want := []string{"+search", "+notes", "+recording", "+meeting-join", "+meeting-leave", "+meeting-list-active", "+meeting-events", "+meeting-message-send"} if !reflect.DeepEqual(commands, want) { t.Fatalf("shortcut commands = %#v, want %#v", commands, want) } diff --git a/shortcuts/vc/vc_meeting_message_send.go b/shortcuts/vc/vc_meeting_message_send.go new file mode 100644 index 000000000..2f1dca712 --- /dev/null +++ b/shortcuts/vc/vc_meeting_message_send.go @@ -0,0 +1,144 @@ +// 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/shortcuts/common" +) + +const ( + meetingMessageTypeText = "text" + meetingMessageTypeReaction = "reaction" +) + +var supportedVCMeetingReactionKeys = map[string]struct{}{ + "VC_CanNotSee": {}, + "VC_NoSound": {}, + "VC_LooksGood": {}, + "VC_SoundsClear": {}, +} + +// VCMeetingMessageSend sends an in-meeting text message or feedback reaction. +var VCMeetingMessageSend = common.Shortcut{ + Service: "vc", + Command: "+meeting-message-send", + Description: "Send an in-meeting text message or feedback reaction", + Risk: "write", + Scopes: []string{"vc:meeting.message:write"}, + AuthTypes: []string{"user", "bot"}, + HasFormat: true, + Flags: []common.Flag{ + {Name: "meeting-id", Required: true, Desc: "meeting ID to send into"}, + {Name: "msg-type", Desc: "message type: text or reaction"}, + {Name: "text", Desc: "text content when --msg-type text"}, + {Name: "emoji-type", Desc: "reaction key when --msg-type reaction"}, + {Name: "uuid", Desc: "optional idempotency key"}, + }, + Validate: func(ctx context.Context, runtime *common.RuntimeContext) error { + if err := validateMeetingEventsMeetingID(runtime.Str("meeting-id")); err != nil { + return err + } + _, err := resolveMeetingMessageType(runtime) + return err + }, + DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI { + body, err := buildMeetingMessageSendBody(runtime) + if err != nil { + return common.NewDryRunAPI().Set("error", err.Error()) + } + return common.NewDryRunAPI(). + POST(buildMeetingMessageSendPath(runtime.Str("meeting-id"))). + Body(body) + }, + Execute: func(ctx context.Context, runtime *common.RuntimeContext) error { + body, err := buildMeetingMessageSendBody(runtime) + if err != nil { + return err + } + data, err := runtime.CallAPITyped(http.MethodPost, buildMeetingMessageSendPath(runtime.Str("meeting-id")), nil, body) + if err != nil { + return err + } + if data == nil { + data = map[string]interface{}{} + } + runtime.OutFormat(data, nil, func(w io.Writer) { + fmt.Fprintln(w, "Meeting message sent.") + if msgType := common.GetString(data, "msg_type"); msgType != "" { + fmt.Fprintf(w, " Type: %s\n", msgType) + } else if msgType, _ := body["msg_type"].(string); msgType != "" { + fmt.Fprintf(w, " Type: %s\n", msgType) + } + if uuid := common.GetString(data, "uuid"); uuid != "" { + fmt.Fprintf(w, " UUID: %s\n", uuid) + } + }) + return nil + }, +} + +func buildMeetingMessageSendPath(meetingID string) string { + return fmt.Sprintf("/open-apis/vc/v1/meetings/%s/messages", strings.TrimSpace(meetingID)) +} + +func buildMeetingMessageSendBody(runtime *common.RuntimeContext) (map[string]interface{}, error) { + msgType, err := resolveMeetingMessageType(runtime) + if err != nil { + return nil, err + } + body := map[string]interface{}{ + "msg_type": msgType, + } + switch msgType { + case meetingMessageTypeText: + body["text"] = strings.TrimSpace(runtime.Str("text")) + case meetingMessageTypeReaction: + body["emoji_type"] = strings.TrimSpace(runtime.Str("emoji-type")) + } + if uuid := strings.TrimSpace(runtime.Str("uuid")); uuid != "" { + body["uuid"] = uuid + } + return body, nil +} + +func resolveMeetingMessageType(runtime *common.RuntimeContext) (string, error) { + msgType := strings.ToLower(strings.TrimSpace(runtime.Str("msg-type"))) + text := strings.TrimSpace(runtime.Str("text")) + emojiType := strings.TrimSpace(runtime.Str("emoji-type")) + + if msgType == "" { + switch { + case text != "" && emojiType == "": + msgType = meetingMessageTypeText + case text == "" && emojiType != "": + msgType = meetingMessageTypeReaction + default: + return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--msg-type is required when both --text and --emoji-type are empty or both are set").WithParam("--msg-type") + } + } + + switch msgType { + case meetingMessageTypeText: + if text == "" { + return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--text is required when --msg-type text").WithParam("--text") + } + case meetingMessageTypeReaction: + if emojiType == "" { + return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--emoji-type is required when --msg-type reaction").WithParam("--emoji-type") + } + if _, ok := supportedVCMeetingReactionKeys[emojiType]; !ok { + return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--emoji-type must be one of VC_CanNotSee, VC_NoSound, VC_LooksGood, VC_SoundsClear").WithParam("--emoji-type") + } + default: + return "", errs.NewValidationError(errs.SubtypeInvalidArgument, "--msg-type must be text or reaction").WithParam("--msg-type") + } + return msgType, nil +} diff --git a/shortcuts/vc/vc_meeting_message_send_test.go b/shortcuts/vc/vc_meeting_message_send_test.go new file mode 100644 index 000000000..210612e28 --- /dev/null +++ b/shortcuts/vc/vc_meeting_message_send_test.go @@ -0,0 +1,125 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package vc + +import ( + "context" + "strings" + "testing" + + "github.com/spf13/cobra" + + "github.com/larksuite/cli/internal/cmdutil" + "github.com/larksuite/cli/shortcuts/common" +) + +func newMeetingMessageSendRuntime() *common.RuntimeContext { + cmd := &cobra.Command{Use: "test"} + cmd.Flags().String("meeting-id", "", "") + cmd.Flags().String("msg-type", "", "") + cmd.Flags().String("text", "", "") + cmd.Flags().String("emoji-type", "", "") + cmd.Flags().String("uuid", "", "") + return common.TestNewRuntimeContext(cmd, defaultConfig()) +} + +func mustSetMeetingMessageSendFlag(t *testing.T, runtime *common.RuntimeContext, name, value string) { + t.Helper() + if err := runtime.Cmd.Flags().Set(name, value); err != nil { + t.Fatalf("Flags().Set(%q, %q) error = %v", name, value, err) + } +} + +func TestMeetingMessageSendBuildBody_Text(t *testing.T) { + runtime := newMeetingMessageSendRuntime() + mustSetMeetingMessageSendFlag(t, runtime, "text", " hello ") + mustSetMeetingMessageSendFlag(t, runtime, "uuid", " cid-1 ") + + body, err := buildMeetingMessageSendBody(runtime) + if err != nil { + t.Fatalf("buildMeetingMessageSendBody() error = %v", err) + } + if body["msg_type"] != meetingMessageTypeText { + t.Fatalf("msg_type = %v, want text", body["msg_type"]) + } + if body["text"] != "hello" { + t.Fatalf("text = %v, want hello", body["text"]) + } + if body["uuid"] != "cid-1" { + t.Fatalf("uuid = %v, want cid-1", body["uuid"]) + } +} + +func TestMeetingMessageSendBuildBody_Reaction(t *testing.T) { + runtime := newMeetingMessageSendRuntime() + mustSetMeetingMessageSendFlag(t, runtime, "msg-type", "reaction") + mustSetMeetingMessageSendFlag(t, runtime, "emoji-type", "VC_LooksGood") + + body, err := buildMeetingMessageSendBody(runtime) + if err != nil { + t.Fatalf("buildMeetingMessageSendBody() error = %v", err) + } + if body["msg_type"] != meetingMessageTypeReaction { + t.Fatalf("msg_type = %v, want reaction", body["msg_type"]) + } + if body["emoji_type"] != "VC_LooksGood" { + t.Fatalf("emoji_type = %v, want VC_LooksGood", body["emoji_type"]) + } + if _, ok := body["text"]; ok { + t.Fatalf("text should be omitted for reaction, got %#v", body["text"]) + } +} + +func TestMeetingMessageSendValidateRejectsMeetingNumber(t *testing.T) { + runtime := newMeetingMessageSendRuntime() + mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "123456789") + mustSetMeetingMessageSendFlag(t, runtime, "text", "hello") + + err := VCMeetingMessageSend.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "9-digit meeting number") { + t.Fatalf("error = %v, want 9-digit meeting number hint", err) + } +} + +func TestMeetingMessageSendValidateRejectsUnknownReaction(t *testing.T) { + runtime := newMeetingMessageSendRuntime() + mustSetMeetingMessageSendFlag(t, runtime, "meeting-id", "7651377260537433044") + mustSetMeetingMessageSendFlag(t, runtime, "msg-type", "reaction") + mustSetMeetingMessageSendFlag(t, runtime, "emoji-type", "LOVE") + + err := VCMeetingMessageSend.Validate(context.Background(), runtime) + if err == nil { + t.Fatal("expected validation error") + } + if !strings.Contains(err.Error(), "VC_CanNotSee") { + t.Fatalf("error = %v, want supported key hint", err) + } +} + +func TestMeetingMessageSendDryRun_Text(t *testing.T) { + f, stdout, _, _ := cmdutil.TestFactory(t, defaultConfig()) + err := mountAndRun(t, VCMeetingMessageSend, []string{ + "+meeting-message-send", "--dry-run", "--as", "user", + "--meeting-id", "7651377260537433044", + "--text", "hello", + "--uuid", "cid-1", + }, f, stdout) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + out := stdout.String() + for _, want := range []string{ + "/open-apis/vc/v1/meetings/7651377260537433044/messages", + "\"msg_type\": \"text\"", + "\"text\": \"hello\"", + "\"uuid\": \"cid-1\"", + } { + if !strings.Contains(out, want) { + t.Fatalf("dry-run output missing %q: %s", want, out) + } + } +} diff --git a/skills/lark-vc-agent/SKILL.md b/skills/lark-vc-agent/SKILL.md index e63fdbd10..0f1a36eab 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: "飞书视频会议会中能力:用于让应用机器人真实加入或离开正在进行的会议,并读取当前身份可见的会中事件,如参会人加入/离开、发言、聊天、屏幕共享。适用于用户询问正在开的会议发生了什么、谁在发言、是否共享内容,或需要发现当前可读的进行中会议 ID。不负责已结束会议搜索、参会人快照、纪要、逐字稿或录制查询,这些使用 lark-vc 技能。" +description: "飞书视频会议会中能力:用于让应用机器人真实加入或离开正在进行的会议,并读取当前身份可见的会中事件、发送会中文本消息或反馈表情。适用于用户询问正在开的会议发生了什么、谁在发言、是否共享内容,或需要发现当前可读的进行中会议 ID。不负责已结束会议搜索、参会人快照、纪要、逐字稿或录制查询,这些使用 lark-vc 技能。" metadata: requires: bins: ["lark-cli"] @@ -26,7 +26,7 @@ metadata: 本 skill 与 [`lark-vc`](../lark-vc/SKILL.md) 并列: - **`lark-vc`** **负责"会后查询"**:搜索历史会议、参会人快照、纪要/逐字稿/录制 -- **`lark-vc-agent`** **负责"会中动作"**:机器人入会 / 读取进行中会议的实时事件 / 机器人离会 +- **`lark-vc-agent`** **负责"会中动作"**:机器人入会 / 读取进行中会议的实时事件 / 发送会中文本或反馈 / 机器人离会 按此分工路由,避免两个 skill 语义混淆。 @@ -35,6 +35,7 @@ metadata: | "帮我入会 123456789"、"代我参会"、"让机器人进会旁听" | **本 skill** `+meeting-join` | | "会议现在还开着,谁刚加入了"、"会议里谁在发言"、"有人共享屏幕吗"(**进行中会议**) | **本 skill** `+meeting-events` | | "我/某个用户现在在哪个会里"、"给我找当前可拉事件的 meeting_id" | **本 skill** `+meeting-list-active` | +| "在会里发一句 xx"、"提示大家 xx"、"反馈听不到/看不到/声音清楚/效果不错"(**进行中会议**) | **本 skill** `+meeting-message-send` | | "退出会议"、"让机器人离开" | **本 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) 发群 | @@ -49,7 +50,7 @@ metadata: | 查询目标用户且应用机器人也在会中的会议 | `--as bot --user-id ` | `--user-id` 必须是 `ou_...`;拿到的 `meeting_id` 后续继续用 `--as bot` 读事件 | | 用户明确要求应用机器人入会/旁听/代参会 | `--as bot` | 这是写操作,会真实产生入会记录;返回的 `meeting.id` 后续继续用 `--as bot` | -硬规则:`meeting_id` 从哪种身份路径拿到,后续 `+meeting-events` 就沿用哪种身份,除非用户明确要求切换场景(例如从“仅查询我当前会”改成“让应用机器人入会旁听”)。 +硬规则:`meeting_id` 从哪种身份路径拿到,后续 `+meeting-events` / `+meeting-message-send` 就沿用哪种身份,除非用户明确要求切换场景(例如从“仅查询我当前会”改成“让应用机器人入会旁听”)。 ## 核心场景 @@ -79,14 +80,31 @@ metadata: 10. 用户直接问“这个会议讲了什么 / 现在讲到哪了”且上下文没有明确 `meeting_id` 时,先用用户身份发现当前会议;如果用户明确要求应用机器人视角,或上下文已经是应用机器人参会流程,再用应用身份发现。若返回多个会议,展示候选并让用户选择。 11. 用户直接提供 **9 位会议号** 并询问会中事件/会议内容时,默认把它当作 active meeting 的筛选条件:先按当前身份查 active meetings,并在返回里匹配 `meeting_no == <9位会议号>`;匹配到唯一会议后取长数字 `meeting_id`,再用同一身份查事件。只有用户明确要求“入会 / 让应用机器人旁听 / 代我参会”时才改用 `+meeting-join`。 -### 3. 离开会议(写操作) +### 3. 发送会中文本或反馈表情(写操作) + +1. 用户明确要求在当前进行中的会议里发送提示、说明、反馈或表情时,用 `+meeting-message-send`。 +2. 输入是长数字 `meeting_id`,不是 9 位会议号。若用户只给 9 位会议号,先按当前身份执行 `+meeting-list-active` 并按 `meeting_no` 匹配,匹配到唯一会议后再发送;不要为了发消息自动入会。 +3. 身份必须延续:`meeting_id` 来自用户身份发现,就继续 `--as user`;来自应用身份发现或应用机器人入会,就继续 `--as bot`。 +4. 文本消息使用 `--text`;反馈表情使用 `--emoji-type`。不要把自然语言硬编码转换成 key,但可以基于用户语义选择最匹配的已支持 key。 +5. 当前支持的会中特定反馈 key:`VC_CanNotSee`、`VC_NoSound`、`VC_LooksGood`、`VC_SoundsClear`。 +6. 该命令只暴露会中文本和会中反馈表情,不作为“发送绑定群消息”的默认能力;如果用户明确要发群聊,请路由到 [`lark-im`](../lark-im/SKILL.md)。 +7. 若使用应用身份发送,应用机器人必须在会中;若使用用户身份发送,当前用户必须正在该会议中。权限错误时按“应用身份权限配置检查”或“用户身份被拒绝时”处理。 + +示例: + +```bash +lark-cli vc +meeting-message-send --as user --meeting-id --text "稍等,我在看文档" +lark-cli vc +meeting-message-send --as bot --meeting-id --msg-type reaction --emoji-type VC_NoSound +``` + +### 4. 离开会议(写操作) 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. 使用与入会或 active meeting 发现相同的应用身份离会。 -### 4. 获取当前可用的进行中会议 ID(读操作) +### 5. 获取当前可用的进行中会议 ID(读操作) 1. `+meeting-list-active` 用来发现当前进行中的会议,并拿到后续 `+meeting-events` 需要的长数字 `meeting_id`。 2. 用户身份:`lark-cli vc +meeting-list-active --as user --format json`,用于发现当前登录用户正在参加的会议;后续 `+meeting-events` 继续 `--as user`。 @@ -95,7 +113,7 @@ metadata: 5. 如果返回多个会议,不要自动任选一个;按 `meeting_title` / `meeting_no` / `meeting_id` 展示候选,等待用户明确选择后再调用 `+meeting-events`。 6. 如果用户给了 9 位会议号,先在 active meeting 结果中按 `meeting_no` 匹配。匹配失败时,不要自动入会;只有用户明确要求应用机器人真实入会时,才询问或执行 `+meeting-join`。 -### 5. Agent 参会示范 +### 6. Agent 参会示范 ```bash # 1. 入会,捕获 meeting.id @@ -136,11 +154,13 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc + [flags]`)。 | [`+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 meeting events visible to the app agent (participant joined/left, transcript, chat, share) | +| [`+meeting-message-send`](references/lark-vc-agent-meeting-message-send.md) | 写 | Send an in-meeting text message or feedback reaction | | [`+meeting-leave`](references/lark-vc-agent-meeting-leave.md) | 写 | Leave a meeting by 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-message-send`](references/lark-vc-agent-meeting-message-send.md):会中文本、会中反馈表情、身份延续和写操作风险。 - [`+meeting-leave`](references/lark-vc-agent-meeting-leave.md):`meeting_id` 的来源与写操作可见性。 ## 应用身份权限配置检查 diff --git a/skills/lark-vc-agent/references/lark-vc-agent-meeting-message-send.md b/skills/lark-vc-agent/references/lark-vc-agent-meeting-message-send.md new file mode 100644 index 000000000..568e9b901 --- /dev/null +++ b/skills/lark-vc-agent/references/lark-vc-agent-meeting-message-send.md @@ -0,0 +1,84 @@ +# vc +meeting-message-send + +发送会中文本消息或会中特定反馈表情。 + +本 skill 对应 shortcut:`lark-cli vc +meeting-message-send`(调用 `POST /open-apis/vc/v1/meetings/{meeting_id}/messages`)。 + +## 适用场景 + +- 用户要求“在会里发一句话”“提示大家”“给当前会议发消息”。 +- 用户要求表达会中反馈,例如“听不到”“看不到”“声音清楚”“效果不错”。 +- 只用于正在进行中的会议;已结束会议不支持。 + +## 身份规则 + +`meeting_id` 从哪种身份路径拿到,发送消息时就沿用哪种身份: + +| meeting_id 来源 | 发送时身份 | +| --- | --- | +| `+meeting-list-active --as user` | `+meeting-message-send --as user` | +| `+meeting-list-active --as bot --user-id ` | `+meeting-message-send --as bot` | +| `+meeting-join --as bot` 返回的 `meeting.id` | `+meeting-message-send --as bot` | + +不要把用户身份发现的 `meeting_id` 改用应用身份发送,也不要把应用身份发现的 `meeting_id` 改用用户身份发送,除非用户明确要求切换。 + +## 参数 + +| 参数 | 说明 | +| --- | --- | +| `--meeting-id` | 必填,长数字 `meeting_id`,不是 9 位会议号 | +| `--msg-type` | 可选,`text` 或 `reaction`;只传 `--text` 或只传 `--emoji-type` 时可自动推断 | +| `--text` | 文本消息内容 | +| `--emoji-type` | 会中反馈表情 key | +| `--uuid` | 可选,幂等 key;不传则服务端生成 | + +## 文本消息 + +```bash +lark-cli vc +meeting-message-send --as user --meeting-id --text "稍等,我在看文档" +``` + +文本消息会出现在会议内的文本互动区。不要把它当成绑定群消息发送能力;如果用户明确要求发到群聊,路由到 `lark-im`。 + +## 反馈表情 + +当前支持的会中特定反馈 key: + +| 用户表达 | 推荐 key | +| --- | --- | +| 听不到、没声音 | `VC_NoSound` | +| 看不到、画面有问题 | `VC_CanNotSee` | +| 声音清楚 | `VC_SoundsClear` | +| 效果不错、看起来可以 | `VC_LooksGood` | + +```bash +lark-cli vc +meeting-message-send --as bot --meeting-id --msg-type reaction --emoji-type VC_NoSound +``` + +不要维护自己的自然语言到 key 的硬编码表;根据用户当前语义选择最匹配的 key。如果没有匹配项,先向用户确认,不要发送普通 IM 表情 key。 + +## 9 位会议号处理 + +如果用户给的是 9 位会议号并要求发送会中消息: + +1. 先按当前身份执行 `+meeting-list-active`。 +2. 在返回结果中按 `meeting_no` 匹配该 9 位会议号。 +3. 匹配到唯一会议后取长数字 `meeting_id`。 +4. 用发现该会议时的同一身份执行 `+meeting-message-send`。 + +匹配失败时不要自动入会。只有用户明确要求“让应用机器人入会/旁听/代参会”时,才改用 `+meeting-join`。 + +## 权限和前置条件 + +- 用户身份:当前用户必须正在该会议中。 +- 应用身份:应用机器人必须正在该会议中。 +- 会议需要开启会中智能体/Agent 能力开关。 +- 需要 `vc:meeting.message:write` 权限;应用身份还需要应用已安装、数据范围已配置。 + +应用身份权限错误时,不要引导用户反复 `auth login`。按主 skill 的“应用身份权限配置检查”处理。 + +## 相关 + +- [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-agent-meeting-join](lark-vc-agent-meeting-join.md) — 应用机器人入会