- CLI
- AI 技能
【免费下载链接】cli
The official Lark/飞书 CLI tool, maintained by the larksuite team — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, and more, with 200+ commands and 20+ AI Agent Skills.
本文围绕飞书知识库(Wiki)的官方 CLI 工具 lark-cli 中wiki +node-copy快捷命令展开,讲解如何把一个 Wiki 节点及其正文内容复制到目标知识空间或目标父节点下,并深入剖析其高危写确认(--yes)、目标参数互斥校验、锁竞争(错误码 131009)指数退避重试等源码级实现。读完本文,你将掌握该命令的完整参数语义、可复制的实战命令、输出字段含义,以及复制与移动(wiki +move)的选型边界。
一、命令定位:复制什么、不复制什么
wiki +node-copy是 skills/lark-wiki 技能集中推荐优先使用的 Wiki 快捷命令(Shortcut)之一。它的语义非常明确:
- 复制一个 Wiki 节点,并包含该节点的正文内容,复制到「目标知识空间」或「目标父节点之下」;
- 非递归复制:只复制被指定的那一个节点及其内容,后代节点(descendant nodes)不会被复制,需要逐个另行复制;
- 保留源节点:复制不会删除或移动源节点,源节点与副本同时存在。
这一「仅当前节点、不递归」的语义同时被命令的 Tips 与测试用例双重固定:在 shortcuts/wiki/wiki_node_copy.go 的 Tips 中明确写着 “This shortcut copies the current node only; descendant nodes are not copied”,而测试TestWikiNodeCopyDeclaresNodeOnlySemantics也断言了这两条提示必须存在(见 shortcuts/wiki/wiki_list_copy_test.go)。
因此,若你的目标是复制整棵子树(父节点加全部子孙),请先列出子树结构,再对每个节点逐一执行+node-copy;若目标是移动而非复制(不保留源节点),则应改用wiki +move(详见下文对比)。
二、高危写操作与--yes确认机制
上游复制 API 被标记为danger: true,因此+node-copy被归类为high-risk-write(高危写)。在源码 shortcuts/wiki/wiki_node_copy.go 中可以看到:
var WikiNodeCopy = common.Shortcut{ Service: "wiki", Command: "+node-copy", Description: "Copy a wiki node to a target space or parent node", Risk: "high-risk-write", Scopes: []string{"wiki:node:copy"}, AuthTypes: []string{"user", "bot"}, HasFormat: true, ... }Risk: "high-risk-write"意味着必须显式追加--yes才能发出请求。如果遗漏--yes:
- CLI 会返回
confirmation_required类型的错误,提示语为 “requires confirmation”,并附带恢复提示add --yes to confirm; - 底层 API 请求根本不会发出,复制操作不会执行。
该确认机制在命令框架层实现:internal/cmdutil/confirm.go中的RequireConfirmation构造一个携带errs.RiskHighRiskWrite风险级别的ConfirmationRequiredError(见 internal/cmdutil/confirm.go)。测试TestWikiNodeCopyDeclaredHighRiskWrite(shortcuts/wiki/wiki_list_copy_test.go)刻意不注册任何 HTTP stub,并断言:未带--yes时命令必然以confirmation_required失败——若确认门禁失效导致请求外泄,httpmock 会以 “no stub” 报错,让回归一目了然。这从测试设计上锁死了「无--yes绝不发请求」的契约。
三、完整用法与参数详解
基本命令
lark-cli wiki +node-copy \ --space-id <source_space_id> \ --node-token <source_node_token> \ (--target-space-id <target_space_id> | --target-parent-node-token <token>) \ [--title <new_title>] \ --yes \ [--as user|bot]参数表
| Flag | 必填 | 说明 |
|---|---|---|
--space-id | 是 | 源知识空间 ID(Wiki 空间 ID 为数字字符串,可通过wiki +space-list获取) |
--node-token | 是 | 待复制的源节点 token |
--target-space-id | 条件必填 | 目标知识空间 ID;当未提供--target-parent-node-token时必须提供 |
--target-parent-node-token | 条件必填 | 目标父节点 token;当未提供--target-space-id时必须提供 |
--title | 否 | 复制后节点的新标题;省略则沿用原标题 |
--yes | 是 | 确认高危写操作;缺少该标志命令拒绝发送 API 请求 |
--format | 否 | 输出格式:json(默认)/pretty/table/csv/ndjson |
--as | 否 | 身份user/bot(默认auto);知识库以用户为中心,建议显式传--as user |
硬性约束:
--target-space-id与--target-parent-node-token必须且只能提供其中一个,二者不可同时为空,也不可同时给出(详见下一节)。
三个典型场景
场景一:复制到另一个知识空间的根目录
lark-cli wiki +node-copy \ --space-id 7211568716812369922 \ --node-token wikcn_SOURCE_TOKEN \ --target-space-id 7352712345678901234 \ --yes \ --as user场景二:复制到同/异空间下某个父节点内,并重命名副本
lark-cli wiki +node-copy \ --space-id 7211568716812369922 \ --node-token wikcn_SOURCE_TOKEN \ --target-parent-node-token wikcn_PARENT_TOKEN \ --title "Getting Started (Copy)" \ --yes \ --as user场景三:先预览将要发出的请求(dry-run):Shortcut 实现了 DryRun 预览逻辑(common.NewDryRunAPI),会展示待发送的POST /open-apis/wiki/v2/spaces/{space_id}/nodes/{node_token}/copy请求及其请求体,适合在真正执行前核对目标与标题参数。
四、目标校验:二选一且互斥
--target-space-id与--target-parent-node-token的约束在命令的Validate钩子中实现(shortcuts/wiki/wiki_node_copy.go),共三条规则:
- 至少提供一个目标参数:两者均为空时,返回
ValidationError(invalid_argument),并同时指出两个参数 “provide --target-space-id or --target-parent-node-token”; - 两者互斥:同时给出时同样返回校验错误,提示 “mutually exclusive; provide only one”——这一设计也保证了请求体永远不会出现歧义的「双目标」形态;
- 参数格式校验:
space-id、node-token、两个目标参数都会经过资源名格式校验(validateOptionalResourceName),防止把 URL、路径或不合法字符直接塞进 token 字段。
对应的测试TestWikiNodeCopyRequiresTargetSpaceOrParent与TestWikiNodeCopyRejectsBothTargetFlags(shortcuts/wiki/wiki_list_copy_test.go)分别验证了这两种失败路径,并断言错误中同时携带了两个问题参数的定位信息。
五、底层 API 与请求体构造
+node-copy最终调用的是飞书开放平台 Wiki v2 的复制接口:
POST /open-apis/wiki/v2/spaces/{space_id}/nodes/{node_token}/copy其中{space_id}对应--space-id,{node_token}对应--node-token(均经过validate.EncodePathSegment进行路径段编码)。请求体由buildNodeCopyBody构造(shortcuts/wiki/wiki_node_copy.go):
--target-space-id非空 → 写入target_space_id;--target-parent-node-token非空 → 写入target_parent_token;--title非空 → 写入title;省略时整个title字段不进入请求体,服务端会沿用原标题。
这条字段映射被测试TestWikiNodeCopyCopiesNodeToTargetSpace和TestWikiNodeCopyCopiesNodeToTargetParent(shortcuts/wiki/wiki_list_copy_test.go)通过捕获请求体(CapturedBody)逐一验证:复制到空间时请求体含target_space_id与title;复制到父节点时请求体含target_parent_token,且未提供--title时请求体中不存在title字段。
六、输出字段解读
命令执行成功后默认输出 JSON,完整示例如下:
{ "space_id": "target_space_id", "node_token": "wikcn_EXAMPLE_TOKEN", "obj_token": "doccn_EXAMPLE_TOKEN", "obj_type": "docx", "node_type": "origin", "title": "Getting Started (Copy)", "parent_node_token": "", "has_child": false }各字段含义:
| 字段 | 说明 |
|---|---|
space_id | 副本所在的知识空间 ID |
node_token | 副本的 Wiki 节点 token(wikcn...前缀) |
obj_token | 副本对应的底层文档对象 token(如doccn...) |
obj_type | 底层对象类型,如docx/sheet/bitable/slides/file等 |
node_type | 节点类型,常见为origin(原始节点);副本对应快捷方式时为shortcut |
title | 副本标题(未指定--title时沿用原标题) |
parent_node_token | 副本的父节点 token;复制到空间根目录时为空字符串 |
has_child | 副本是否含子节点(复制本身非递归,通常为false) |
除此之外,源码在输出时还会尝试补充url字段(shortcuts/wiki/wiki_node_copy.go):优先取上游响应中携带的真实url,缺失时回退为按品牌(brand)合成的资源链接(见wikiNodeURL实现 shortcuts/wiki/wiki_helpers.go)。该行为同样有测试断言(TestWikiNodeCopyCopiesNodeToTargetSpace中校验url必须取响应中的真实链接)。
使用--format pretty时,命令会以可读的多行形式渲染:title、node_token、space_id、obj_type、obj_token,并在有值时追加parent_node_token与url(shortcuts/wiki/wiki_node_copy.go)。
七、锁竞争(131009)自动重试:指数退避实现
Wiki 服务在并发写入同一目标父节点时可能返回131009 lock contention(internal/output/lark_errors.go中常量LarkErrWikiLockContention = 131009,见 internal/output/lark_errors.go)。+node-copy对这一错误做了有界指数退避自动重试,实现在runWikiNodeCopyWithRetry(shortcuts/wiki/wiki_node_copy.go):
- 常量:
wikiNodeCopyMaxRetries = 2、wikiNodeCopyRetryBaseDelay = 250 * time.Millisecond; - 退避策略:第 n 次重试前等待
baseDelay << (n-1),即250ms → 500ms,共 3 次尝试(1 次初始 + 2 次重试); - 只对锁竞争重试:
isWikiNodeLockContention判断为 131009 时才进入下一次尝试,其余错误(如权限类、参数类)立即返回、不重试; - 重试窗口内若上下文被取消(Ctrl-C 或超时),返回带
network_transport/network_timeout分类的上下文错误,并保留原始 cause; - 重试耗尽后仍失败时,会为原始错误追加提示:
wiki node copy failed after 2 retries due to lock contention; try again later or reduce concurrent writes under the same target parent(见wrapWikiNodeCopyRetryError)。
相关测试覆盖了全部关键路径(shortcuts/wiki/wiki_list_copy_test.go):
TestRunWikiNodeCopyRetriesLockContentionThenSucceeds:第一次 131009、第二次成功,断言恰好调用 2 次;TestRunWikiNodeCopyDoesNotRetryOtherErrors:权限错误不重试,且错误分类与 cause 被完整保留;TestRunWikiNodeCopyBackoffCancellationPreservesErrorContract:退避期间取消 / 超时的错误契约;TestRunWikiNodeCopyRetryExhaustionPreservesErrorContract:重试耗尽后仍保留 131009 的retryable属性,并追加重试耗尽提示。
实操建议:若连续重试后仍遇 131009,请先等待片刻再重试,并避免在同一目标父节点下并发执行多个复制/写入操作。
八、权限与身份
- 必需 Scope:
wiki:node:copy。框架的 preflight 会对 scope 做精确匹配(相关测试TestWikiListShortcutsDeclareNarrowScopes论证了窄 scope 的必要性),因此授权时应确保应用/用户具备该精确 scope。 - 身份选择:命令支持
--as user与--as bot。但知识空间与节点本质是用户个人资源,--as默认值为auto,不带时常常被解析成bot,列出/操作的是应用所属空间而非用户的。策略上应显式使用--as user,仅当用户明确要求「应用 / bot 视角」时才用--as bot(依据 skills/lark-wiki/SKILL.md 的「身份选择:优先使用 user 身份」一节)。 - 权限错误提示:若遇到 Wiki 服务返回的 131006(空间/节点 ACL 拒绝),命令层会给出稳定的恢复提示:这是资源访问权限问题,而非应用 scope 授权问题,不应重试同一请求或反复切换身份试错,应向资源所有者 / 知识库管理员申请读权限(见
wikiPermissionDeniedHint,shortcuts/wiki/wiki_helpers.go)。
九、实战流程:复制前先用wiki +node-get确认
复制属于写操作,且源与目标涉及两处资源,推荐在执行前先做一次「我要动什么」的确认:
- 解析并确认源节点:
wiki +node-get支持直接传入 wiki URL / node_token / obj_token,通过node_by_token解析出space_id、obj_type、parent_node_token、has_child等关键信息(实现见 shortcuts/wiki/wiki_node_get.go)。它被设计为+move/+node-copy/+delete-space之前的核对步骤。 - 确认目标空间或父节点:用
wiki +space-list --as user拿目标数字space_id;若目标是某父节点,用wiki +node-list --space-id <space_id>找到对应node_token(不要把 wiki URL、doc token 直接当--space-id/--target-parent-node-token使用)。 - 执行复制并带上
--yes。 - 核对输出:重点检查返回的
space_id/parent_node_token是否符合预期,必要时用wiki +node-get回读副本确认。
十、与wiki +move的选型对比
| 维度 | wiki +node-copy | wiki +move |
|---|---|---|
| 语义 | 复制节点及内容,保留源节点 | 移动节点,源不保留 |
| 目标表达 | --target-space-id或--target-parent-node-token(二选一互斥) | 支持--target-parent-token、--target-space-id,另有 Drive 文档迁入知识库的docs_to_wiki模式(--obj-type+--obj-token) |
| 递归 | 非递归,仅当前节点 | 单节点移动 |
| 风险等级 | high-risk-write(需--yes) | write |
| 所需 Scope | wiki:node:copy | wiki:node:move、wiki:node:read、wiki:space:read |
| 适用场景 | 模板复用、归档副本、跨空间克隆 | 整理/迁移节点位置、Drive 文档入知识库 |
原文档明确建议:要移动已有 Wiki 节点且不保留源时,用wiki +move而不是「复制再删除」。+move的完整说明见 skills/lark-wiki/references/lark-wiki-move.md,其实现位于 shortcuts/wiki/wiki_move.go。
十一、常见错误与排障速查
| 现象 | 原因 | 处理方式 |
|---|---|---|
confirmation_required,提示requires confirmation | 未带--yes | 追加--yes后重发;高危写确认是刻意设计,勿去掉该保护 |
invalid_argument:--target-space-id/--target-parent-node-token均为空 | 未提供任何目标参数 | 提供两者之一 |
invalid_argument:两者互斥 | 同时提供了两个目标参数 | 只保留一个目标参数 |
| 错误码 131009(lock contention),且重试后仍失败 | 同一目标父节点下并发写入冲突 | 稍等再试;避免同目标并发写 |
| 错误码 131006(permission denied) | 对源/目标空间或节点无资源访问权限 | 不重试、不切换身份试错;向知识库管理员申请资源读权限 |
| 复制后副本无子节点 | 复制本身非递归 | 对需要的每个后代节点分别执行+node-copy |
十二、相关资源
- 本命令参考文档:skills/lark-wiki/references/lark-wiki-node-copy.md
- 技能总览与身份/成员约束:skills/lark-wiki/SKILL.md
- 命令实现:shortcuts/wiki/wiki_node_copy.go
- 测试用例(确认门禁、目标校验、重试、请求体断言):shortcuts/wiki/wiki_list_copy_test.go
- 节点解析与复制前核对:shortcuts/wiki/wiki_node_get.go
- 移动命令对比:shortcuts/wiki/wiki_move.go、skills/lark-wiki/references/lark-wiki-move.md
- 高危写确认机制:internal/cmdutil/confirm.go
- 锁竞争错误码定义:internal/output/lark_errors.go
- CLI
- AI 技能
【免费下载链接】cli
The official Lark/飞书 CLI tool, maintained by the larksuite team — built for humans and AI Agents. Covers core business domains including Messenger, Docs, Base, Sheets, Calendar, Mail, Tasks, Meetings, and more, with 200+ commands and 20+ AI Agent Skills.
相关推荐
Lark CLI 知识库(Wiki)端到端测试 100% 覆盖解析:节点工作流与 +space-list / +node-list / +node-copy 快捷命令验证
Lark CLI 知识库(Wiki)端到端测试 100% 覆盖解析:节点工作流与 +space list / +node list / +node copy 快
CLIAI 技能Lark CLI 知识整理工作流 Rollback:云盘/知识库移动失败后的安全恢复与清理机制
Lark CLI 知识整理工作流 Rollback:云盘/知识库移动失败后的安全恢复与清理机制 导读 knowledge_organize (知识整理)是 La
CLIAI 技能飞书云空间文件复制实战:lark-cli drive +copy 完全指南
飞书云空间文件复制实战:lark cli drive +copy 完全指南 本指南以官方 CLI 工具 lark cli 的 drive +copy 快捷命令为
CLIAI 技能
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考