diff --git a/shortcuts/wiki/wiki_member_helpers.go b/shortcuts/wiki/wiki_member_helpers.go index 573b65686..8eac0da2e 100644 --- a/shortcuts/wiki/wiki_member_helpers.go +++ b/shortcuts/wiki/wiki_member_helpers.go @@ -13,7 +13,7 @@ import ( // wikiMemberTypes is the set of member_type values the space-member APIs // accept. Shared by +member-add and +member-remove so the two stay aligned. var wikiMemberTypes = []string{ - "openid", "userid", "email", "unionid", "openchat", "opendepartmentid", + "openid", "userid", "email", "unionid", "openchat", "opendepartmentid", "appid", } // wikiMemberRoles is the set of member_role values the space-member APIs diff --git a/shortcuts/wiki/wiki_member_test.go b/shortcuts/wiki/wiki_member_test.go index 140468622..63626ca27 100644 --- a/shortcuts/wiki/wiki_member_test.go +++ b/shortcuts/wiki/wiki_member_test.go @@ -83,6 +83,17 @@ func TestWikiMemberShortcutsDeclareRiskAndAuth(t *testing.T) { } } +func TestWikiMemberTypesIncludeAppID(t *testing.T) { + t.Parallel() + + for _, typ := range wikiMemberTypes { + if typ == "appid" { + return + } + } + t.Fatalf("wikiMemberTypes = %v, want appid", wikiMemberTypes) +} + // ── +member-add ────────────────────────────────────────────────────────────── func TestWikiMemberAddRequestBodyOmitsQueryWhenNotificationFlagUnset(t *testing.T) { @@ -149,6 +160,24 @@ func TestWikiMemberAddDryRunSingleStep(t *testing.T) { } } +func TestWikiMemberAddDryRunSupportsAppID(t *testing.T) { + t.Parallel() + + dry := buildWikiMemberAddDryRun(wikiMemberAddSpec{ + SpaceID: "space_42", + MemberID: "cli_app_123", + MemberType: "appid", + MemberRole: "member", + }) + api := dryRunAPIList(t, dry) + if len(api) != 1 || api[0].Method != "POST" || api[0].URL != "/open-apis/wiki/v2/spaces/space_42/members" { + t.Fatalf("dry-run api = %#v", api) + } + if api[0].Body["member_id"] != "cli_app_123" || api[0].Body["member_type"] != "appid" { + t.Fatalf("dry-run appid body = %#v", api[0].Body) + } +} + func TestWikiMemberAddDryRunMyLibraryIsTwoStep(t *testing.T) { t.Parallel() @@ -204,6 +233,20 @@ func TestWikiMemberAddRejectsBotWithDepartment(t *testing.T) { } } +func TestWikiMemberAddAcceptsAppIDWithoutFormatValidation(t *testing.T) { + t.Parallel() + + cmd := newMemberAddCmd("space_1", "app_123", "appid", "member") + runtime := common.TestNewRuntimeContext(cmd, nil) + spec, err := readWikiMemberAddSpec(runtime) + if err != nil { + t.Fatalf("readWikiMemberAddSpec() error = %v", err) + } + if spec.MemberID != "app_123" || spec.MemberType != "appid" { + t.Fatalf("spec = %#v", spec) + } +} + func TestWikiMemberAddMountedExecuteFlattensMember(t *testing.T) { t.Setenv("LARKSUITE_CLI_CONFIG_DIR", t.TempDir()) @@ -706,6 +749,16 @@ func newMemberRemoveCmd(spaceID, memberID, memberType, memberRole string) *cobra return cmd } +func newMemberAddCmd(spaceID, memberID, memberType, memberRole string) *cobra.Command { + cmd := &cobra.Command{Use: "wiki +member-add"} + cmd.Flags().String("space-id", spaceID, "") + cmd.Flags().String("member-id", memberID, "") + cmd.Flags().String("member-type", memberType, "") + cmd.Flags().String("member-role", memberRole, "") + cmd.Flags().Bool("need-notification", false, "") + return cmd +} + // dryRunAPIList serializes a DryRunAPI through JSON to match how the framework // exposes it to callers — same approach used by +space-create's tests. func dryRunAPIList(t *testing.T, dry *common.DryRunAPI) []struct { diff --git a/skill-template/domains/wiki.md b/skill-template/domains/wiki.md index 417236cf9..dd5ace5b3 100644 --- a/skill-template/domains/wiki.md +++ b/skill-template/domains/wiki.md @@ -14,17 +14,18 @@ - 命中 0 条:停下来问用户是名称拼错了还是调用方无权限;**不要**自行改名字重试。 - 用户明确选定后再执行 `lark-cli wiki +delete-space --space-id --yes`(高风险写操作,必须显式 `--yes`)。 - 用户要在知识库中创建新节点,优先使用 `lark-cli wiki +node-create`。 -- 用户说“给知识库添加成员/管理员”:先把目标解析成“用户 / 群 / 部门”三类之一,再决定 `--member-type`,不要先调 `wiki +member-add` 再根据报错反推类型。 +- 用户说“给知识库添加成员/管理员”:先把目标解析成“用户 / 群 / 部门 / 应用”四类之一,再决定 `--member-type`,不要先调 `wiki +member-add` 再根据报错反推类型。 - 用户说“部门 + bot”:这是已知不支持路径。不要继续尝试 `wiki +member-add --as bot`;直接提示必须改成 `--as user`,或明确告知当前要求无法完成。 -- 用户说“用户 / 群 + 添加成员”:先解析对应 ID,再执行 `wiki +member-add`。 +- 用户说“用户 / 群 / 应用 + 添加成员”:先解析对应 ID,再执行 `wiki +member-add`。 - 用户说“查看 / 列出空间成员”:用 `wiki +member-list`;该 shortcut 默认只取一页,多成员场景显式加 `--page-all`。 - 用户说“移除 / 删除空间成员”:用 `wiki +member-remove`,必须传齐原始授予时的 `--member-type` 和 `--member-role`(不知道就先 `wiki +member-list` 查一下)。 ## 成员添加流程 -- 调用 `lark-cli wiki +member-add` 前,先把自然语言里的“人 / 群 / 部门”解析成正确的 `--member-id`,不要猜格式。 +- 调用 `lark-cli wiki +member-add` 前,先把自然语言里的“人 / 群 / 部门 / 应用”解析成正确的 `--member-id`,不要猜格式。 - 用户场景默认优先 `--member-type=openid`:用 `lark-cli contact +search-user --query "<姓名/邮箱/手机号>" --format json` 获取 `open_id`。 - 群组场景使用 `--member-type=openchat`:用 `lark-cli im +chat-search --query "<群名关键词>" --format json` 获取 `chat_id`。 +- 应用场景使用 `--member-type=appid`:`--member-id` 传应用 ID,格式通常为 `cli_xxx`。 - `userid` / `unionid` 只在下游明确要求时才使用;先拿到 `open_id`,再调用 `lark-cli api GET /open-apis/contact/v3/users/ --params '{"user_id_type":"open_id"}' --format json` 读取 `user_id` / `union_id`。 - 部门场景使用 `--member-type=opendepartmentid`:当前 CLI 没有 shortcut,需调用 `lark-cli api POST /open-apis/contact/v3/departments/search --as user --params '{"department_id_type":"open_department_id"}' --data '{"query":"<部门名>"}'` 获取 `open_department_id`。 - 只有在目标类型和身份都已确认可行后,才调用 `lark-cli wiki +member-add`。对于部门场景,这意味着必须是 `--as user`。 diff --git a/skills/lark-wiki/SKILL.md b/skills/lark-wiki/SKILL.md index b5fde05dd..38b252e7b 100644 --- a/skills/lark-wiki/SKILL.md +++ b/skills/lark-wiki/SKILL.md @@ -33,17 +33,18 @@ metadata: - 命中 0 条:停下来问用户是名称拼错了还是调用方无权限;**不要**自行改名字重试。 - 用户明确选定后再执行 `lark-cli wiki +delete-space --space-id --yes`(高风险写操作,必须显式 `--yes`)。 - 用户要在知识库中创建新节点,优先使用 `lark-cli wiki +node-create`。 -- 用户说“给知识库添加成员/管理员”:先把目标解析成“用户 / 群 / 部门”三类之一,再决定 `--member-type`,不要先调 `wiki +member-add` 再根据报错反推类型。 +- 用户说“给知识库添加成员/管理员”:先把目标解析成“用户 / 群 / 部门 / 应用”四类之一,再决定 `--member-type`,不要先调 `wiki +member-add` 再根据报错反推类型。 - 用户说“部门 + bot”:这是已知不支持路径。不要继续尝试 `wiki +member-add --as bot`;直接提示必须改成 `--as user`,或明确告知当前要求无法完成。 -- 用户说“用户 / 群 + 添加成员”:先解析对应 ID,再执行 `wiki +member-add`。 +- 用户说“用户 / 群 / 应用 + 添加成员”:先解析对应 ID,再执行 `wiki +member-add`。 - 用户说“查看 / 列出空间成员”:用 `wiki +member-list`;该 shortcut 默认只取一页,多成员场景显式加 `--page-all`。 - 用户说“移除 / 删除空间成员”:用 `wiki +member-remove`,必须传齐原始授予时的 `--member-type` 和 `--member-role`(不知道就先 `wiki +member-list` 查一下)。 ## 成员添加流程 -- 调用 `lark-cli wiki +member-add` 前,先把自然语言里的“人 / 群 / 部门”解析成正确的 `--member-id`,不要猜格式。 +- 调用 `lark-cli wiki +member-add` 前,先把自然语言里的“人 / 群 / 部门 / 应用”解析成正确的 `--member-id`,不要猜格式。 - 用户场景默认优先 `--member-type=openid`:用 `lark-cli contact +search-user --query "<姓名/邮箱/手机号>" --format json` 获取 `open_id`。 - 群组场景使用 `--member-type=openchat`:用 `lark-cli im +chat-search --query "<群名关键词>" --format json` 获取 `chat_id`。 +- 应用场景使用 `--member-type=appid`:`--member-id` 传应用 ID,格式通常为 `cli_xxx`。 - `userid` / `unionid` 只在下游明确要求时才使用;先拿到 `open_id`,再调用 `lark-cli api GET /open-apis/contact/v3/users/ --params '{"user_id_type":"open_id"}' --format json` 读取 `user_id` / `union_id`。 - 部门场景使用 `--member-type=opendepartmentid`:当前 CLI 没有 shortcut,需调用 `lark-cli api POST /open-apis/contact/v3/departments/search --as user --params '{"department_id_type":"open_department_id"}' --data '{"query":"<部门名>"}'` 获取 `open_department_id`。 - 只有在目标类型和身份都已确认可行后,才调用 `lark-cli wiki +member-add`。对于部门场景,这意味着必须是 `--as user`。 diff --git a/skills/lark-wiki/references/lark-wiki-member-add.md b/skills/lark-wiki/references/lark-wiki-member-add.md index f27bd1a2f..3af2d8885 100644 --- a/skills/lark-wiki/references/lark-wiki-member-add.md +++ b/skills/lark-wiki/references/lark-wiki-member-add.md @@ -10,8 +10,8 @@ Add a member to a wiki space. OpenAPI: `POST /open-apis/wiki/v2/spaces/:space_id # Add a user as a regular member lark-cli wiki +member-add \ --space-id \ - --member-id \ - --member-type \ + --member-id \ + --member-type \ --member-role \ [--need-notification] \ [--as user|bot] @@ -34,7 +34,7 @@ lark-cli wiki +member-add \ |------|------|----------|---------|-------------| | `--space-id` | string | **Yes** | — | Wiki space ID; use `my_library` for the personal document library (user only) | | `--member-id` | string | **Yes** | — | Member ID; interpretation is decided by `--member-type` | -| `--member-type` | enum | **Yes** | — | `openchat` / `userid` / `email` / `opendepartmentid` / `openid` / `unionid` | +| `--member-type` | enum | **Yes** | — | `openchat` / `userid` / `email` / `opendepartmentid` / `openid` / `unionid` / `appid` | | `--member-role` | enum | **Yes** | — | `admin` (full space administration) / `member` (collaborator) | | `--need-notification` | bool | No | unset | Send an in-app notification after the grant. **Omitting the flag sends no `need_notification` query at all** — passing `--need-notification=false` is the explicit opt-out | | `--as` | enum | No | `auto` | Identity `user`/`bot`; wiki is user-centric → pass `--as user` | @@ -57,6 +57,7 @@ lark-cli wiki +member-add \ - **Bot + `my_library` is rejected upfront** — `my_library` is a per-user alias with no meaning for a tenant token. Pass an explicit `--space-id` when `--as bot`. - **Bot + `opendepartmentid` is a known unsupported path on the backend.** The CLI does not pre-block it (the API may evolve), but the call will fail. Use `--as user` for department adds. +- **App member uses `--member-type=appid`.** The corresponding `--member-id` is the app ID, commonly formatted as `cli_xxx`. - Resolve `--member-id` **before** calling: `lark-cli contact +search-user` for users, `lark-cli im +chat-search` for groups, `lark-cli api POST /open-apis/contact/v3/departments/search` for departments. Do not call `+member-add` first and reverse-engineer the type from the error. - The role switch (`admin` ⇄ `member`) is not a single update — call [`+member-remove`](lark-wiki-member-remove.md) for the old role first, then `+member-add` with the new one. - `--dry-run` previews 2 steps when `--space-id my_library` (resolve → add), 1 step otherwise. diff --git a/skills/lark-wiki/references/lark-wiki-member-remove.md b/skills/lark-wiki/references/lark-wiki-member-remove.md index a6254fa1c..f191c8634 100644 --- a/skills/lark-wiki/references/lark-wiki-member-remove.md +++ b/skills/lark-wiki/references/lark-wiki-member-remove.md @@ -9,8 +9,8 @@ Remove a member from a wiki space. OpenAPI: `DELETE /open-apis/wiki/v2/spaces/:s ```bash lark-cli wiki +member-remove \ --space-id \ - --member-id \ - --member-type \ + --member-id \ + --member-type \ --member-role \ [--as user|bot] @@ -32,7 +32,7 @@ lark-cli wiki +member-remove \ |------|------|----------|---------|-------------| | `--space-id` | string | **Yes** | — | Wiki space ID; use `my_library` for the personal document library (user only) | | `--member-id` | string | **Yes** | — | Member ID; interpretation is decided by `--member-type` | -| `--member-type` | enum | **Yes** | — | Must **match the original grant**: `openchat` / `userid` / `email` / `opendepartmentid` / `openid` / `unionid` | +| `--member-type` | enum | **Yes** | — | Must **match the original grant**: `openchat` / `userid` / `email` / `opendepartmentid` / `openid` / `unionid` / `appid` | | `--member-role` | enum | **Yes** | — | Must **match the original grant**: `admin` / `member` | | `--as` | enum | No | `auto` | Identity `user`/`bot`; wiki is user-centric → pass `--as user` | diff --git a/tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go b/tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go new file mode 100644 index 000000000..6abd8f097 --- /dev/null +++ b/tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2026 Lark Technologies Pte. Ltd. +// SPDX-License-Identifier: MIT + +package wiki + +import ( + "context" + "testing" + "time" + + clie2e "github.com/larksuite/cli/tests/cli_e2e" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/tidwall/gjson" +) + +func TestWikiMemberAddDryRun(t *testing.T) { + setWikiNodeCreateDryRunEnv(t) + + t.Run("SupportsAppIDMemberType", func(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + t.Cleanup(cancel) + + result, err := clie2e.RunCmd(ctx, clie2e.Request{ + Args: []string{ + "wiki", "+member-add", + "--space-id", "space_42", + "--member-id", "cli_app_123", + "--member-type", "appid", + "--member-role", "member", + "--dry-run", + }, + DefaultAs: "bot", + }) + require.NoError(t, err) + result.AssertExitCode(t, 0) + + assert.Equal(t, "POST", gjson.Get(result.Stdout, "api.0.method").String()) + assert.Equal(t, "/open-apis/wiki/v2/spaces/space_42/members", gjson.Get(result.Stdout, "api.0.url").String()) + assert.Equal(t, "cli_app_123", gjson.Get(result.Stdout, "api.0.body.member_id").String()) + assert.Equal(t, "appid", gjson.Get(result.Stdout, "api.0.body.member_type").String()) + assert.Equal(t, "member", gjson.Get(result.Stdout, "api.0.body.member_role").String()) + }) +}