mirror of
https://github.com/larksuite/cli.git
synced 2026-07-08 02:00:19 +08:00
Compare commits
1 Commits
feat/apps-
...
features/F
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8470e92427 |
@@ -14,5 +14,6 @@ func Shortcuts() []common.Shortcut {
|
||||
VCMeetingJoin,
|
||||
VCMeetingLeave,
|
||||
VCMeetingEvents,
|
||||
VCMeetingListActive,
|
||||
}
|
||||
}
|
||||
|
||||
98
shortcuts/vc/vc_meeting_list_active.go
Normal file
98
shortcuts/vc/vc_meeting_list_active.go
Normal file
@@ -0,0 +1,98 @@
|
||||
// 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 vcActiveMeetingsAPIPath = "/open-apis/vc/v1/bots/active-meetings"
|
||||
|
||||
// VCMeetingListActive lists the caller's ongoing meetings.
|
||||
// As user (UAT): returns the user's own ongoing meetings.
|
||||
// As bot (TAT): returns the target user's ongoing meetings that the bot is also in.
|
||||
var VCMeetingListActive = common.Shortcut{
|
||||
Service: "vc",
|
||||
Command: "+meeting-list-active",
|
||||
Description: "List ongoing meetings for the current identity (UAT: self; TAT: target user with bot in meeting)",
|
||||
Risk: "read",
|
||||
Scopes: []string{"vc:meeting.active:read"},
|
||||
AuthTypes: []string{"user", "bot"},
|
||||
HasFormat: true,
|
||||
Flags: []common.Flag{
|
||||
{Name: "user-id", Desc: "target user open_id (ou_...), required when --as bot"},
|
||||
},
|
||||
Validate: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
userID := strings.TrimSpace(runtime.Str("user-id"))
|
||||
if runtime.IsBot() {
|
||||
if userID == "" {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id is required when --as bot").WithParam("--user-id")
|
||||
}
|
||||
if !strings.HasPrefix(userID, "ou_") {
|
||||
return errs.NewValidationError(errs.SubtypeInvalidArgument, "--user-id must be an open_id (ou_...), got %q", userID).WithParam("--user-id")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
},
|
||||
DryRun: func(ctx context.Context, runtime *common.RuntimeContext) *common.DryRunAPI {
|
||||
dryRun := common.NewDryRunAPI().GET(vcActiveMeetingsAPIPath)
|
||||
if params := buildActiveMeetingsParams(runtime); len(params) > 0 {
|
||||
dryRun.Params(params)
|
||||
}
|
||||
return dryRun
|
||||
},
|
||||
Execute: func(ctx context.Context, runtime *common.RuntimeContext) error {
|
||||
params := buildActiveMeetingsParams(runtime)
|
||||
data, err := runtime.CallAPITyped(http.MethodGet, vcActiveMeetingsAPIPath, 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 ongoing meetings.")
|
||||
return
|
||||
}
|
||||
for _, raw := range meetings {
|
||||
m, _ := raw.(map[string]interface{})
|
||||
if m == nil {
|
||||
continue
|
||||
}
|
||||
title := common.GetString(m, "meeting_title")
|
||||
if title == "" {
|
||||
title = "(no title)"
|
||||
}
|
||||
fmt.Fprintf(w, "%s\n", title)
|
||||
if id := common.GetString(m, "meeting_id"); id != "" {
|
||||
fmt.Fprintf(w, " Meeting ID: %s\n", id)
|
||||
}
|
||||
if no := common.GetString(m, "meeting_no"); no != "" {
|
||||
fmt.Fprintf(w, " Meeting No: %s\n", no)
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
func buildActiveMeetingsParams(runtime *common.RuntimeContext) map[string]interface{} {
|
||||
params := map[string]interface{}{}
|
||||
if runtime.IsBot() {
|
||||
if userID := strings.TrimSpace(runtime.Str("user-id")); userID != "" {
|
||||
params["user_id"] = userID
|
||||
}
|
||||
}
|
||||
return params
|
||||
}
|
||||
175
shortcuts/vc/vc_meeting_list_active_test.go
Normal file
175
shortcuts/vc/vc_meeting_list_active_test.go
Normal file
@@ -0,0 +1,175 @@
|
||||
// Copyright (c) 2026 Lark Technologies Pte. Ltd.
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
package vc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/larksuite/cli/internal/cmdutil"
|
||||
"github.com/larksuite/cli/internal/core"
|
||||
"github.com/larksuite/cli/internal/httpmock"
|
||||
"github.com/larksuite/cli/shortcuts/common"
|
||||
)
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Unit tests: validation + param assembly (UAT/TAT identity split)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func newActiveMeetingsCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{Use: "test"}
|
||||
cmd.Flags().String("user-id", "", "")
|
||||
return cmd
|
||||
}
|
||||
|
||||
func TestMeetingListActive_Validate_BotRequiresUserID(t *testing.T) {
|
||||
cmd := newActiveMeetingsCmd()
|
||||
runtime := common.TestNewRuntimeContextWithIdentity(cmd, defaultConfig(), core.AsBot)
|
||||
if err := VCMeetingListActive.Validate(context.Background(), runtime); err == nil {
|
||||
t.Fatal("expected error: --user-id required when --as bot")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingListActive_Validate_BotRejectsNonOpenID(t *testing.T) {
|
||||
cmd := newActiveMeetingsCmd()
|
||||
_ = cmd.Flags().Set("user-id", "123456")
|
||||
runtime := common.TestNewRuntimeContextWithIdentity(cmd, defaultConfig(), core.AsBot)
|
||||
if err := VCMeetingListActive.Validate(context.Background(), runtime); err == nil {
|
||||
t.Fatal("expected error: --user-id must be an open_id")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingListActive_Validate_UserNoUserID_OK(t *testing.T) {
|
||||
cmd := newActiveMeetingsCmd()
|
||||
runtime := common.TestNewRuntimeContextWithIdentity(cmd, defaultConfig(), core.AsUser)
|
||||
if err := VCMeetingListActive.Validate(context.Background(), runtime); err != nil {
|
||||
t.Fatalf("unexpected error for UAT without user-id: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildActiveMeetingsParams_UAT_OmitsUserID(t *testing.T) {
|
||||
cmd := newActiveMeetingsCmd()
|
||||
_ = cmd.Flags().Set("user-id", "ou_ignored")
|
||||
runtime := common.TestNewRuntimeContextWithIdentity(cmd, defaultConfig(), core.AsUser)
|
||||
params := buildActiveMeetingsParams(runtime)
|
||||
if _, exists := params["user_id"]; exists {
|
||||
t.Errorf("UAT must not send user_id, got %v", params["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildActiveMeetingsParams_TAT_SendsUserID(t *testing.T) {
|
||||
cmd := newActiveMeetingsCmd()
|
||||
_ = cmd.Flags().Set("user-id", "ou_target")
|
||||
runtime := common.TestNewRuntimeContextWithIdentity(cmd, defaultConfig(), core.AsBot)
|
||||
params := buildActiveMeetingsParams(runtime)
|
||||
if params["user_id"] != "ou_target" {
|
||||
t.Errorf("TAT user_id = %v, want ou_target", params["user_id"])
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Execute tests: final observable (no / single / multi meeting semantics)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
func TestMeetingListActive_Execute_UAT_Multi(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/vc/v1/bots/active-meetings",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"meetings": []interface{}{
|
||||
map[string]interface{}{"meeting_no": "111", "meeting_id": "9001", "meeting_title": "Standup"},
|
||||
map[string]interface{}{"meeting_no": "222", "meeting_id": "9002", "meeting_title": "Review"},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, VCMeetingListActive, []string{
|
||||
"+meeting-list-active", "--as", "user", "--format", "json",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
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) != 2 {
|
||||
t.Fatalf("meetings count = %d, want 2 (envelope: %s)", len(meetings), stdout.String())
|
||||
}
|
||||
first, _ := meetings[0].(map[string]any)
|
||||
if first["meeting_id"] != "9001" || first["meeting_title"] != "Standup" {
|
||||
t.Errorf("unexpected first meeting: %v", first)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingListActive_Execute_Empty(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
reg.Register(&httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/vc/v1/bots/active-meetings",
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{"meetings": []interface{}{}},
|
||||
},
|
||||
})
|
||||
|
||||
err := mountAndRun(t, VCMeetingListActive, []string{
|
||||
"+meeting-list-active", "--as", "user", "--format", "json",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
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) != 0 {
|
||||
t.Errorf("meetings count = %d, want 0", len(meetings))
|
||||
}
|
||||
}
|
||||
|
||||
func TestMeetingListActive_Execute_TAT_SendsUserIDParam(t *testing.T) {
|
||||
f, stdout, _, reg := cmdutil.TestFactory(t, defaultConfig())
|
||||
var capturedUserID string
|
||||
stub := &httpmock.Stub{
|
||||
Method: "GET",
|
||||
URL: "/open-apis/vc/v1/bots/active-meetings",
|
||||
OnMatch: func(req *http.Request) {
|
||||
capturedUserID = req.URL.Query().Get("user_id")
|
||||
},
|
||||
Body: map[string]interface{}{
|
||||
"code": 0, "msg": "ok",
|
||||
"data": map[string]interface{}{
|
||||
"meetings": []interface{}{
|
||||
map[string]interface{}{"meeting_no": "333", "meeting_id": "9003", "meeting_title": "Bot Meeting"},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
reg.Register(stub)
|
||||
|
||||
err := mountAndRun(t, VCMeetingListActive, []string{
|
||||
"+meeting-list-active", "--as", "bot", "--user-id", "ou_target", "--format", "json",
|
||||
}, f, stdout)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if capturedUserID != "ou_target" {
|
||||
t.Errorf("captured user_id query = %q, want ou_target", capturedUserID)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ metadata:
|
||||
| ---------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- |
|
||||
| "帮我入会 123456789"、"代我参会"、"让机器人进会旁听" | **本 skill** `+meeting-join` |
|
||||
| "会议现在还开着,谁刚加入了"、"会议里谁在发言"、"有人共享屏幕吗"(**进行中会议**,且**机器人已入会**) | **本 skill** `+meeting-events` |
|
||||
| "我现在在哪些会里"、"这个用户正在开的会有哪些"、"机器人和某用户共同在哪些会"(**当前在会列表**) | **本 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) 发群 |
|
||||
@@ -97,10 +98,12 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc +<verb> [flags]`)。
|
||||
| --------------------------------------------------------------- | -- | -------------------------------------------------------------------------- |
|
||||
| [`+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 ongoing meetings for current identity (UAT: self; TAT: target user + bot in meeting) |
|
||||
| [`+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 两种身份的语义差异:`--as user` 查自己当前在会列表(无需 `--user-id`);`--as bot` 查目标用户(`--user-id ou_...`)与 bot 共同在会的会议;返回 `meeting_no` / `meeting_id` / `meeting_title`,覆盖无/单/多会议语义,可把 `meeting_id` 直接喂给 `+meeting-events`。
|
||||
- 使用 `+meeting-leave` 前**必须**阅读 [references/lark-vc-agent-meeting-leave.md](references/lark-vc-agent-meeting-leave.md),了解 `meeting_id` 的来源与写操作可见性。
|
||||
|
||||
## 权限表
|
||||
@@ -109,6 +112,7 @@ Shortcut 是对常用操作的高级封装(`lark-cli vc +<verb> [flags]`)。
|
||||
| ----------------- | ------------------------------ |
|
||||
| `+meeting-join` | `vc:meeting.bot.join:write` |
|
||||
| `+meeting-events` | `vc:meeting.meetingevent:read` |
|
||||
| `+meeting-list-active` | `vc:meeting.active:read` |
|
||||
| `+meeting-leave` | `vc:meeting.bot.join:write` |
|
||||
|
||||
## 延伸
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
|
||||
# vc +meeting-list-active
|
||||
|
||||
> **前置条件:** 先阅读 [`../lark-shared/SKILL.md`](../../lark-shared/SKILL.md) 了解认证、全局参数和安全规则。
|
||||
|
||||
查询「当前正在进行中(Ongoing)的会议列表」。该命令是**读操作**,按发起方身份返回不同语义:
|
||||
|
||||
- **UAT(`--as user`)**:以**当前用户**身份,查询用户**自己**当前正在参加的会议列表。无需 `--user-id`。
|
||||
- **TAT(`--as bot`)**:以**应用/机器人**身份,按目标用户 `--user-id`(open_id, `ou_...`)查询该用户当前在会列表,并**只保留 bot 也在其中**的会议(用户与 bot 的共同在会列表)。
|
||||
|
||||
返回每条会议的 `meeting_no` / `meeting_id` / `meeting_title`,覆盖**无 / 单 / 多** 会议三种语义。返回的 `meeting_id` 可直接喂给 [`+meeting-events`](lark-vc-agent-meeting-events.md)。
|
||||
|
||||
本 skill 对应 shortcut:`lark-cli vc +meeting-list-active`(调用 `GET /open-apis/vc/v1/bots/active-meetings`)。
|
||||
|
||||
## 命令
|
||||
|
||||
```bash
|
||||
# UAT:查我自己当前在会列表
|
||||
lark-cli vc +meeting-list-active --as user --format pretty
|
||||
|
||||
# TAT:查目标用户与 bot 共同在会的会议
|
||||
lark-cli vc +meeting-list-active --as bot --user-id ou_xxxxxxxxxxxxxxxx --format json
|
||||
|
||||
# 预览 API 调用(不实际请求)
|
||||
lark-cli vc +meeting-list-active --as bot --user-id ou_xxxxxxxxxxxxxxxx --dry-run
|
||||
```
|
||||
|
||||
## 参数
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
|------|------|------|
|
||||
| `--as <user\|bot>` | 否 | 发起身份。`user`=UAT(查自己);`bot`=TAT(查目标用户与 bot 共同在会)。默认取配置的 default-as |
|
||||
| `--user-id <open_id>` | TAT 必填 | 目标用户 open_id(`ou_...`)。仅 `--as bot` 时使用且必填;`--as user` 时忽略 |
|
||||
| `--format <fmt>` | 否 | 输出格式:json (CLI 默认) / pretty(本 skill 推荐) / table / ndjson / csv |
|
||||
| `--dry-run` | 否 | 预览 API 调用,不执行 |
|
||||
|
||||
## 核心约束
|
||||
|
||||
### 1. 数据为后台聚合级,非毫秒实时
|
||||
|
||||
在会列表来自 admin meeting_list(Ongoing)数据源。它匹配「IM 聊天后定位用户当前在哪些会」这类场景,但不保证毫秒级实时;刚入会/刚离会可能有秒级延迟。
|
||||
|
||||
### 2. UAT 与 TAT 身份语义不同
|
||||
|
||||
- UAT 返回的是**用户自己**的在会列表,按操作者身份天然鉴权,无需管理员权限。
|
||||
- TAT 在用户在会列表基础上**额外过滤**「bot 是否也在这场会中」,因此结果是用户与 bot 的**交集**。若 bot 未入任何会,TAT 通常返回空列表。
|
||||
|
||||
### 3. 返回语义:无 / 单 / 多
|
||||
|
||||
- **无**:`meetings` 为空数组——用户当前不在任何进行中会议(TAT 下为无共同在会)。
|
||||
- **单**:返回单条,可直接用其 `meeting_id`。
|
||||
- **多**:返回全部,由调用方提示用户选择。
|
||||
|
||||
## 权限
|
||||
|
||||
| Shortcut | 所需 scope |
|
||||
|----------|-----------|
|
||||
| `+meeting-list-active` | `vc:meeting.active:read` |
|
||||
@@ -135,6 +135,7 @@ lark-cli vc meeting get --params '{"meeting_id":"<meeting_id>","with_participant
|
||||
| 参会人快照(谁参加过、何时入/离会,任意时点)| `vc meeting get --with-participants` | 本 skill |
|
||||
| 已结束会议的发言内容 | `vc +notes` 取 `verbatim_doc_token` 再 `docs +fetch --api-version v2` | 本 skill |
|
||||
| **进行中会议**的实时事件流(转写、聊天、共享、会中加入/离开)| `vc +meeting-events` | [`lark-vc-agent`](../lark-vc-agent/SKILL.md) |
|
||||
| **当前在会列表**(我正在哪些会 / 用户与 bot 共同在会)| `vc +meeting-list-active` | [`lark-vc-agent`](../lark-vc-agent/SKILL.md) |
|
||||
| **Agent 真实入会 / 离会** | `vc +meeting-join` / `vc +meeting-leave` | [`lark-vc-agent`](../lark-vc-agent/SKILL.md) |
|
||||
|
||||
## 资源关系
|
||||
|
||||
Reference in New Issue
Block a user