Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion shortcuts/wiki/wiki_member_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
}
Comment thread
caojie0621 marked this conversation as resolved.

// wikiMemberRoles is the set of member_role values the space-member APIs
Expand Down
53 changes: 53 additions & 0 deletions shortcuts/wiki/wiki_member_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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())

Expand Down Expand Up @@ -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 {
Expand Down
7 changes: 4 additions & 3 deletions skill-template/domains/wiki.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,18 @@
- 命中 0 条:停下来问用户是名称拼错了还是调用方无权限;**不要**自行改名字重试。
- 用户明确选定后再执行 `lark-cli wiki +delete-space --space-id <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/<open_id> --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`。
Expand Down
7 changes: 4 additions & 3 deletions skills/lark-wiki/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,17 +33,18 @@ metadata:
- 命中 0 条:停下来问用户是名称拼错了还是调用方无权限;**不要**自行改名字重试。
- 用户明确选定后再执行 `lark-cli wiki +delete-space --space-id <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/<open_id> --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`。
Expand Down
7 changes: 4 additions & 3 deletions skills/lark-wiki/references/lark-wiki-member-add.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <space_id> \
--member-id <open_id|email|user_id|...> \
--member-type <openid|email|userid|unionid|openchat|opendepartmentid> \
--member-id <open_id|email|user_id|app_id|...> \
--member-type <openid|email|userid|unionid|openchat|opendepartmentid|appid> \
--member-role <admin|member> \
[--need-notification] \
[--as user|bot]
Expand All @@ -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` |
Expand All @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions skills/lark-wiki/references/lark-wiki-member-remove.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <space_id> \
--member-id <open_id|email|user_id|...> \
--member-type <openid|email|userid|unionid|openchat|opendepartmentid> \
--member-id <open_id|email|user_id|app_id|...> \
--member-type <openid|email|userid|unionid|openchat|opendepartmentid|appid> \
--member-role <admin|member> \
[--as user|bot]

Expand All @@ -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` |

Expand Down
44 changes: 44 additions & 0 deletions tests/cli_e2e/wiki/wiki_member_add_dryrun_test.go
Original file line number Diff line number Diff line change
@@ -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())
})
}
Loading