Compare commits

..
Author SHA1 Message Date
南润 9f5923736e fix(aitable): validate field contracts before MCP 2026-08-10 17:06:41 +08:00
南润 dbe4b5f388 hint优化 2026-08-10 10:08:17 +08:00
瑞达 405a027002 fix(doc): preserve explicit headings and shorten workflows 2026-08-07 15:42:53 +08:00
南润 09643fc6a1 doc hint 优化 2026-08-06 16:40:05 +08:00
南润 a3e5145322 doc hint 优化 2026-08-06 16:24:04 +08:00
瑞达 0e4ec9af34 fix(doc): drop unnecessary fidelity guards 2026-08-06 11:56:55 +08:00
瑞达 9b62989ddb fix(doc): enforce workflow and result fidelity 2026-08-06 11:45:38 +08:00
南润 e81e040579 hint优化 2026-08-06 10:08:25 +08:00
南润 4f4fd4eb66 im优化 2026-08-05 16:25:26 +08:00
南润 8510cb8b91 im修复 2026-08-05 16:15:44 +08:00
瑞达 31cec9efa7 feat(aitable): align skill with progressive schema loading 2026-08-05 15:49:52 +08:00
瑞达 88f5a5a0e8 feat(doc): align skill routing with runtime schema 2026-08-05 15:46:24 +08:00
南润 9dd8e232bb hint优化 2026-08-04 20:24:27 +08:00
南润 7094e5eceb 修复flag 子命令hint错误 2026-08-04 10:01:55 +08:00
南润 e909f1083b hint优化 2026-08-03 21:52:09 +08:00
105 changed files with 4929 additions and 2943 deletions
@@ -1,301 +0,0 @@
# IM Chat Skill 精简与渐进加载优化方案
## 1. 背景
当前 `im-chat-skill-hint-align` 分支已经增强了 Chat Skill 的 Shortcut 路由、执行骨架、身份边界、查询与资源处理、低频原子回退和错误导航。与 Lark IM Skill 对比后,DWS 在 Agent 路由和执行约束上更直接,但根 Skill 仍存在以下问题:
- 高频执行骨架与核心意图表重复。
- Runtime Shortcut Catalog、leaf Schema 和 leaf Help 的读取规则在多处重复。
- Shortcut 错误处理与 Workflow 错误导航重复。
- 身份、ID 和三种置顶对象的边界分散在不同章节。
- 缺少简短、集中且可复用的核心对象与查询结果语义。
- Frontmatter 能力召回仍可扩展,但不能削弱 DING、邮件和班级群等产品边界。
本方案只调整根文件 `skills/multi/dingtalk-chat/SKILL.md` 的组织和必要语义,不把 API 手册、完整 Shortcut 清单或权限表重新放回根 Skill。
## 2. 修改目标
1. 提高高频 Chat 意图的直接命中率,减少不必要的 Catalog、Schema 和 `--help` 调用。
2. 提前建立渐进加载顺序,避免模型在高频任务中优先进入原子命令树。
3. 补齐身份、核心对象、ID 和查询结果的必要语义,降低错误重试和 ID 混用。
4. 合并重复 SOP,确保新增内容不会增加根 Skill 的总体 token。
5. 保持参数、安全和完整 API 事实由 leaf Schema、Help 和 references 按需提供。
## 3. 设计原则
### 3.1 根 Skill 只保留决策必需信息
根 Skill 应负责:
- Skill 触发范围和跨产品排除边界。
- 渐进加载与能力选择顺序。
- 高频意图到精确 Shortcut 的映射。
- 身份、ID、幂等、分页、部分失败等跨命令不变量。
- 低频能力的导航入口。
- 错误恢复和停止条件。
以下内容继续留在 Schema 或 references:
- 完整 Shortcut Catalog。
- API Resources 全量列表。
- 权限 scope 表。
- 叶级参数全集和接口字段格式。
- 只服务单个命令的实现细节。
### 3.2 渐进加载规则必须早于命令骨架
模型应在看到具体命令前,先知道何时直接执行、何时才加载额外上下文。但完整一级命令树不应提前,以免低频原子命令干扰高频 Shortcut 选择。
### 3.3 同一事实只保留一个权威位置
- 高频 Shortcut 只出现在一张核心意图表中。
- Catalog、Schema 和 Help 的读取顺序只定义一次。
- 错误恢复与 reference 导航只定义一次。
- 身份、对象和 ID 的公共边界集中定义,后续章节只引用,不重复解释。
## 4. 目标章节结构
```text
1. Frontmatter
2. Preconditions
3. 加载与路由顺序
4. 核心对象与 ID
5. 核心意图与执行骨架
6. 统一发送
7. 查询、资源与卡片
8. 低频原子路由
├── 一级命令树
├── branch references
└── 低频操作回退表
9. 错误恢复与按需 Reference
10. 跨产品协作
```
## 5. 具体修改
### 5.1 扩展 Frontmatter 产品能力
扩展 `description` 的正向能力召回,覆盖:
- 单聊、群聊、建群、群搜索和群成员管理。
- 消息发送、回复、转发、撤回、查询和聊天记录搜索。
- 图片、文件和消息资源下载。
- 表情回应、收藏、Pin、消息置顶和会话置顶。
- 应用机器人、Webhook 和互动卡片。
- 未读、红点、消息已读状态和会话分类。
同时保留明确排除:
- DING、短信和电话转到 `dingtalk-ding`。
- 邮件转到 `dingtalk-mail`。
- 班级群转到对应的低频产品 Skill。
- 找人本身由 `dingtalk-contact` 或 `dingtalk-aisearch` 负责,Chat 只消费真实人员 ID。
Frontmatter 只描述真实能力和路由边界,不加入参数、SOP 或 token 实现细节。
### 5.2 前移并合并渐进加载规则
将现有“Shortcut 发现”“Shortcut 执行契约”和“渐进加载与一级路由”的加载决策部分合并为紧随 Preconditions 的唯一章节:
```markdown
## 加载与路由顺序
1. 已知高频意图:直接使用“核心意图与执行骨架”,不查 Help。
2. 已有匹配 Shortcut:直接执行;参数、约束或安全不确定时才查 leaf Schema。
3. 仅 Cobra flags 不确定时查 leaf `--help`。
4. 现有路由无法定位低频能力时,才查 Runtime Shortcut Catalog。
5. 没有 Shortcut 时,按需读取对应 branch reference,进入原子命令。
```
同一章节保留以下公共规则:
- 路由优先级为 `exact recipe/runnable script > public Shortcut > atomic command`。
- 不猜测 `cli_path` 或参数名称。
- `confirmation=user_required` 时先确认,再添加 `--yes`。
- 来源冲突时采用更安全的解释并报告契约漂移。
- 命令已确定且参数清楚时直接执行,不为验证已知路径重复发现。
删除其他章节重复出现的 Catalog、Schema、Help 选择说明。
### 5.3 新增“核心对象与 ID”小表
增加不超过 8 行的表格,集中表达:
| 对象 | 核心标识与边界 |
|---|---|
| 人员 | 姓名必须先解析成唯一真实的 `userId` 或 `openDingTalkId`,名称不能作为 ID 传递 |
| 会话 | 使用真实 `openConversationId` / cid;群名只能用于 Shortcut 的目标解析 |
| 消息 | 使用真实 `openMessageId` / msgId,并保持与身份及会话一致 |
| 发送任务 | `openTaskId` 只用于查询发送状态,不能替代消息 ID |
| Thread | thread/topic ID 必须绑定真实会话,不跨会话复用 |
| 身份 | current-user、app-bot 和 Webhook 是不同操作者,不能自动互换 |
| 状态 | 收藏、消息置顶、消息 Pin 和会话置顶作用于不同对象 |
新增后删除后文对这些边界的重复说明。
### 5.4 合并高频骨架与核心意图表
删除独立的“高频直接执行骨架”,将其全部合入唯一的“核心意图与执行骨架”表。表格固定为三列:
| 用户意图 | 精确 Shortcut 骨架 | 必须保留的执行边界 |
|---|---|---|
至少覆盖以下高频场景:
- 姓名发单聊、群名发群消息。
- user、bot、webhook 三种身份发送。
- 建群、改群名、拉人和成员查询。
- 拉取会话消息、查询详情、撤回和发送状态。
- 关键词搜索、组合搜索和查询 @ 我的消息。
- 群邀请链接和群机器人。
- 会话置顶和收藏列表。
- 查和某人的聊天记录。
- 群消息翻页导出。
- 机器人多群广播。
表中直接给出正确参数骨架;命中后照抄参数名,不先调用 `--help`。同一个 Shortcut 不再在其他表中重复列出。
### 5.5 补充统一身份规则
在“统一发送”开头加入统一规则:
> 身份决定真实操作者、可见范围和可用能力;同一目标使用 user、bot 或 webhook 时,结果和权限可能不同,禁止自动切换身份重试。
继续保留:
- 发送前检查身份、目标、正文、标题、@、消息类型和附件路径。
- 重试复用相同 `--idempotency-key`。
- user、bot、webhook 的精确发送模板。
- @ 占位符、新行和文件能力边界。
不加入 Lark 的 access token 类型说明。
### 5.6 补充查询结果与增强失败语义
在“查询、资源与卡片”中增加:
- 发送者名称缺失时保留真实 ID,不猜姓名,也不自动扩大通讯录查询。
- 可选增强字段缺失不代表主查询失败;增强请求失败时保留主结果并写入 per-item ledger。
继续保留:
- `--page-all` 只在确需完整分页时使用。
- 部分失败保留已有结果,禁止把不完整结果声明为完整。
- 资源下载默认关闭,显式请求后才增加请求和本地输出。
- 子消息资源优先使用子 `messageId`。
- 输出路径、覆盖、HTTPS 和重定向安全限制。
### 5.7 拆分“渐进加载”与“一级命令树”
前移的只有加载决策。完整一级命令树及 branch references 改名为“低频原子路由”,保留在高频意图、统一发送和查询规则之后。
这样可以:
- 防止高频任务优先进入 atomic branch。
- 降低不必要的 Schema 和 Help 查询。
- 继续为没有 Shortcut 的能力提供确定导航。
低频原子回退表继续保留收藏、编辑、外部群升级、群昵称、分类、共同群、群公告、群身份、置顶、未读、已读、授权、退群和解散群等差异化入口。
### 5.8 合并错误恢复与 Workflow 导航
将现有“Shortcut 错误处理”和“Workflow 与错误导航”合并为:
```markdown
## 错误恢复与按需 Reference
```
只保留以下规则:
- 路径或参数错误时,按 Catalog、Schema、Help 的既定顺序校正一次。
- 始终从实际输出重新提取下游 ID。
- 复杂消息任务按需读取 `01-messaging.md`。
- Onboarding 按需读取对应 workflow。
- 命令错误按需读取 `chat-error-recovery.md`。
- 权限不足、歧义未消除、无结果或契约冲突时停止并报告。
删除其他位置重复的 `01-messaging.md` 和错误恢复入口。
### 5.9 保留跨产品协作边界
继续保留根 Skill 中不可由 Chat 自己完成的路由:
- 人名解析到 Contact / AISearch。
- DING、短信和电话到 Ding Skill。
- 邮件到 Mail Skill。
- 本地文件与已有 mediaId 的发送差异。
如果某项边界已经在 Frontmatter 或核心对象表中完整表达,正文只保留执行阶段真正需要的补充,不重复整段说明。
## 6. 删除与合并清单
| 当前内容 | 处理方式 |
|---|---|
| “Shortcut 发现(按需)” | 合入前置“加载与路由顺序” |
| “Shortcut 执行契约” | 公共规则合入前置章节 |
| “高频直接执行骨架” | 删除,内容合入核心意图表 |
| “渐进加载与一级路由”中的加载说明 | 前移并去重 |
| 完整一级命令树 | 保留,改放“低频原子路由” |
| “Shortcut 错误处理” | 合入统一错误章节 |
| “Workflow 与错误导航” | 合入统一错误章节 |
| 分散的身份、ID、置顶说明 | 合入核心对象表或统一身份规则 |
| 重复的 `01-messaging.md` 入口 | 只保留一处 |
## 7. 不纳入本次修改
- 不展开 97 个公开 Shortcut。
- 不复制 Lark 的完整 API Resources 和权限 scope 表。
- 不在根 Skill 中维护 leaf 参数全集。
- 不引入与当前 CLI 不一致的新命令或参数。
- 不改变 Schema、Help、Runtime Catalog 和 reference 的事实优先级。
- 不通过增加默认查询、自动通讯录查询或默认资源增强来换取结果丰富度。
## 8. 实施顺序
1. 更新 Frontmatter description,确认能力召回和排除边界。
2. 合并并前移“加载与路由顺序”。
3. 新增“核心对象与 ID”表,删除相应重复边界。
4. 合并高频骨架和核心意图表。
5. 补充统一身份规则。
6. 补充查询结果和增强失败语义。
7. 将一级命令树调整为后置的“低频原子路由”。
8. 合并错误恢复与 reference 导航。
9. 全文检查重复命令、重复 reference 和冲突参数。
10. 运行 Skill 格式及相关策略测试,并用高频场景做静态路由验证。
## 9. 验收标准
### 9.1 内容与结构
- 根 Skill 中只有一份 Catalog、Schema、Help 读取顺序。
- 根 Skill 中只有一张高频意图与 Shortcut 骨架表。
- 根 Skill 中只有一个错误恢复章节。
- `01-messaging.md` 的同类导航不重复。
- 核心对象与 ID 表不超过 8 行数据。
- 一级原子命令树位于高频路由之后。
- Frontmatter 同时覆盖主要产品能力和明确排除边界。
### 9.2 执行行为
- “发给某人”“发到某群”“查 @ 我”“改群名”等高频意图可直接选中已评审 Shortcut,不先查 Help。
- 低频未知意图才触发 Runtime Shortcut Catalog。
- 参数或安全不确定时读取 leaf Schema;只有 Cobra flags 不确定时读取 leaf Help。
- user、bot、webhook 不被自动互换。
- `openTaskId`、消息 ID、会话 ID 不混用。
- 发送者名称或 reaction 等增强缺失时,不把主查询误判为失败。
- 部分失败保留已有结果并明确报告 ledger/completeness。
### 9.3 Token 与维护成本
- 修改后的根 Skill 不超过当前 211 行,并以不丢失必要路由和边界为前提尽量低于 195 行。
- 文件单词数和字符数不高于修改前基线。
- 新增内容通过删除重复 SOP 抵消。
- 不新增完整 Shortcut、API 或权限清单。
## 10. 预期收益
- 减少高频任务中的 `--help` 和重复 Schema 查询。
- 降低因身份切换、ID 混用和发送者名称缺失导致的错误重试。
- 让 Shortcut、Schema、Help、Catalog 和 references 各自保持单一职责。
- 在不增加默认 token 和耗时的前提下,提高 Chat Skill 的选择准确率和执行成功率。
- 降低后续新增 Shortcut 时同时维护多张表和多处规则的漂移风险。
@@ -15,11 +15,75 @@ package app
import (
stderrors "errors"
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
)
func TestChatGroupCreateMembersGuidanceUsesGenericExample(t *testing.T) {
root := NewRootCommand()
cmd := mustFindCommand(t, root, "chat", "group", "create")
err := enrichChatWorkbookError(cmd, stderrors.New("unknown flag: --members"))
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("error = %T, want *errors.Error", err)
}
joined := strings.Join(typed.Examples, "\n")
for _, unwanted := range []string{"V2评审小组", "489149", "550582"} {
if strings.Contains(joined, unwanted) {
t.Fatalf("example is fitted to evaluation data %q: %s", unwanted, joined)
}
}
for _, want := range []string{"<群名称>", "<userId1>,<userId2>"} {
if !strings.Contains(joined, want) {
t.Fatalf("generic example missing %q: %s", want, joined)
}
}
}
func TestChatHintPathsAreReportedAsUnknownSubcommandsBeforeFlagParsing(t *testing.T) {
for _, tc := range []struct {
name string
args []string
message string
flag string
}{
{name: "group search", args: []string{"chat", "group", "search", "--query", "1", "--format", "json"}, message: "chat group 下不存在 search 子命令", flag: "--query"},
{name: "send", args: []string{"chat", "send", "--group", "cid", "--text", "hi", "--format", "json"}, message: "chat 下不存在 send 子命令", flag: "--group"},
{name: "history", args: []string{"chat", "history", "--group", "cid", "--time", "2026-08-03 10:00:00", "--format", "json"}, message: "chat 下不存在 history 子命令", flag: "--group"},
} {
t.Run(tc.name, func(t *testing.T) {
err := validateChatWorkbookRawArgs(tc.args)
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("error = %T, want *errors.Error", err)
}
if typed.Message != tc.message || typed.Reason != "unknown_subcommand" {
t.Fatalf("guidance = %#v", typed)
}
if strings.Contains(typed.Message, tc.flag) || strings.Contains(typed.Reason, "unknown flag") {
t.Fatalf("subcommand error was misreported as a flag error: %#v", typed)
}
})
}
root := NewRootCommand()
chat := mustFindCommand(t, root, "chat")
for _, child := range chat.Commands() {
if child.Name() == "send" || child.Name() == "history" {
t.Fatalf("chat %s must not be registered as a hint subcommand", child.Name())
}
}
group := mustFindCommand(t, root, "chat", "group")
for _, child := range group.Commands() {
if child.Name() == "search" {
t.Fatal("chat group search must not be registered as a hint subcommand")
}
}
}
func TestValidateChatWorkbookRawArgs(t *testing.T) {
t.Parallel()
+68
View File
@@ -82,6 +82,74 @@ func TestFlagErrorWithSuggestions_unknownFlagHintAndFlags(t *testing.T) {
}
}
func TestFlagErrorWithSuggestionsDocParameterFamilies(t *testing.T) {
for _, tc := range []struct {
name string
path []string
flag string
wantHint string
wantText string
}{
{"shortcut-create-file", []string{"doc", "+create"}, "content-file", "doc create", "--content-file"},
{"inspect-info", []string{"doc", "+inspect"}, "include-info", "移除 --include-info", "默认返回"},
{"inspect-versions", []string{"doc", "+inspect"}, "include-versions", "--include-history", "版本历史"},
{"inspect-generic", []string{"doc", "+inspect"}, "include", "具体 --include-*", "block list"},
} {
t.Run(tc.name, func(t *testing.T) {
root := &cobra.Command{Use: "dws"}
parent := root
for _, name := range tc.path {
child := &cobra.Command{Use: name}
parent.AddCommand(child)
parent = child
}
cmd := parent
err := flagErrorWithSuggestions(cmd, fmt.Errorf("unknown flag: --%s", tc.flag))
var typed *apperrors.Error
if !stderrors.As(err, &typed) {
t.Fatalf("want structured validation, got %T: %v", err, err)
}
if typed.Reason != "doc_parameter_family_mismatch" || !strings.Contains(typed.Hint, tc.wantHint) || !strings.Contains(typed.Message, tc.wantText) {
t.Fatalf("error = reason %q hint %q message %q", typed.Reason, typed.Hint, typed.Message)
}
})
}
}
func TestDocParameterFamilyErrorsStopBeforeDispatch(t *testing.T) {
for _, tc := range []struct {
path []string
flag string
}{
{[]string{"doc", "+create"}, "content-file"},
{[]string{"doc", "+inspect"}, "include-info"},
{[]string{"doc", "+inspect"}, "include-versions"},
{[]string{"doc", "+inspect"}, "include"},
} {
t.Run(strings.Join(tc.path, "/")+"/"+tc.flag, func(t *testing.T) {
calls := 0
root := &cobra.Command{Use: "dws", SilenceUsage: true, SilenceErrors: true}
root.SetFlagErrorFunc(flagErrorWithSuggestions)
parent := root
for _, name := range tc.path {
child := &cobra.Command{Use: name}
parent.AddCommand(child)
parent = child
}
parent.RunE = func(*cobra.Command, []string) error { calls++; return nil }
root.SetArgs(append(tc.path, "--"+tc.flag))
err := root.Execute()
var typed *apperrors.Error
if !stderrors.As(err, &typed) || typed.Reason != "doc_parameter_family_mismatch" {
t.Fatalf("error = %#v", err)
}
if calls != 0 {
t.Fatalf("dispatch calls = %d, want 0", calls)
}
})
}
}
// TestFlagErrorWithSuggestions_fallbackTailHint 验证 fallback 路径(非 unknown flag 类错误,
// 如 missing required flag / ambiguous shorthand)也带尾部 See '<cmd> --help' for usage.
// 这是 wukong / docker / kubectl 的通用 UX——任何 flag 解析错误都给用户一条 help 入口。
+80 -1
View File
@@ -168,6 +168,27 @@ func suppressJSONDeprecationPreamble(root *cobra.Command, args []string) {
func validateChatWorkbookRawArgs(args []string) error {
path := strings.Join(args, " ")
switch {
case len(args) >= 3 && args[0] == "chat" && args[1] == "group" && args[2] == "search":
return apperrors.NewValidation(
"chat group 下不存在 search 子命令",
apperrors.WithReason("unknown_subcommand"),
apperrors.WithActions("群聊搜索使用 dws chat search,而不是 dws chat group search", "移除路径中的 group 后重试"),
apperrors.WithExamples(`dws chat search --query <群名关键词> --format json`),
)
case len(args) >= 2 && args[0] == "chat" && args[1] == "send":
return apperrors.NewValidation(
"chat 下不存在 send 子命令",
apperrors.WithReason("unknown_subcommand"),
apperrors.WithActions("发送消息使用 dws chat message send,而不是 dws chat send", "在路径中补充 message 后重试"),
apperrors.WithExamples(`dws chat message send --group <openConversationId> --text <消息正文> --format json`),
)
case len(args) >= 2 && args[0] == "chat" && args[1] == "history":
return apperrors.NewValidation(
"chat 下不存在 history 子命令",
apperrors.WithReason("unknown_subcommand"),
apperrors.WithActions("查询会话消息使用 dws chat message list,而不是 dws chat history", "改用 message list 并按帮助补充目标和时间参数"),
apperrors.WithExamples(`dws chat message list --group <openConversationId> --time <YYYY-MM-DD HH:mm:ss> --format json`),
)
case strings.HasPrefix(path, "chat message send ") && rawArgsFlagValue(args, "msg-type") == "file" &&
rawArgsContainFlag(args, "media-id"):
return apperrors.NewValidation(
@@ -441,7 +462,7 @@ func enrichChatWorkbookError(cmd *cobra.Command, err error) error {
"建群命令不支持 --members",
"chat group create 使用 --users 接收逗号分隔的成员 userId;--members 是其他命令的参数名",
[]string{"将 --members 改为 --users", "成员标识不确定时先查询 userId"},
[]string{`dws chat group create --name "V2评审小组" --users 489149,550582 --format json`},
[]string{`dws chat group create --name "<群名称>" --users <userId1>,<userId2> --format json`},
}
case path == "chat group bots" && strings.Contains(message, "unknown flag: --id"):
guide = chatWorkbookGuidance{
@@ -600,6 +621,61 @@ func enrichChatWorkbookError(cmd *cobra.Command, err error) error {
)
}
// enrichDocFlagError turns common native/shortcut parameter-family mixups into
// actionable local errors. It only uses the current Cobra command path and
// parse error, so it always runs before any helper RunE or MCP dispatch.
func enrichDocFlagError(cmd *cobra.Command, err error) error {
if cmd == nil || err == nil {
return err
}
path := cmd.CommandPath()
if fields := strings.Fields(path); len(fields) > 1 {
path = strings.Join(fields[1:], " ")
}
message := err.Error()
var guide chatWorkbookGuidance
switch {
case path == "doc +create" && (strings.Contains(message, "unknown flag: --content-file") || strings.Contains(message, "unknown flag: --content-format")):
guide = chatWorkbookGuidance{
"文档创建命令与参数不属于同一组",
"+create 是精简 Shortcut,不支持 --content-file/--content-format;需要从文件写入或指定 Markdown/JSONML 时应使用原生命令 doc create",
[]string{"改用 dws doc create,并保留 --content-file/--content-format", "只创建短文本时才继续使用 +create 的 --content"},
[]string{`dws doc create --name "<标题>" --content-file ./body.md --content-format markdown --format json`},
}
case path == "doc +inspect" && strings.Contains(message, "unknown flag: --include-info"):
guide = chatWorkbookGuidance{
"+inspect 不需要 --include-info",
"文档基本信息会由 +inspect 默认返回;移除 --include-info,只按需增加真实存在的 --include-history、--include-media、--include-comments、--include-permissions 或 --include-style",
[]string{"移除 --include-info", "只保留任务需要的 --include-* 参数"},
[]string{`dws doc +inspect --node <DOC_ID> --format json`},
}
case path == "doc +inspect" && strings.Contains(message, "unknown flag: --include-versions"):
guide = chatWorkbookGuidance{
"+inspect 不支持 --include-versions",
"版本历史对应的参数名是 --include-history;如果只需要版本列表,也可以使用 dws doc version list",
[]string{"将 --include-versions 改为 --include-history", "只查询版本时使用 doc version list"},
[]string{`dws doc +inspect --node <DOC_ID> --include-history --format json`, `dws doc version list --node <DOC_ID> --format json`},
}
case path == "doc +inspect" && strings.Contains(message, "unknown flag: --include"):
guide = chatWorkbookGuidance{
"+inspect 没有通用 --include 参数",
"请直接使用具体开关:--include-history、--include-media、--include-comments、--include-permissions 或 --include-style;文档块请使用 doc block list",
[]string{"把 --include <类型> 改成对应的具体 --include-* 开关", "需要 blockId 时改用 doc block list"},
[]string{`dws doc +inspect --node <DOC_ID> --include-history --format json`, `dws doc block list --node <DOC_ID> --format json`},
}
default:
return err
}
return apperrors.NewValidation(
guide.message+": "+guide.reason,
apperrors.WithReason("doc_parameter_family_mismatch"),
apperrors.WithHint(guide.actions[0]),
apperrors.WithActions(guide.actions...),
apperrors.WithExamples(guide.examples...),
apperrors.WithCause(err),
)
}
// newPreParseValidationError keeps pipeline handler identity in internal logs
// while exposing only the underlying parameter-domain error to CLI users.
func newPreParseValidationError(err error) error {
@@ -682,6 +758,9 @@ func flagErrorWithSuggestions(cmd *cobra.Command, err error) error {
if enriched := enrichChatWorkbookError(cmd, err); enriched != err {
return enriched
}
if enriched := enrichDocFlagError(cmd, err); enriched != err {
return enriched
}
// Common flag aliases and suggestions
suggestions := map[string]string{
+50
View File
@@ -90,6 +90,56 @@ func TestChatAgentGuidanceRendersOnlyOnStdout(t *testing.T) {
}
}
func TestCandidateAlignmentChatGuidance(t *testing.T) {
tests := []struct {
args []string
wants []string
}{
{[]string{"chat", "message", "query-send-status", "--help"}, []string{"sendStatus=SUCCESS", "FAILED"}},
{[]string{"chat", "+messages-query-send-status", "--help"}, []string{"sendStatus=SUCCESS", "openTaskId"}},
{[]string{"chat", "message", "set-pin-msg", "--help"}, []string{"openTaskId", "set-top-msg", "chat set-top"}},
{[]string{"chat", "message", "unset-pin-msg", "--help"}, []string{"复用", "unset-top-msg"}},
{[]string{"chat", "message", "add-emoji", "--help"}, []string{"同一条真实消息", "openTaskId"}},
{[]string{"chat", "message", "remove-emoji", "--help"}, []string{"复用", "表情名称"}},
{[]string{"chat", "group", "members", "remove", "--help"}, []string{"群主", "转让群主"}},
{[]string{"chat", "group", "update-icon", "--help"}, []string{"dentryId", "能力边界"}},
{[]string{"chat", "group", "update-settings", "--help"}, []string{"群级设置", "user-settings set"}},
{[]string{"chat", "group", "user-settings", "query", "--help"}, []string{"当前用户视角", "保存原值"}},
{[]string{"chat", "group", "user-settings", "set", "--help"}, []string{"再次 query", "真实值恢复"}},
}
for _, tc := range tests {
cmd := NewRootCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(io.Discard)
cmd.SetArgs(tc.args)
if err := cmd.Execute(); err != nil {
t.Fatalf("%v: %v", tc.args, err)
}
for _, want := range tc.wants {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("%v help missing %q:\n%s", tc.args, want, stdout.String())
}
}
}
}
func TestCandidateAlignmentDriveUploadSchemaGuidance(t *testing.T) {
cmd := NewRootCommand()
var stdout bytes.Buffer
cmd.SetOut(&stdout)
cmd.SetErr(io.Discard)
cmd.SetArgs([]string{"schema", "drive.upload", "--format", "json"})
if err := cmd.Execute(); err != nil {
t.Fatalf("drive.upload schema: %v", err)
}
for _, want := range []string{"暂时不要发送", "不会发送聊天消息"} {
if !strings.Contains(stdout.String(), want) {
t.Fatalf("drive.upload schema missing %q:\n%s", want, stdout.String())
}
}
}
func TestRootKeepsMainBranchChatCompatibilityCommands(t *testing.T) {
root := NewRootCommand()
listDirect := mustFindCommand(t, root, "chat", "message", "list-direct")
+1 -1
View File
@@ -1099,7 +1099,7 @@ var generatedParamAliases = []ParamAliasEntry{
"open-conversation-ids": "conversation-ids",
"user-ids": "users",
},
Blocked: []string{"at-user-ids", "chat-id", "conversation-id", "group-id", "group-ids", "open-conversation-id", "staff-id", "uid", "userid"},
Blocked: []string{"at-user-ids", "chat-id", "group-id", "group-ids", "open-conversation-id", "staff-id", "uid", "userid"},
},
{
CLIPath: "chat message send",
+99 -87
View File
@@ -264,7 +264,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
],
"confirmation": "not_required",
"effect": "read",
@@ -308,7 +308,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -317,7 +317,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -525,7 +525,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
],
"confirmation": "not_required",
"effect": "read",
@@ -568,7 +568,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -577,7 +577,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -1815,7 +1815,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
],
"confirmation": "not_required",
"effect": "read",
@@ -1858,7 +1858,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -1867,7 +1867,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -3624,7 +3624,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
],
"confirmation": "not_required",
"effect": "read",
@@ -3667,7 +3667,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -3676,7 +3676,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4659,7 +4659,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
],
"confirmation": "not_required",
"effect": "read",
@@ -4702,7 +4702,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4711,7 +4711,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4877,7 +4877,7 @@
},
"use_when": {
"value": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4886,7 +4886,7 @@
"candidates": [
{
"value": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4909,7 +4909,7 @@
"internal/cli/schema_hints/selection/aitable.json"
],
"use_when": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
]
},
"aitable +resolve-table": {
@@ -4917,7 +4917,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
],
"confirmation": "not_required",
"effect": "read",
@@ -4960,7 +4960,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -4969,7 +4969,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -5135,7 +5135,7 @@
},
"use_when": {
"value": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -5144,7 +5144,7 @@
"candidates": [
{
"value": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -5167,7 +5167,7 @@
"internal/cli/schema_hints/selection/aitable.json"
],
"use_when": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
]
},
"aitable +role-list": {
@@ -5949,7 +5949,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
],
"confirmation": "not_required",
"effect": "read",
@@ -5993,7 +5993,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -6002,7 +6002,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11191,11 +11191,11 @@
]
},
"aitable base search": {
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
],
"confirmation": "not_required",
"effect": "read",
@@ -11205,14 +11205,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -11244,7 +11244,7 @@
},
"avoid_when": {
"value": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11253,7 +11253,7 @@
"candidates": [
{
"value": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11407,7 +11407,7 @@
},
"use_when": {
"value": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11416,7 +11416,7 @@
"candidates": [
{
"value": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -11441,10 +11441,12 @@
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.base_search",
"skills/mono/references/products/aitable.md"
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
],
"use_when": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
]
},
"aitable base update": {
@@ -16699,11 +16701,11 @@
]
},
"aitable field get": {
"agent_summary": "获取字段完整配置。",
"agent_summary": "底层获取字段完整类型与 config。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
],
"confirmation": "not_required",
"effect": "read",
@@ -16713,14 +16715,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "获取字段完整配置。",
"value": "底层获取字段完整类型与 config。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "获取字段完整配置。",
"value": "底层获取字段完整类型与 config。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -16752,7 +16754,7 @@
},
"avoid_when": {
"value": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16761,7 +16763,7 @@
"candidates": [
{
"value": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16913,7 +16915,7 @@
},
"use_when": {
"value": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16922,7 +16924,7 @@
"candidates": [
{
"value": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -16950,10 +16952,12 @@
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.field_get",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-field.md"
"skills/mono/references/products/aitable/aitable-field.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md"
],
"use_when": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
]
},
"aitable field list": {
@@ -22339,11 +22343,11 @@
]
},
"aitable record create": {
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
],
"confirmation": "not_required",
"effect": "write",
@@ -22353,14 +22357,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "新增记录(cells 的 key 必须是 fieldId)。",
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "新增记录(cells 的 key 必须是 fieldId)。",
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -22392,7 +22396,7 @@
},
"avoid_when": {
"value": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22401,7 +22405,7 @@
"candidates": [
{
"value": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22553,7 +22557,7 @@
},
"use_when": {
"value": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22562,7 +22566,7 @@
"candidates": [
{
"value": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -22592,10 +22596,12 @@
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/mono/references/products/aitable/aitable-record-create.md"
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md"
],
"use_when": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
]
},
"aitable record delete": {
@@ -23929,11 +23935,11 @@
]
},
"aitable record query": {
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"confirmation": "not_required",
"effect": "read",
@@ -23943,14 +23949,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -23982,7 +23988,7 @@
},
"avoid_when": {
"value": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -23991,7 +23997,7 @@
"candidates": [
{
"value": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24151,7 +24157,7 @@
},
"use_when": {
"value": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24160,7 +24166,7 @@
"candidates": [
{
"value": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24189,10 +24195,12 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.query_records",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-record-query.md"
"skills/mono/references/products/aitable/aitable-record-query.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md"
],
"use_when": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
]
},
"aitable record query-empty": {
@@ -24711,11 +24719,11 @@
]
},
"aitable record update": {
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"confirmation": "not_required",
"effect": "write",
@@ -24725,14 +24733,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -24764,7 +24772,7 @@
},
"avoid_when": {
"value": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24773,7 +24781,7 @@
"candidates": [
{
"value": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24925,7 +24933,7 @@
},
"use_when": {
"value": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24934,7 +24942,7 @@
"candidates": [
{
"value": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -24963,10 +24971,12 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.record_update",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md"
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md"
],
"use_when": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
]
},
"aitable record upsert": {
@@ -27862,11 +27872,11 @@
]
},
"aitable table get": {
"agent_summary": "获取数据表结构(字段+视图目录)。",
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
],
"confirmation": "not_required",
"effect": "read",
@@ -27876,14 +27886,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "获取数据表结构(字段+视图目录)。",
"value": "底层获取数据表结构、精简字段目录与视图目录。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"candidates": [
{
"value": "获取数据表结构(字段+视图目录)。",
"value": "底层获取数据表结构、精简字段目录与视图目录。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -27915,7 +27925,7 @@
},
"avoid_when": {
"value": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -27924,7 +27934,7 @@
"candidates": [
{
"value": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -28076,7 +28086,7 @@
},
"use_when": {
"value": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -28085,7 +28095,7 @@
"candidates": [
{
"value": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -28115,10 +28125,12 @@
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-advperm.md",
"skills/mono/references/products/aitable/aitable-primary-doc.md",
"skills/mono/references/products/aitable/aitable-record-create.md"
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
],
"use_when": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
]
},
"aitable table list": {
+87 -90
View File
@@ -9301,11 +9301,11 @@
]
},
"chat +messages-query-send-status": {
"agent_summary": "查询消息发送状态",
"agent_summary": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
],
"confirmation": "not_required",
"effect": "read",
@@ -9315,14 +9315,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "查询消息发送状态",
"value": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"candidates": [
{
"value": "查询消息发送状态",
"value": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -9348,7 +9348,7 @@
},
"avoid_when": {
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -9357,7 +9357,7 @@
"candidates": [
{
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -9523,7 +9523,7 @@
},
"use_when": {
"value": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -9532,7 +9532,7 @@
"candidates": [
{
"value": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -9555,7 +9555,7 @@
"internal/cli/schema_hints/selection/chat.json"
],
"use_when": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
]
},
"chat +messages-read-status": {
@@ -11145,8 +11145,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
],
"confirmation": "not_required",
"effect": "read",
@@ -11190,8 +11189,7 @@
},
"avoid_when": {
"value": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -11200,8 +11198,7 @@
"candidates": [
{
"value": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -20570,7 +20567,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
],
"confirmation": "not_required",
"effect": "write",
@@ -20619,7 +20616,7 @@
},
"avoid_when": {
"value": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -20628,7 +20625,7 @@
"candidates": [
{
"value": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -20807,7 +20804,7 @@
},
"use_when": {
"value": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -20816,7 +20813,7 @@
"candidates": [
{
"value": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -20847,7 +20844,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
]
},
"chat group members remove-bot": {
@@ -24067,7 +24064,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
],
"confirmation": "not_required",
"effect": "write",
@@ -24116,7 +24113,7 @@
},
"avoid_when": {
"value": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24125,7 +24122,7 @@
"candidates": [
{
"value": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24271,7 +24268,7 @@
},
"use_when": {
"value": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24280,7 +24277,7 @@
"candidates": [
{
"value": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24309,7 +24306,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
]
},
"chat group update-nick": {
@@ -24605,7 +24602,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
],
"confirmation": "not_required",
"effect": "write",
@@ -24654,7 +24651,7 @@
},
"avoid_when": {
"value": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24663,7 +24660,7 @@
"candidates": [
{
"value": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24809,7 +24806,7 @@
},
"use_when": {
"value": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24818,7 +24815,7 @@
"candidates": [
{
"value": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -24847,7 +24844,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
]
},
"chat group upgrade-to-external": {
@@ -25115,7 +25112,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
],
"confirmation": "not_required",
"effect": "read",
@@ -25158,7 +25155,7 @@
},
"avoid_when": {
"value": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25167,7 +25164,7 @@
"candidates": [
{
"value": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25333,7 +25330,7 @@
},
"use_when": {
"value": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25342,7 +25339,7 @@
"candidates": [
{
"value": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25365,7 +25362,7 @@
"wukong-develop:wukong/products"
],
"use_when": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
]
},
"chat group user-settings set": {
@@ -25373,7 +25370,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
],
"confirmation": "not_required",
"effect": "write",
@@ -25416,7 +25413,7 @@
},
"avoid_when": {
"value": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25425,7 +25422,7 @@
"candidates": [
{
"value": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25591,7 +25588,7 @@
},
"use_when": {
"value": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25600,7 +25597,7 @@
"candidates": [
{
"value": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -25623,7 +25620,7 @@
"wukong-develop:wukong/products"
],
"use_when": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
]
},
"chat group-mute": {
@@ -29276,11 +29273,11 @@
]
},
"chat message add-emoji": {
"agent_summary": "给指定消息添加表情回应",
"agent_summary": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
],
"confirmation": "not_required",
"effect": "write",
@@ -29290,14 +29287,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "给指定消息添加表情回应",
"value": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"candidates": [
{
"value": "给指定消息添加表情回应",
"value": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -29329,7 +29326,7 @@
},
"avoid_when": {
"value": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -29338,7 +29335,7 @@
"candidates": [
{
"value": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -29492,7 +29489,7 @@
},
"use_when": {
"value": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -29501,7 +29498,7 @@
"candidates": [
{
"value": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -29529,7 +29526,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
]
},
"chat message add-favorite": {
@@ -34637,11 +34634,11 @@
]
},
"chat message query-send-status": {
"agent_summary": "查询异步消息发送任务的状态",
"agent_summary": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
],
"confirmation": "not_required",
"effect": "read",
@@ -34651,14 +34648,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "查询异步消息发送任务的状态",
"value": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"candidates": [
{
"value": "查询异步消息发送任务的状态",
"value": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -34690,7 +34687,7 @@
},
"avoid_when": {
"value": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -34699,7 +34696,7 @@
"candidates": [
{
"value": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -34845,7 +34842,7 @@
},
"use_when": {
"value": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -34854,7 +34851,7 @@
"candidates": [
{
"value": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -34883,7 +34880,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
]
},
"chat message read-status": {
@@ -35694,11 +35691,11 @@
]
},
"chat message remove-emoji": {
"agent_summary": "移除指定消息上的表情回应",
"agent_summary": "从同一条真实消息移除此前添加的同名表情回应",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
],
"confirmation": "not_required",
"effect": "write",
@@ -35708,14 +35705,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "移除指定消息上的表情回应",
"value": "从同一条真实消息移除此前添加的同名表情回应",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"candidates": [
{
"value": "移除指定消息上的表情回应",
"value": "从同一条真实消息移除此前添加的同名表情回应",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -35747,7 +35744,7 @@
},
"avoid_when": {
"value": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -35756,7 +35753,7 @@
"candidates": [
{
"value": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -35910,7 +35907,7 @@
},
"use_when": {
"value": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -35919,7 +35916,7 @@
"candidates": [
{
"value": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -35947,7 +35944,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
]
},
"chat message remove-favorite": {
@@ -38322,11 +38319,11 @@
]
},
"chat message set-pin-msg": {
"agent_summary": "把指定消息设为会话置顶消息",
"agent_summary": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
],
"confirmation": "not_required",
"effect": "write",
@@ -38336,14 +38333,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "把指定消息设为会话置顶消息",
"value": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"candidates": [
{
"value": "把指定消息设为会话置顶消息",
"value": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -38375,7 +38372,7 @@
},
"avoid_when": {
"value": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -38384,7 +38381,7 @@
"candidates": [
{
"value": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -38530,7 +38527,7 @@
},
"use_when": {
"value": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -38539,7 +38536,7 @@
"candidates": [
{
"value": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -38568,7 +38565,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
]
},
"chat message set-top-msg": {
@@ -38857,11 +38854,11 @@
]
},
"chat message unset-pin-msg": {
"agent_summary": "取消指定消息的会话置顶",
"agent_summary": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
],
"confirmation": "not_required",
"effect": "write",
@@ -38871,14 +38868,14 @@
],
"field_provenance": {
"agent_summary": {
"value": "取消指定消息的会话置顶",
"value": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"candidates": [
{
"value": "取消指定消息的会话置顶",
"value": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
"selected": true,
@@ -38910,7 +38907,7 @@
},
"avoid_when": {
"value": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -38919,7 +38916,7 @@
"candidates": [
{
"value": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -39065,7 +39062,7 @@
},
"use_when": {
"value": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -39074,7 +39071,7 @@
"candidates": [
{
"value": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
],
"source": "internal/cli/schema_hints/selection/chat.json",
"precedence": "reviewed_explicit",
@@ -39103,7 +39100,7 @@
"skills/mono/references/products/chat.md"
],
"use_when": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
]
},
"chat message unset-top-msg": {
File diff suppressed because it is too large Load Diff
+21 -12
View File
@@ -10478,8 +10478,10 @@
"agent_summary_source": "dws-agent-selection/drive",
"availability": "available",
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -10496,14 +10498,14 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件",
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10525,27 +10527,31 @@
},
"avoid_when": {
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10601,7 +10607,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
@@ -10611,7 +10617,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10704,7 +10710,7 @@
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": false,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
},
@@ -10733,24 +10739,26 @@
"use_when": {
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"candidates": [
{
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"source": "internal/cli/schema_hints/selection/drive.json",
"precedence": "reviewed_explicit",
"selected": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。"
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。"
}
]
}
@@ -10774,7 +10782,8 @@
],
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
},
+14 -13
View File
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:7111ced2e1cd0764338aacce792d95603734177a82deea4a4642626691940485",
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"coverage": {
"surface_products": 26,
@@ -12,8 +12,8 @@
"tools_with_avoid_when": 875,
"tools_with_examples": 875,
"tools_with_interface_mode": 875,
"unmatched_skill_tools": 96,
"unreviewed_skill_tools": 11
"unmatched_skill_tools": 97,
"unreviewed_skill_tools": 12
},
"products": {
"aisearch": {
@@ -88,20 +88,20 @@
]
},
"aitable": {
"agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary_source": "dws-agent-selection/aitable",
"avoid_when": [
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
],
"field_provenance": {
"agent_summary": {
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"candidates": [
{
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
"selected": true
@@ -110,7 +110,7 @@
},
"avoid_when": {
"value": [
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -118,7 +118,7 @@
"candidates": [
{
"value": [
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -128,7 +128,7 @@
},
"use_when": {
"value": [
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -136,7 +136,7 @@
"candidates": [
{
"value": [
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
],
"source": "internal/cli/schema_hints/selection/aitable.json",
"precedence": "reviewed_explicit",
@@ -153,10 +153,11 @@
"internal/cli/schema_hints/selection/aitable.json",
"skills/mono/SKILL.md",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/aitable.md"
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
],
"use_when": [
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
]
},
"attendance": {
+79 -31
View File
@@ -1,6 +1,6 @@
{
"version": 1,
"source_hash": "sha256:7111ced2e1cd0764338aacce792d95603734177a82deea4a4642626691940485",
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"source_files": 160,
"hint_files": 54,
@@ -36,8 +36,8 @@
"tools_with_avoid_when": 875,
"tools_with_examples": 875,
"tools_with_interface_mode": 875,
"unmatched_skill_tools": 96,
"unreviewed_skill_tools": 11
"unmatched_skill_tools": 97,
"unreviewed_skill_tools": 12
},
"source_products": [
"agoal",
@@ -1301,6 +1301,30 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc drive upload",
"source": "skills/mono/references/products/doc.md",
"line": 668,
"candidates": [
"doc upload",
"drive upload",
"doc +comment-create"
]
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
"line": 669,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
@@ -1315,6 +1339,30 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc drive upload",
"source": "skills/mono/references/products/doc.md",
"line": 745,
"candidates": [
"doc upload",
"drive upload",
"doc +comment-create"
]
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
"line": 746,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc.md",
@@ -1357,34 +1405,6 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 12,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 13,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
@@ -1413,6 +1433,34 @@
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 16,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "doc import",
"source": "skills/mono/references/products/doc/doc-import.md",
"line": 17,
"candidates": [
"doc import get",
"doc +comment-create",
"doc +comment-list"
],
"review": {
"status": "stale",
"reason": "旧版 Skill 或说明性引用,当前公开命令面无等价 leaf,禁止词法模糊映射"
}
},
{
"tool_path": "event consume user_im_message_receive_o2o_all",
"source": "skills/mono/references/products/event.md",
+121 -109
View File
@@ -1,18 +1,18 @@
{
"version": 1,
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"source_hash": "sha256:a862c8d93ff1e66de35a0148aeea6e269cf19587b12bebfb53f4fff8a4a71cc6",
"source_hash": "sha256:807d11cf6c62a0d4aaca4b5a6b438e6654b2792d3a32b4e271032202d45ff8fa",
"catalog": {
"agent_metadata": {
"products_with_metadata": 26,
"source": "embedded-skill-metadata",
"source_hash": "sha256:7111ced2e1cd0764338aacce792d95603734177a82deea4a4642626691940485",
"source_hash": "sha256:35727a6c324fbffde5271e96d972843abdf780391151ba858f6d04384268cd51",
"surface_hash": "sha256:4897d582a472d9d3596f88fb4546e7795d6b9bc334d020712f5d895ce894f7a7",
"surface_products": 26,
"surface_tools": 875,
"tools_with_agent_summary": 875,
"tools_with_metadata": 875,
"unmatched_skill_tools": 96,
"unmatched_skill_tools": 97,
"version": 1
},
"count": 26,
@@ -225,12 +225,13 @@
"internal/cli/schema_hints/selection/aitable.json",
"skills/mono/SKILL.md",
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/aitable.md"
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
],
"agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary_source": "dws-agent-selection/aitable",
"avoid_when": [
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
],
"description": "AI 表格操作",
"field_provenance": {
@@ -240,13 +241,13 @@
"precedence": "reviewed_explicit",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。"
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。"
"value": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。"
},
"avoid_when": {
"candidates": [
@@ -255,7 +256,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
]
}
],
@@ -263,7 +264,7 @@
"resolution": "highest_precedence",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
]
},
"use_when": {
@@ -273,7 +274,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
]
}
],
@@ -281,7 +282,7 @@
"resolution": "highest_precedence",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
]
}
},
@@ -726,11 +727,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
],
"canonical_path": "aitable.base_search",
"cli_name": "search",
@@ -752,7 +753,7 @@
"risk": "low",
"title": "搜索 AI 表格",
"use_when": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
]
},
{
@@ -1375,11 +1376,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "获取字段完整配置。",
"agent_summary": "底层获取字段完整类型与 config。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
],
"canonical_path": "aitable.field_get",
"cli_name": "get",
@@ -1401,7 +1402,7 @@
"risk": "low",
"title": "获取字段详情",
"use_when": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
]
},
{
@@ -1948,14 +1949,14 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"agent_summary_source": "dws-agent-selection/aitable",
"aliases": [
"aitable record list"
],
"availability": "available",
"avoid_when": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"canonical_path": "aitable.query_records",
"cli_name": "query",
@@ -1977,7 +1978,7 @@
"risk": "low",
"title": "获取行记录",
"use_when": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
]
},
{
@@ -2009,11 +2010,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
],
"canonical_path": "aitable.record_create",
"cli_name": "create",
@@ -2035,7 +2036,7 @@
"risk": "medium",
"title": "新增记录",
"use_when": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
]
},
{
@@ -2257,11 +2258,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"canonical_path": "aitable.record_update",
"cli_name": "update",
@@ -2283,7 +2284,7 @@
"risk": "medium",
"title": "更新记录",
"use_when": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
]
},
{
@@ -2596,7 +2597,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
],
"canonical_path": "aitable.shortcut_base_list",
"cli_name": "+base-list",
@@ -2622,7 +2623,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
],
"canonical_path": "aitable.shortcut_base_search",
"cli_name": "+base-search",
@@ -2752,7 +2753,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
],
"canonical_path": "aitable.shortcut_field_get",
"cli_name": "+field-get",
@@ -2934,7 +2935,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
],
"canonical_path": "aitable.shortcut_record_query",
"cli_name": "+record-query",
@@ -3038,7 +3039,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
],
"canonical_path": "aitable.shortcut_resolve_base",
"cli_name": "+resolve-base",
@@ -3055,7 +3056,7 @@
"risk": "low",
"title": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)",
"use_when": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
]
},
{
@@ -3064,7 +3065,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
],
"canonical_path": "aitable.shortcut_resolve_table",
"cli_name": "+resolve-table",
@@ -3081,7 +3082,7 @@
"risk": "low",
"title": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
"use_when": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
]
},
{
@@ -3168,7 +3169,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
],
"canonical_path": "aitable.shortcut_table_get",
"cli_name": "+table-get",
@@ -3382,11 +3383,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "获取数据表结构(字段+视图目录)。",
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
],
"canonical_path": "aitable.table_get",
"cli_name": "get",
@@ -3408,7 +3409,7 @@
"risk": "low",
"title": "获取数据表",
"use_when": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
]
},
{
@@ -4571,7 +4572,7 @@
}
],
"use_when": [
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
]
},
{
@@ -7784,11 +7785,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "给指定消息添加表情回应",
"agent_summary": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
],
"canonical_path": "chat.add_emoji_reaction",
"cli_name": "add-emoji",
@@ -7810,7 +7811,7 @@
"risk": "medium",
"title": "对消息添加 emoji 表情回应",
"use_when": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
]
},
{
@@ -7967,7 +7968,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
],
"canonical_path": "chat.batch_query_group_chat_settings",
"cli_name": "query",
@@ -7985,7 +7986,7 @@
"risk": "low",
"title": "批量查询当前用户的群会话设置",
"use_when": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
]
},
{
@@ -7994,7 +7995,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
],
"canonical_path": "chat.batch_update_group_chat_settings",
"cli_name": "set",
@@ -8012,7 +8013,7 @@
"risk": "medium",
"title": "批量更新当前用户的群会话设置",
"use_when": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
]
},
{
@@ -9430,11 +9431,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "查询异步消息发送任务的状态",
"agent_summary": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
],
"canonical_path": "chat.query_message_send_status",
"cli_name": "query-send-status",
@@ -9456,7 +9457,7 @@
"risk": "low",
"title": "查询消息发送状态",
"use_when": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
]
},
{
@@ -9672,11 +9673,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "移除指定消息上的表情回应",
"agent_summary": "从同一条真实消息移除此前添加的同名表情回应",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
],
"canonical_path": "chat.remove_emoji_reaction",
"cli_name": "remove-emoji",
@@ -9698,7 +9699,7 @@
"risk": "medium",
"title": "移除消息的 emoji 表情回应",
"use_when": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
]
},
{
@@ -9707,7 +9708,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
],
"canonical_path": "chat.remove_group_member",
"cli_name": "remove",
@@ -9729,7 +9730,7 @@
"risk": "medium",
"title": "移除群成员",
"use_when": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
]
},
{
@@ -10342,11 +10343,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "把指定消息设为会话置顶消息",
"agent_summary": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
],
"canonical_path": "chat.set_pin_message",
"cli_name": "set-pin-msg",
@@ -10368,7 +10369,7 @@
"risk": "medium",
"title": "钉住消息(Pin)",
"use_when": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
]
},
{
@@ -11393,11 +11394,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "查询消息发送状态",
"agent_summary": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
],
"canonical_path": "chat.shortcut_messages_query_send_status",
"cli_name": "+messages-query-send-status",
@@ -11414,7 +11415,7 @@
"risk": "low",
"title": "查询消息发送状态",
"use_when": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
]
},
{
@@ -11579,8 +11580,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
],
"canonical_path": "chat.shortcut_search_msg",
"cli_name": "+search-msg",
@@ -11769,11 +11769,11 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "取消指定消息的会话置顶",
"agent_summary": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
],
"canonical_path": "chat.unset_pin_message",
"cli_name": "unset-pin-msg",
@@ -11795,7 +11795,7 @@
"risk": "medium",
"title": "取消钉住消息(Unpin)",
"use_when": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
]
},
{
@@ -11919,7 +11919,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
],
"canonical_path": "chat.update_group_icon",
"cli_name": "update-icon",
@@ -11941,7 +11941,7 @@
"risk": "medium",
"title": "更新群头像",
"use_when": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
]
},
{
@@ -12008,7 +12008,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
],
"canonical_path": "chat.update_group_settings",
"cli_name": "update-settings",
@@ -12030,7 +12030,7 @@
"risk": "medium",
"title": "更新群设置",
"use_when": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
]
},
{
@@ -15378,13 +15378,13 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "创建一篇新的在线文档",
"agent_summary": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc",
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type \u003ctype\u003e(勿用 doc create)",
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert"
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"canonical_path": "doc.create_document",
"cli_name": "create",
@@ -15405,8 +15405,8 @@
"risk": "medium",
"title": "创建文档",
"use_when": [
"用户要新建一篇文字在线文档(adoc),可空文档或带初始 Markdown 时",
"创建到指定文件夹 --folder、知识库根 --workspace,或默认「我的文档」根目录时"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
]
},
{
@@ -15537,7 +15537,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -15601,7 +15601,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"canonical_path": "doc.download_doc_attachment",
"cli_name": "download",
@@ -15623,7 +15624,7 @@
"risk": "low",
"title": "下载文档附件",
"use_when": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
]
},
{
@@ -15666,7 +15667,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"非 adoc(表格/多维表/普通文件)不要用本命令;先 doc info 再路由",
"原始 alidocs URL 类型未知或目标不是 adoc 时先用 drive info 探测并路由;表格/多维表/普通文件不要用本命令",
"要元信息用 doc info;要块结构用 doc block list",
"Markdown 为有损投影:保形复制模板请用 doc copy,不要 read→create"
],
@@ -15689,20 +15690,21 @@
"risk": "low",
"title": "读取文档内容 (Markdown)",
"use_when": [
"用户要读取钉钉在线文字文档(adoc)正文(Markdown)时",
"用户直接粘贴文档 URL 且无其他指令时(默认读内容)",
"已由 drive info 确认 extension=adoc,用户要读取正文(Markdown)时",
"用户提供已知 adoc nodeId/URL,且要读取内容或抽取指定章节时",
"只需标题大纲、指定块区间/单块或特定 JSONML tags 时使用 --content-format jsonml 与 --scope"
]
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等)",
"agent_summary": "在已确认是 ALIDOC 后读取文档专属元信息",
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"已确认是 adoc 且只要正文改用 dws doc read",
"原始 alidocs URL 的类型探测、extension 路由或可靠 fileSize 使用 dws drive info;不要先猜是文档",
"已确认是 adoc 且只要正文时改用 dws doc read",
"只要目录列表改用 dws drive list / wiki node list",
"需要可靠文件大小 fileSize 时改用 dws drive info;文档元信息接口可能不返回大小"
"普通文件、电子表格或 AI 表格不使用本命令"
],
"canonical_path": "doc.get_document_info",
"cli_name": "info",
@@ -15723,8 +15725,8 @@
"risk": "low",
"title": "获取文档元信息",
"use_when": [
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
"drive info 已确认是 ALIDOC,用户还要标题、创建者、权限或 docUrl 等文档专属元信息时",
"创建响应只有 nodeId、缺少 docUrl,需要补查文档链接时"
]
},
{
@@ -15760,7 +15762,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"canonical_path": "doc.import_get",
"cli_name": "get",
@@ -15781,7 +15783,7 @@
"risk": "medium",
"title": "查询导入任务结果(手动兜底)",
"use_when": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
]
},
{
@@ -15790,8 +15792,9 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"canonical_path": "doc.insert_document_block",
@@ -15814,7 +15817,9 @@
"risk": "medium",
"title": "插入块元素",
"use_when": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
]
},
{
@@ -15877,7 +15882,7 @@
"risk": "low",
"title": "查询块元素",
"use_when": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
]
},
{
@@ -16106,7 +16111,8 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"canonical_path": "doc.reply_comment",
"cli_name": "reply",
@@ -16128,7 +16134,7 @@
"risk": "medium",
"title": "回复评论",
"use_when": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
]
},
{
@@ -16245,7 +16251,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"常规文件管理优先 drive copy;要搬走原件用 move;复制后必须从真实返回取副本 nodeId,禁止继续编辑源文档"
],
"canonical_path": "doc.shortcut_copy",
"cli_name": "+copy",
@@ -16271,7 +16277,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"长、多行、表格或文件内容使用 doc update --mode append --content-file;指定位置或富结构使用 block insert;不要跳过 user_required 确认"
],
"canonical_path": "doc.shortcut_doc_append",
"cli_name": "+doc-append",
@@ -16323,7 +16329,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"用户要直接拿到本地 docx/markdown/pdf 文件时使用一体化 dws doc export;不要默认手工编排 submit/get 轮询"
],
"canonical_path": "doc.shortcut_export_submit",
"cli_name": "+export-submit",
@@ -16349,7 +16355,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要最近访问、extension/时间/创建者等组合过滤时使用 +search;目标已给 nodeId/URL 时直接进入 info/read,不再搜索"
],
"canonical_path": "doc.shortcut_find_doc",
"cli_name": "+find-doc",
@@ -16375,7 +16381,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"全局或钉盘目录浏览优先 drive list,知识库节点树优先 wiki node list;本 Shortcut 只列已知 doc folder/workspace 的直接子节点"
],
"canonical_path": "doc.shortcut_list",
"cli_name": "+list",
@@ -16401,7 +16407,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"常规文件管理优先 drive move;要保留原位置副本用 copy;目标位置不明确或未确认时不要移动"
],
"canonical_path": "doc.shortcut_move",
"cli_name": "+move",
@@ -16427,7 +16433,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只有一个关键词且只要紧凑标题/URL/type/token 投影时优先 +find-doc;目标已给 nodeId/URL 时不要再搜索"
],
"canonical_path": "doc.shortcut_search",
"cli_name": "+search",
@@ -16453,7 +16459,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"同名人员未消歧、缺少真实文档 URL 或用户未确认时不要发送;本命令不授予文档权限,授权应走 drive permission"
],
"canonical_path": "doc.shortcut_share_doc",
"cli_name": "+share-doc",
@@ -16557,7 +16563,7 @@
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只查看历史用 +version-list,只保存当前快照用 +version-save;版本号未核实、用户未确认或只需改单块时不要回滚"
],
"canonical_path": "doc.shortcut_version_revert",
"cli_name": "+version-revert",
@@ -16821,12 +16827,13 @@
},
{
"agent_metadata_source": "embedded-skill-metadata",
"agent_summary": "更新文档内容(追加 / 覆盖;覆盖需 --yes)",
"agent_summary": "用原生自动分片管道追加或整篇覆盖 adoc 内容",
"agent_summary_source": "dws-agent-selection/doc",
"availability": "available",
"avoid_when": [
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"canonical_path": "doc.update_document",
@@ -16848,9 +16855,10 @@
"risk": "medium",
"title": "更新文档内容",
"use_when": [
"用户要向已有 adoc 追加内容时用 --mode append(更安全)",
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
]
},
{
@@ -16860,7 +16868,8 @@
"availability": "available",
"avoid_when": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"canonical_path": "doc.update_document_block",
"cli_name": "update",
@@ -16882,7 +16891,7 @@
"risk": "medium",
"title": "更新块元素",
"use_when": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
]
},
{
@@ -18284,8 +18293,10 @@
"agent_summary_source": "dws-agent-selection/drive",
"availability": "available",
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -18308,7 +18319,8 @@
"title": "上传本地文件到钉盘或文档空间",
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
+99 -87
View File
@@ -8704,13 +8704,15 @@
"internal/cli/schema_hints/metadata/aitable.json",
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.base_search",
"skills/mono/references/products/aitable.md"
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
],
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
],
"canonical_path": "aitable.base_search",
"cli_name": "search",
@@ -8731,14 +8733,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。"
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。"
"value": "底层按名称搜索 AI 表格 Base,返回原始候选列表。"
},
"availability": {
"candidates": [
@@ -8770,7 +8772,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
]
}
],
@@ -8779,7 +8781,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
]
},
"canonical_path": {
@@ -9009,7 +9011,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
]
}
],
@@ -9018,7 +9020,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
]
}
},
@@ -9237,7 +9239,7 @@
"source": "reviewed_command_registry",
"title": "搜索 AI 表格",
"use_when": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
]
},
"aitable.base_update": {
@@ -23545,13 +23547,15 @@
"internal/cli/schema_hints/selection/aitable.json",
"internal/cli/schema_mcp_metadata.json#tools.aitable.field_get",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-field.md"
"skills/mono/references/products/aitable/aitable-field.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md"
],
"agent_summary": "获取字段完整配置。",
"agent_summary": "底层获取字段完整类型与 config。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
],
"canonical_path": "aitable.field_get",
"cli_name": "get",
@@ -23572,14 +23576,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取字段完整配置。"
"value": "底层获取字段完整类型与 config。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取字段完整配置。"
"value": "底层获取字段完整类型与 config。"
},
"availability": {
"candidates": [
@@ -23611,7 +23615,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
]
}
],
@@ -23620,7 +23624,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
]
},
"canonical_path": {
@@ -23848,7 +23852,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
]
}
],
@@ -23857,7 +23861,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
]
}
},
@@ -24201,7 +24205,7 @@
"source": "reviewed_command_registry",
"title": "获取字段详情",
"use_when": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
]
},
"aitable.field_list": {
@@ -38119,16 +38123,18 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.query_records",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-record-query.md"
"skills/mono/references/products/aitable/aitable-record-query.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md"
],
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"agent_summary_source": "dws-agent-selection/aitable",
"aliases": [
"aitable record list"
],
"availability": "available",
"avoid_when": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"canonical_path": "aitable.query_records",
"cli_name": "query",
@@ -38149,14 +38155,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。"
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。"
"value": "底层查询/搜索记录,额外支持原子命令的完整分页控制。"
},
"availability": {
"candidates": [
@@ -38188,7 +38194,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
]
}
],
@@ -38197,7 +38203,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
]
},
"canonical_path": {
@@ -38447,7 +38453,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
]
}
],
@@ -38456,7 +38462,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
]
}
},
@@ -39699,7 +39705,7 @@
"source": "reviewed_command_registry",
"title": "获取行记录",
"use_when": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
]
},
"aitable.record_batch_update": {
@@ -40397,13 +40403,15 @@
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/mono/references/products/aitable/aitable-record-create.md"
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md"
],
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
],
"canonical_path": "aitable.record_create",
"cli_name": "create",
@@ -40424,14 +40432,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "新增记录(cells 的 key 必须是 fieldId)。"
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "新增记录(cells 的 key 必须是 fieldId)。"
"value": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。"
},
"availability": {
"candidates": [
@@ -40463,7 +40471,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
]
}
],
@@ -40472,7 +40480,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
]
},
"canonical_path": {
@@ -40700,7 +40708,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
]
}
],
@@ -40709,7 +40717,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
]
}
},
@@ -41156,7 +41164,7 @@
"source": "reviewed_command_registry",
"title": "新增记录",
"use_when": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
]
},
"aitable.record_delete": {
@@ -46549,13 +46557,15 @@
"internal/cli/schema_mcp_metadata.json#tools.aitable.record_update",
"skills/mono/references/products/aitable-record-ops.md",
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-attachment.md"
"skills/mono/references/products/aitable/aitable-attachment.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md"
],
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"canonical_path": "aitable.record_update",
"cli_name": "update",
@@ -46576,14 +46586,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。"
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。"
"value": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。"
},
"availability": {
"candidates": [
@@ -46615,7 +46625,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
]
}
],
@@ -46624,7 +46634,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
]
},
"canonical_path": {
@@ -46852,7 +46862,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
]
}
],
@@ -46861,7 +46871,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
]
}
},
@@ -47308,7 +47318,7 @@
"source": "reviewed_command_registry",
"title": "更新记录",
"use_when": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
]
},
"aitable.record_upsert": {
@@ -53447,7 +53457,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
],
"canonical_path": "aitable.shortcut_base_list",
"cli_name": "+base-list",
@@ -53502,7 +53512,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
]
}
],
@@ -53511,7 +53521,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
]
},
"canonical_path": {
@@ -53943,7 +53953,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
],
"canonical_path": "aitable.shortcut_base_search",
"cli_name": "+base-search",
@@ -53997,7 +54007,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
]
}
],
@@ -54006,7 +54016,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
]
},
"canonical_path": {
@@ -56196,7 +56206,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
],
"canonical_path": "aitable.shortcut_field_get",
"cli_name": "+field-get",
@@ -56250,7 +56260,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
]
}
],
@@ -56259,7 +56269,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
]
},
"canonical_path": {
@@ -60316,7 +60326,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
],
"canonical_path": "aitable.shortcut_record_query",
"cli_name": "+record-query",
@@ -60370,7 +60380,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
]
}
],
@@ -60379,7 +60389,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
]
},
"canonical_path": {
@@ -63485,7 +63495,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
],
"canonical_path": "aitable.shortcut_resolve_base",
"cli_name": "+resolve-base",
@@ -63539,7 +63549,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
]
}
],
@@ -63548,7 +63558,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
]
},
"canonical_path": {
@@ -63758,7 +63768,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
]
}
],
@@ -63767,7 +63777,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
]
}
},
@@ -63884,7 +63894,7 @@
"source": "reviewed_command_registry",
"title": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)",
"use_when": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
]
},
"aitable.shortcut_resolve_table": {
@@ -63900,7 +63910,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
],
"canonical_path": "aitable.shortcut_resolve_table",
"cli_name": "+resolve-table",
@@ -63954,7 +63964,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
]
}
],
@@ -63963,7 +63973,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
]
},
"canonical_path": {
@@ -64173,7 +64183,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
]
}
],
@@ -64182,7 +64192,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
]
}
},
@@ -64395,7 +64405,7 @@
"source": "reviewed_command_registry",
"title": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
"use_when": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
]
},
"aitable.shortcut_role_list": {
@@ -65656,7 +65666,7 @@
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
],
"canonical_path": "aitable.shortcut_table_get",
"cli_name": "+table-get",
@@ -65711,7 +65721,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
]
}
],
@@ -65720,7 +65730,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
]
},
"canonical_path": {
@@ -70522,13 +70532,15 @@
"skills/mono/references/products/aitable.md",
"skills/mono/references/products/aitable/aitable-advperm.md",
"skills/mono/references/products/aitable/aitable-primary-doc.md",
"skills/mono/references/products/aitable/aitable-record-create.md"
"skills/mono/references/products/aitable/aitable-record-create.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md"
],
"agent_summary": "获取数据表结构(字段+视图目录)。",
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
"agent_summary_source": "dws-agent-selection/aitable",
"availability": "available",
"avoid_when": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
],
"canonical_path": "aitable.table_get",
"cli_name": "get",
@@ -70549,14 +70561,14 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取数据表结构(字段+视图目录)。"
"value": "底层获取数据表结构、精简字段目录与视图目录。"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": "获取数据表结构(字段+视图目录)。"
"value": "底层获取数据表结构、精简字段目录与视图目录。"
},
"availability": {
"candidates": [
@@ -70588,7 +70600,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
]
}
],
@@ -70597,7 +70609,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
]
},
"canonical_path": {
@@ -70825,7 +70837,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
]
}
],
@@ -70834,7 +70846,7 @@
"review_reason": "人工结合实时 dws schema MCP 描述(或 helper 无 live 时用 Skill/Cobra)、兄弟命令分流与 Runtime 确认门禁审阅选型语义。",
"source": "internal/cli/schema_hints/selection/aitable.json",
"value": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
]
}
},
@@ -71081,7 +71093,7 @@
"source": "reviewed_command_registry",
"title": "获取数据表",
"use_when": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
]
},
"aitable.table_list": {
+87 -90
View File
@@ -1158,11 +1158,11 @@
"skills/mono/SKILL.md",
"skills/mono/references/products/chat.md"
],
"agent_summary": "给指定消息添加表情回应",
"agent_summary": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
],
"canonical_path": "chat.add_emoji_reaction",
"cli_name": "add-emoji",
@@ -1193,14 +1193,14 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "给指定消息添加表情回应"
"value": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "给指定消息添加表情回应"
"value": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应"
},
"availability": {
"candidates": [
@@ -1232,7 +1232,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
]
}
],
@@ -1241,7 +1241,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
]
},
"canonical_path": {
@@ -1479,7 +1479,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
]
}
],
@@ -1488,7 +1488,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
]
}
},
@@ -2220,7 +2220,7 @@
"source": "reviewed_command_registry",
"title": "对消息添加 emoji 表情回应",
"use_when": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
]
},
"chat.add_group_member": {
@@ -6372,7 +6372,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
],
"canonical_path": "chat.batch_query_group_chat_settings",
"cli_name": "query",
@@ -6426,7 +6426,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
]
}
],
@@ -6435,7 +6435,7 @@
"review_reason": "人工审阅:对照悟空 develop 同名命令实现、Cobra Long/Example 与 Runtime 确认门禁移植;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
]
},
"canonical_path": {
@@ -6645,7 +6645,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
]
}
],
@@ -6654,7 +6654,7 @@
"review_reason": "人工审阅:对照悟空 develop 同名命令实现、Cobra Long/Example 与 Runtime 确认门禁移植;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
]
}
},
@@ -6773,7 +6773,7 @@
"source": "reviewed_command_registry",
"title": "批量查询当前用户的群会话设置",
"use_when": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
]
},
"chat.batch_update_group_chat_settings": {
@@ -6789,7 +6789,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
],
"canonical_path": "chat.batch_update_group_chat_settings",
"cli_name": "set",
@@ -6843,7 +6843,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
]
}
],
@@ -6852,7 +6852,7 @@
"review_reason": "人工审阅:对照悟空 develop 同名命令实现、Cobra Long/Example 与 Runtime 确认门禁移植;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
]
},
"canonical_path": {
@@ -7062,7 +7062,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
]
}
],
@@ -7071,7 +7071,7 @@
"review_reason": "人工审阅:对照悟空 develop 同名命令实现、Cobra Long/Example 与 Runtime 确认门禁移植;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
]
}
},
@@ -7190,7 +7190,7 @@
"source": "reviewed_command_registry",
"title": "批量更新当前用户的群会话设置",
"use_when": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
]
},
"chat.clear_all_red_point": {
@@ -38313,11 +38313,11 @@
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/chat.md"
],
"agent_summary": "查询异步消息发送任务的状态",
"agent_summary": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
],
"canonical_path": "chat.query_message_send_status",
"cli_name": "query-send-status",
@@ -38338,14 +38338,14 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "查询异步消息发送任务的状态"
"value": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "查询异步消息发送任务的状态"
"value": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功"
},
"availability": {
"candidates": [
@@ -38377,7 +38377,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
]
}
],
@@ -38386,7 +38386,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
]
},
"canonical_path": {
@@ -38608,7 +38608,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
]
}
],
@@ -38617,7 +38617,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
]
}
},
@@ -38767,7 +38767,7 @@
"source": "reviewed_command_registry",
"title": "查询消息发送状态",
"use_when": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
]
},
"chat.query_msg_read_status": {
@@ -43352,11 +43352,11 @@
"skills/mono/SKILL.md",
"skills/mono/references/products/chat.md"
],
"agent_summary": "移除指定消息上的表情回应",
"agent_summary": "从同一条真实消息移除此前添加的同名表情回应",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
],
"canonical_path": "chat.remove_emoji_reaction",
"cli_name": "remove-emoji",
@@ -43387,14 +43387,14 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "移除指定消息上的表情回应"
"value": "从同一条真实消息移除此前添加的同名表情回应"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "移除指定消息上的表情回应"
"value": "从同一条真实消息移除此前添加的同名表情回应"
},
"availability": {
"candidates": [
@@ -43426,7 +43426,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
]
}
],
@@ -43435,7 +43435,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
]
},
"canonical_path": {
@@ -43673,7 +43673,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
]
}
],
@@ -43682,7 +43682,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
]
}
},
@@ -44414,7 +44414,7 @@
"source": "reviewed_command_registry",
"title": "移除消息的 emoji 表情回应",
"use_when": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
]
},
"chat.remove_group_member": {
@@ -44435,7 +44435,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
],
"canonical_path": "chat.remove_group_member",
"cli_name": "remove",
@@ -44495,7 +44495,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
]
}
],
@@ -44504,7 +44504,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
]
},
"canonical_path": {
@@ -44759,7 +44759,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
]
}
],
@@ -44768,7 +44768,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
]
}
},
@@ -45075,7 +45075,7 @@
"source": "reviewed_command_registry",
"title": "移除群成员",
"use_when": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
]
},
"chat.remove_message_favorite": {
@@ -65014,11 +65014,11 @@
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/chat.md"
],
"agent_summary": "把指定消息设为会话置顶消息",
"agent_summary": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
],
"canonical_path": "chat.set_pin_message",
"cli_name": "set-pin-msg",
@@ -65039,14 +65039,14 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "把指定消息设为会话置顶消息"
"value": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "把指定消息设为会话置顶消息"
"value": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin"
},
"availability": {
"candidates": [
@@ -65078,7 +65078,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
]
}
],
@@ -65087,7 +65087,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
]
},
"canonical_path": {
@@ -65309,7 +65309,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
]
}
],
@@ -65318,7 +65318,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
]
}
},
@@ -65598,7 +65598,7 @@
"source": "reviewed_command_registry",
"title": "钉住消息(Pin)",
"use_when": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
]
},
"chat.set_top_conversation": {
@@ -87865,11 +87865,11 @@
"internal/cli/schema_hints/metadata/chat.json",
"internal/cli/schema_hints/selection/chat.json"
],
"agent_summary": "查询消息发送状态",
"agent_summary": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
],
"canonical_path": "chat.shortcut_messages_query_send_status",
"cli_name": "+messages-query-send-status",
@@ -87890,14 +87890,14 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "查询消息发送状态"
"value": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "查询消息发送状态"
"value": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功"
},
"availability": {
"candidates": [
@@ -87923,7 +87923,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
]
}
],
@@ -87932,7 +87932,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
]
},
"canonical_path": {
@@ -88142,7 +88142,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
]
}
],
@@ -88151,7 +88151,7 @@
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
]
}
},
@@ -88268,7 +88268,7 @@
"source": "reviewed_command_registry",
"title": "查询消息发送状态",
"use_when": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
]
},
"chat.shortcut_messages_read_status": {
@@ -94279,8 +94279,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
],
"canonical_path": "chat.shortcut_search_msg",
"cli_name": "+search-msg",
@@ -94380,8 +94379,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
]
}
],
@@ -94390,8 +94388,7 @@
"review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
]
},
"canonical_path": {
@@ -101130,11 +101127,11 @@
"skills/mono/references/intent-guide.md",
"skills/mono/references/products/chat.md"
],
"agent_summary": "取消指定消息的会话置顶",
"agent_summary": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
],
"canonical_path": "chat.unset_pin_message",
"cli_name": "unset-pin-msg",
@@ -101155,14 +101152,14 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "取消指定消息的会话置顶"
"value": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin"
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": "取消指定消息的会话置顶"
"value": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin"
},
"availability": {
"candidates": [
@@ -101194,7 +101191,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
]
}
],
@@ -101203,7 +101200,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
]
},
"canonical_path": {
@@ -101425,7 +101422,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
]
}
],
@@ -101434,7 +101431,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
]
}
},
@@ -101714,7 +101711,7 @@
"source": "reviewed_command_registry",
"title": "取消钉住消息(Unpin)",
"use_when": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
]
},
"chat.unset_top_message": {
@@ -104381,7 +104378,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
],
"canonical_path": "chat.update_group_icon",
"cli_name": "update-icon",
@@ -104441,7 +104438,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
]
}
],
@@ -104450,7 +104447,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
]
},
"canonical_path": {
@@ -104672,7 +104669,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
]
}
],
@@ -104681,7 +104678,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
]
}
},
@@ -105003,7 +105000,7 @@
"source": "reviewed_command_registry",
"title": "更新群头像",
"use_when": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
]
},
"chat.update_group_name": {
@@ -106220,7 +106217,7 @@
"agent_summary_source": "dws-agent-selection/chat",
"availability": "available",
"avoid_when": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
],
"canonical_path": "chat.update_group_settings",
"cli_name": "update-settings",
@@ -106280,7 +106277,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
]
}
],
@@ -106289,7 +106286,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
]
},
"canonical_path": {
@@ -106511,7 +106508,7 @@
"selected": true,
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
]
}
],
@@ -106520,7 +106517,7 @@
"review_reason": "AI 生成并复核的 Agent 选择语义,依据精确 CommandRegistry identity、真实 Cobra help 与产品参考,不改写执行、安全或接口事实。",
"source": "internal/cli/schema_hints/selection/chat.json",
"value": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
]
}
},
@@ -106924,7 +106921,7 @@
"source": "reviewed_command_registry",
"title": "更新群设置",
"use_when": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
]
},
"chat.update_notification_off": {
File diff suppressed because it is too large Load Diff
+21 -12
View File
@@ -27242,8 +27242,10 @@
"agent_summary_source": "dws-agent-selection/drive",
"availability": "available",
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -27267,7 +27269,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
@@ -27275,7 +27277,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件"
},
@@ -27299,12 +27301,14 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
]
@@ -27312,11 +27316,13 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
]
@@ -27429,7 +27435,7 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
@@ -27440,7 +27446,7 @@
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"dws drive upload --file ./report.pdf --format json",
@@ -27528,7 +27534,7 @@
},
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": false,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": true
@@ -27580,23 +27586,25 @@
"candidates": [
{
"precedence": "reviewed_explicit",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"selected": true,
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
],
"precedence": "reviewed_explicit",
"resolution": "highest_precedence",
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source": "internal/cli/schema_hints/selection/drive.json",
"value": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
@@ -28319,7 +28327,8 @@
"title": "上传本地文件到钉盘或文档空间",
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
]
}
@@ -289,12 +289,12 @@
]
},
"aitable.base_search": {
"agent_summary": "按名称搜索 AI 表格 Base(优先于仅最近访问的 list)。",
"agent_summary": "底层按名称搜索 AI 表格 Base,返回原始候选列表。",
"use_when": [
"用户要找某个 AI 表格/多维表,按名称检索时优先使用"
"需要 Shortcut 未公开的底层参数、原始 search_bases 响应或自定义候选处理时"
],
"avoid_when": [
"只要最近访问列表用 base list;已知 baseId 取详情用 base get;电子表格 axls 用 sheet"
"普通按名称解析唯一 baseId 优先用 +resolve-base;只要候选列表用 +base-search;最近访问用 +base-list;电子表格 axls 用 sheet"
],
"examples": [
"dws aitable base search --query \"项目\""
@@ -304,7 +304,8 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable base search --help",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md",
"dws-schema-live:aitable.search_bases"
]
},
@@ -710,12 +711,12 @@
]
},
"aitable.field_get": {
"agent_summary": "获取字段完整配置。",
"agent_summary": "底层获取字段完整类型与 config。",
"use_when": [
"需要字段类型/config(选项、公式等)详情时"
"需要 +field-get 未公开的底层参数、原始响应或不同执行语义时"
],
"avoid_when": [
"表级字段目录也可用 table get;创建字段用 field create;不可用本命令改类型"
"正常按字段 ID 展开类型/config 优先用 +field-get;精简字段目录用 +table-get;创建字段用 field create"
],
"examples": [
"dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID>"
@@ -725,7 +726,8 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable field get --help",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-field.md",
"dws-schema-live:aitable.get_fields"
]
},
@@ -1110,12 +1112,12 @@
]
},
"aitable.query_records": {
"agent_summary": "查询/搜索记录(filters/sort/分页/--all;cells 键为 fieldId)。",
"agent_summary": "底层查询/搜索记录,额外支持原子命令的完整分页控制。",
"use_when": [
"查看、筛选、全文搜索或遍历记录时的主入口"
"需要 +record-query 未公开的 --all/--page-limit、底层原始响应或不同执行语义时"
],
"avoid_when": [
"已知 recordId 窄查可用 record get;空行用 query-empty;写入用 create/update;电子表格单元格用 sheet"
"普通按 ID、关键词、filters、sort 或 cursor 查询优先用 +record-query;空行用 +record-query-empty;写入用 create/update;电子表格单元格用 sheet"
],
"examples": [
"dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID>"
@@ -1125,7 +1127,8 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable record query --help",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-query.md",
"dws-schema-live:aitable.query_records"
]
},
@@ -1150,12 +1153,12 @@
]
},
"aitable.record_create": {
"agent_summary": "新增记录(cells 的 key 必须是 fieldId)。",
"agent_summary": "新增记录;使用 fieldId 写入并从 newRecordIds 回读验证。",
"use_when": [
"需要插入新行数据时"
"已取得 baseId/tableId 与字段完整配置,需要插入一条或多条新记录并回读时"
],
"avoid_when": [
"更新已有行用 record update;有则更无则增用 upsert;批量同 patch 用 batch-update"
"未核对字段类型时先 field get;更新已有行用 record update;有则更无则增用 upsert;本地 CSV/JSON 批量追加可用已评审脚本"
],
"examples": [
"dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> --records '[{\"cells\":{\"fldXXX\":\"值\"}}]'"
@@ -1165,7 +1168,8 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable record create --help",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-create.md",
"dws-schema-live:aitable.create_records"
]
},
@@ -1310,12 +1314,12 @@
]
},
"aitable.record_update": {
"agent_summary": "更新已有记录字段(先 query 拿 recordId;只传需改字段)。",
"agent_summary": "更新已有记录字段;先 query 拿 recordId,使用 fieldId 写入并回读。",
"use_when": [
"需要修改已有记录若干字段时"
"已查询得到真实 recordId 并核对字段类型,需要修改每条记录各自的 cells 时"
],
"avoid_when": [
"新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
"未定位 recordId 或字段配置时先 query/field get;新建用 create;多条共用同一 cells patch 用 batch-update;混合创建/更新用 upsert"
],
"examples": [
"dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> --records '[{\"recordId\":\"recXXX\",\"cells\":{\"fldYYY\":\"新值\"}}]'"
@@ -1325,7 +1329,8 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable record update --help",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable/aitable-record-update.md",
"dws-schema-live:aitable.update_records"
]
},
@@ -1550,12 +1555,12 @@
]
},
"aitable.table_get": {
"agent_summary": "获取数据表结构(字段+视图目录)。",
"agent_summary": "底层获取数据表结构、精简字段目录与视图目录。",
"use_when": [
"需要字段 fieldId 或视图目录以继续 record/field 操作时优先 table get"
"需要 +table-get 未公开的底层参数、原始 get_tables 响应或不同执行语义时"
],
"avoid_when": [
"只关心 Base 级 tables 列表可先 base get;字段完整配置也可用 field get"
"正常获取表、精简字段和视图目录优先用 +table-get;按表名解析 ID 用 +resolve-table;完整字段 config 用 +field-get"
],
"examples": [
"dws aitable table get --base-id BASE_ID"
@@ -1565,7 +1570,8 @@
"source_refs": [
"internal/cli/schema_command_registry.json",
"cobra-help:dws aitable table get --help",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md",
"dws-schema-live:aitable.get_tables"
]
},
@@ -2335,7 +2341,7 @@
"当你不知道具体 baseId、想先浏览自己最近用过或可访问的 AI 表格清单以便定位目标时使用;支持游标分页,返回 Base 列表及其 baseId。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"知道名称并要解析唯一 baseId 用 +resolve-base;按关键词要完整候选用 +base-search;不要把最近访问列表描述成全部 Base"
],
"examples": [
"dws aitable +base-list",
@@ -2355,7 +2361,7 @@
"当你知道某个 AI 表格的名字或部分关键词、想直接定位到它并拿到 baseId 时使用;输入名称关键词,返回匹配的 Base 列表。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需唯一 baseId 用 +resolve-base;浏览最近访问用 +base-list;需要未公开底层参数或原始响应才用 base search"
],
"examples": [
"dws aitable +base-search --query \"项目管理\""
@@ -2393,7 +2399,7 @@
"当你已进入某个 Base、需要了解其中某些数据表有哪些字段(拿 fieldId)、有哪些视图(拿 viewId)以便读写数据时使用;批量返回表信息、字段目录和视图目录。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只知道表名并要解析唯一 tableId 用 +resolve-table;需要字段完整 config 用 +field-get;需要未公开底层语义才用 table get"
],
"examples": [
"dws aitable +table-get --base-id BASE_ID",
@@ -2413,7 +2419,7 @@
"当你需要查看字段的完整类型配置(如单选选项、关联表设置、AI 配置)以便正确写入数据或改配置时使用;批量返回字段详情。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只需精简 fieldId/type 目录用 +table-get;创建或修改字段用 field create/update;需要未公开底层语义才用 field get"
],
"examples": [
"dws aitable +field-get --base-id B --table-id T"
@@ -2432,7 +2438,7 @@
"当你要读取表格里的行数据——按 recordId 精确取、按结构化条件筛选、按关键词全文搜索或分页遍历时使用;返回匹配记录及其单元格值。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"完全空行扫描用 +record-query-empty;变更历史用 +record-history-list;需要 --all/--page-limit 或未公开底层语义才用 record query"
],
"examples": [
"dws aitable +record-query --base-id B --table-id T --query \"关键词\" --limit 50"
@@ -2849,10 +2855,10 @@
"aitable.shortcut_resolve_base": {
"agent_summary": "按名称搜索多维表 Base 并解析出唯一 baseId(只读)",
"use_when": [
"当你只知道某个多维表 Base 的名称(或名称里的关键词)、想把它解析成可直接用于后续工具的 baseId 时使用;内部按 --name 关键词调用 search_bases 搜索 Base,再在本地投影出每个候选的 baseId 和 name。如果只命中一个 Base 就直接返回它的 baseId;如果命中多个则列出全部候选让你消歧,绝不替你瞎猜;如果一个都没命中则提示未找到。这是纯只读操作,只做搜索与本地投影,不会修改任何 Base。"
"只知道 Base 名称或关键词,需要解析成唯一 baseId 供后续 Table/Record 操作时"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;只要完整候选列表用 +base-search;浏览最近访问用 +base-list;已知 baseId 不再重复搜索"
],
"examples": [
"dws aitable +resolve-base --name 项目管理"
@@ -2868,10 +2874,10 @@
"aitable.shortcut_resolve_table": {
"agent_summary": "在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读)",
"use_when": [
"当你已经知道某个多维表 Base 的 baseId、又只记得里面某张数据表(table)的名称或名称关键词、想把它解析成可直接用于后续工具的 tableId 时使用;内部先用 get_tables(只传 baseId)列出该 Base 下的全部数据表,再在本地把每张表投影成 tableId、name,并按 --name 关键词做大小写不敏感的包含匹配来筛选候选。如果只命中一张表就直接返回它的 tableId;如果命中多张则列出全部候选让你消歧,绝不替你瞎猜;如果一张都没命中则提示未找到。这是纯只读操作,只做列举、本地匹配与投影,不会创建、修改或删除任何数据表。"
"已有真实 baseId、只知道数据表名称或关键词,需要解析成唯一 tableId 时"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"多候选时必须让用户消歧;需要所有表及字段/视图目录用 +table-get;已知 tableId 不再重复解析"
],
"examples": [
"dws aitable +resolve-table --base B --name 任务"
@@ -2887,18 +2893,19 @@
},
"products": {
"aitable": {
"agent_summary": "管理 AI 表格 Base、数据表、字段、记录、视图、表单、仪表盘、权限、导入导出与自动化工作流。",
"agent_summary": "管理 AI 表格的 Base、Table、Field、Record、视图表单、仪表盘、权限、导入导出与自动化工作流。",
"use_when": [
"需要读取或管理 AI 表格中的结构、数据、视图、权限、导入导出或工作流时"
"目标是 AI 表格/多维表中的结构化 Base、数据表、字段、记录、视图、权限、文件导入导出或自动化工作流时"
],
"avoid_when": [
"目标是在线电子表格单元格读写时用 sheet;普通文档用 doc"
"在线电子表格工作表、单元格或公式用 sheet;普通文档用 doc;钉盘普通文件用 drive;类型不明的 alidocs URL 先做类型预检"
],
"reviewed": true,
"review_reason": "人工结合实时 MCP/Skill/Cobra 审阅产品级选择边界。",
"review_reason": "人工结合多产品边界、根 Skill 的渐进加载顺序、真实 Cobra/Shortcut 路径与 MCP 语义审阅产品级选择边界。",
"source_refs": [
"internal/cli/schema_command_registry.json",
"skills/mono/references/products/aitable.md",
"skills/multi/dingtalk-aitable/SKILL.md",
"skills/multi/dingtalk-aitable/references/aitable.md",
"dws-schema-live:product-index"
]
}
+29 -30
View File
@@ -29,12 +29,12 @@
]
},
"chat.add_emoji_reaction": {
"agent_summary": "给指定消息添加表情回应",
"agent_summary": "使用同一会话中真实的 openMessageId 给指定消息添加表情回应",
"use_when": [
"需要对已有消息添加一个 emoji reaction 时"
"需要对已有消息添加一个 emoji reaction 时;conversation-id 与 msg-id 必须来自同一条真实消息"
],
"avoid_when": [
"发送文本消息或文字表情时不要使用"
"发送文本消息或文字表情时不要使用;不要把 openTaskId 当作 msg-id"
],
"examples": [
"dws chat message add-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""
@@ -658,12 +658,12 @@
]
},
"chat.query_message_send_status": {
"agent_summary": "查询异步消息发送任务的状态",
"agent_summary": "查询异步消息发送任务的业务终态,并按 sendStatus 判断是否真正投递成功",
"use_when": [
"发送命令返回 openTaskId 后需要确认投递结果时"
"发送命令返回 openTaskId 后需要确认投递结果时;只有 sendStatus=SUCCESS 才能宣称消息发送成功"
],
"avoid_when": [
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId"
"没有 message send 真实返回的 openTaskId 时不要使用;openMessageId 和会话 ID 都不能代替 openTaskId;FAILED 不得当作接口成功"
],
"examples": [
"dws chat message query-send-status --open-task-id <openTaskId>"
@@ -802,12 +802,12 @@
]
},
"chat.remove_emoji_reaction": {
"agent_summary": "移除指定消息上的表情回应",
"agent_summary": "从同一条真实消息移除此前添加的同名表情回应",
"use_when": [
"需要取消此前添加的 emoji reaction 时"
"需要取消此前添加的 emoji reaction 时;复用添加时的 conversation-id、msg-id 和 emoji"
],
"avoid_when": [
"移除文字表情时使用 chat message remove-text-emotion"
"移除文字表情时使用 chat message remove-text-emotion;不要猜测新的消息 ID 或表情名称"
],
"examples": [
"dws chat message remove-emoji --conversation-id <openConversationId> --msg-id <openMessageId> --emoji \"赞\""
@@ -824,10 +824,10 @@
"chat.remove_group_member": {
"agent_summary": "从指定群聊移除成员",
"use_when": [
"群管理员明确要移除一个或多个普通成员时"
"群管理员明确要移除一个或多个普通成员时;清理临时群只传本次加入的普通成员"
],
"avoid_when": [
"移除机器人时使用 chat group members remove-bot"
"移除机器人时使用 chat group members remove-bot;不要把建群者或群主加入 --users,也不要为了清理成员而转让群主"
],
"examples": [
"dws chat group members remove --id <openConversationId> --users userId1,userId2"
@@ -1236,12 +1236,12 @@
]
},
"chat.set_pin_message": {
"agent_summary": "把指定消息设为会话置顶消息",
"agent_summary": "使用同源的 openConversationId 和 openMessageId 把指定消息设为 Pin",
"use_when": [
"需要在会话中置顶一条已知消息时"
"需要在会话中钉住一条已知消息时;若发送只返回 openTaskId,先确认发送终态,再从消息查询结果取得真实 openMessageId"
],
"avoid_when": [
"取消置顶使用 chat message unset-pin-msg"
"取消 Pin 使用 chat message unset-pin-msg;置顶到会话顶部使用 set-top-msg,置顶整个会话使用 chat set-top"
],
"examples": [
"dws chat message set-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"
@@ -1316,12 +1316,12 @@
]
},
"chat.unset_pin_message": {
"agent_summary": "取消指定消息的会话置顶",
"agent_summary": "使用设置 Pin 时的同源会话和消息 ID 取消指定消息的 Pin",
"use_when": [
"需要移除一条已知置顶消息时"
"需要移除一条已知 Pin 消息时;复用 set-pin-msg 成功时的 openConversationId 和 openMessageId"
],
"avoid_when": [
"新增置顶使用 chat message set-pin-msg"
"新增 Pin 使用 chat message set-pin-msg;不要改用 unset-top-msg 或 chat set-top --off"
],
"examples": [
"dws chat message unset-pin-msg --open-conversation-id <openConversationId> --msg-id <openMessageId>"
@@ -1378,10 +1378,10 @@
"chat.update_group_icon": {
"agent_summary": "使用真实媒体 ID 更新群头像",
"use_when": [
"已有上传后的头像 mediaId 并要修改群头像时"
"已有可用于群头像接口的真实 mediaId 并要修改群头像时"
],
"avoid_when": [
"没有真实可用 mediaId 时先完成媒体上传"
"本地图片路径、钉盘 dentryId、聊天文件 uploadKey 都不能代替 icon-media-id;当前 CLI 无本地图片转群头像 mediaId 的能力,缺少 mediaId 时应如实报告能力边界"
],
"examples": [
"dws chat group update-icon --group <openConversationId> --icon-media-id @mediaId"
@@ -1418,10 +1418,10 @@
"chat.update_group_settings": {
"agent_summary": "更新指定群聊的一项设置开关",
"use_when": [
"需要调整 searchable、入群验证或群权限等设置时"
"需要调整 searchable、入群验证、@所有人权限等群级设置时;修改后用群信息或设置查询核对终态"
],
"avoid_when": [
"全员禁言和成员禁言使用专门的 mute 命令"
"全员禁言和成员禁言使用专门的 mute 命令;当前用户自己的会话置顶或免打扰使用 group user-settings set"
],
"examples": [
"dws chat group update-settings --group <openConversationId> --setting-key searchable --status 1"
@@ -2845,12 +2845,12 @@
]
},
"chat.shortcut_messages_query_send_status": {
"agent_summary": "查询消息发送状态",
"agent_summary": "查询消息发送任务的业务终态,只有 sendStatus=SUCCESS 才表示真正发送成功",
"use_when": [
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id。"
"当你发消息后拿到 openTaskId、想确认这条消息是否发送成功时使用;只读返回发送状态,需传 --open-task-id;FAILED 必须如实报告,不能因外层 success=true 宣称成功。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令;不要把 openTaskId 当 openMessageId 做 Pin、置顶或表情操作"
],
"examples": [
"dws chat +messages-query-send-status --open-task-id <openTaskId>"
@@ -3023,8 +3023,7 @@
"当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。"
],
"avoid_when": [
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget",
"按群名找群或解析群 openConversationId 时不要使用;改用 +chat-search"
"只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget"
],
"examples": [
"dws chat +search-msg --query \"周报\" --senders <openDingTalkId> --days 3 --page-all",
@@ -3143,10 +3142,10 @@
"chat.batch_query_group_chat_settings": {
"agent_summary": "批量查询当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
"use_when": [
"用户说 看下这些群我的置顶和免打扰设置"
"用户说看下这些群中我自己的置顶和免打扰设置;修改前先查询并保存原值用于恢复"
],
"avoid_when": [
"管理员级群功能开关用 chat group update-settings"
"管理员级群功能开关用 chat group update-settings;查询结果是当前用户视角,不代表群级设置"
],
"examples": [
"dws chat group user-settings query --groups cid1,cid2 --format json"
@@ -3162,10 +3161,10 @@
"chat.batch_update_group_chat_settings": {
"agent_summary": "批量更新当前用户自己的群会话设置(置顶/免打扰/群昵称/群备注)",
"use_when": [
"用户说 把这些群都设为免打扰/置顶"
"用户说把这些群都设为免打扰或置顶;items 中只写需要改变的当前用户设置,完成后再次 query 验证"
],
"avoid_when": [
"单个群昵称优先 chat group update-nick"
"单个群昵称优先 chat group update-nick;不要用本命令修改 searchable、@所有人权限等群级开关;临时测试结束时按修改前 query 的真实值恢复"
],
"examples": [
"dws chat group user-settings set --items '[{\"openConversationId\":\"cid1\",\"top\":true,\"mute\":false}]' --format json"
+63 -54
View File
@@ -82,22 +82,22 @@
]
},
"doc.create_document": {
"agent_summary": "创建一篇新的在线文档",
"agent_summary": "在默认根目录、文档文件夹或知识库根创建带可选初始内容的 adoc",
"use_when": [
"用户要新建一篇文字在线文档(adoc),可空文档或带初始 Markdown 时",
"创建到指定文件夹 --folder、知识库根 --workspace,或默认「我的文档」根目录时"
"用户要新建文字在线文档(adoc),可空文档或用 --content-file 写入明确要求的初始 Markdown/JSONML;用户显式要求正文 H1 时必须保留,不能用 --name 替代",
"已知目标文档文件夹 --folder 或知识库 --workspace;长 Markdown 仍直接使用本命令,由原生写入管道自动分片;返回 nodeId 绑定同一请求的后续 list/insert/export,后续可观察操作仍须分别执行"
],
"avoid_when": [
"创建表格/脑图/白板/多维表/演示改用 dws wiki node create --type <type>(勿用 doc create)",
"在知识库建空节点实体也可用 wiki node create;本命令侧重可写初始内容的 adoc",
"导入本地 Word/Markdown 为在线文档改用 dws doc import(若可用)或 upload --convert"
"导入本地 Word/Markdown 为在线文档用 dws doc import,并显式提供 --folder 或 --workspace;不要用已迁移的 doc upload --convert"
],
"examples": [
"dws doc create --name \"项目周报\" --format json",
"dws doc create --name \"Q1 总结\" --content \"# Q1 总结\" --folder <FOLDER_ID> --format json"
"dws doc create --name \"Q1 总结\" --content-file ./q1.md --workspace <WORKSPACE_ID> --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐当前 Cobra 原生内容写入管道、显式正文 H1、同请求新 nodeId 绑定和可观察块操作边界;强调长内容无需外部分片,不改变命令身份、参数或安全事实。",
"source_refs": [
"CommandRegistry:canonical_path=doc.create_document",
"cobra-help:dws doc create",
@@ -205,7 +205,7 @@
"用户明确要求用 doc delete 兼容入口将文档/文件移入回收站,且已确认目标时"
],
"avoid_when": [
"常规文件删除优先 dws drive delete;本入口仅为兼容",
"常规文件删除使用 dws drive delete;不要继续选择已弃用的 doc delete 兼容入口",
"用户未确认或目标不清时不要删",
"删块用 doc block delete;删评论用 doc comment delete"
],
@@ -250,10 +250,11 @@
"doc.download_doc_attachment": {
"agent_summary": "获取文档附件的临时下载链接",
"use_when": [
"获取文档正文中附件的临时下载 URL(resourceId 来自 block list attachment)时"
"获取文档正文中附件的临时下载 URL 时;resourceId 必须来自本次 block list 返回的 attachment 块"
],
"avoid_when": [
"下载钉盘普通文件用 drive download;导出在线文档用 doc export"
"下载钉盘普通文件用 drive download;导出在线文档用 doc export",
"不要把 blockId/nodeId 当作 resourceId,也不要编造 --output:本命令返回临时 downloadUrl"
],
"examples": [
"dws doc media download --node <DOC_ID> --resource-id <RESOURCE_ID> --format json"
@@ -295,12 +296,12 @@
"doc.get_document_content": {
"agent_summary": "读取完整文档内容,或按 outline/range/section/tags 获取 JSONML fragment",
"use_when": [
"用户要读取钉钉在线文字文档(adoc)正文(Markdown)时",
"用户直接粘贴文档 URL 且无其他指令时(默认读内容)",
"已由 drive info 确认 extension=adoc,用户要读取正文(Markdown)时",
"用户提供已知 adoc nodeId/URL,且要读取内容或抽取指定章节时",
"只需标题大纲、指定块区间/单块或特定 JSONML tags 时使用 --content-format jsonml 与 --scope"
],
"avoid_when": [
"非 adoc(表格/多维表/普通文件)不要用本命令;先 doc info 再路由",
"原始 alidocs URL 类型未知或目标不是 adoc 时先用 drive info 探测并路由;表格/多维表/普通文件不要用本命令",
"要元信息用 doc info;要块结构用 doc block list",
"Markdown 为有损投影:保形复制模板请用 doc copy,不要 read→create"
],
@@ -309,7 +310,7 @@
"dws doc read --node <DOC_ID> --content-format jsonml --scope outline --max-depth 3"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 doc Skill 与 doc-info reference 的 extension 预检边界,并保留 Markdown/JSONML scope 的真实 Cobra 契约。",
"source_refs": [
"CommandRegistry:canonical_path=doc.get_document_content",
"cobra-help:dws doc read",
@@ -321,22 +322,23 @@
]
},
"doc.get_document_info": {
"agent_summary": "获取文档元信息(标题/类型/创建者/权限等)",
"agent_summary": "在已确认是 ALIDOC 后读取文档专属元信息",
"use_when": [
"用户要查看文档/节点元信息(标题、类型、创建者、权限)时",
"准备读内容前必须先看 contentType/extension 以路由到 read/sheet/aitable/download 时"
"drive info 已确认是 ALIDOC,用户还要标题、创建者、权限或 docUrl 等文档专属元信息时",
"创建响应只有 nodeId、缺少 docUrl,需要补查文档链接时"
],
"avoid_when": [
"已确认是 adoc 且只要正文改用 dws doc read",
"原始 alidocs URL 的类型探测、extension 路由或可靠 fileSize 使用 dws drive info;不要先猜是文档",
"已确认是 adoc 且只要正文时改用 dws doc read",
"只要目录列表改用 dws drive list / wiki node list",
"需要可靠文件大小 fileSize 时改用 dws drive info;文档元信息接口可能不返回大小"
"普通文件、电子表格或 AI 表格不使用本命令"
],
"examples": [
"dws doc info --node <DOC_ID> --format json",
"dws doc info --node \"https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>\" --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 drive info 统一类型探测入口与 doc info 的文档专属补查职责;不改变 node 参数或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.get_document_info",
"cobra-help:dws doc info",
@@ -350,10 +352,10 @@
"doc.import_get": {
"agent_summary": "根据 taskId 查询文档导入任务的执行结果",
"use_when": [
"查询文档导入任务结果(已有 taskId,导入超时/中断后兜底)时"
"仅在 doc import 已返回真实 taskId 且自动轮询超时/中断后,查询导入任务结果时"
],
"avoid_when": [
"发起导入用 doc import(若入口可用);不要用本命令代替导入"
"发起导入用 doc import;不要先调用 import get,也不要使用示例占位 taskId"
],
"examples": [
"dws doc import get --task-id <TASK_ID> --format json"
@@ -371,11 +373,14 @@
"doc.insert_document_block": {
"agent_summary": "向文档插入块元素",
"use_when": [
"在文档中插入新块(段落/标题等);简单场景用 --text/--heading,复杂块用 --element JSON"
"用户明确要求在文档中插入新块(段落/标题/列表等)时;简单段落/标题用 --text/--heading,复杂块用 --element JSON",
"用户要求有序列表块时使用 JSONML p.list.isOrdered=true(同一 listId)或等价原生 orderedList 结构,不能用普通 Markdown 代替",
"相对插入时先 block list 获取真实 blockId,再同时传 --ref-block 与 --where before|after"
],
"avoid_when": [
"整篇追加 Markdown 优先 doc update --mode append",
"插入本地文件附件优先 doc media insert",
"仅在用户没有指定块操作、只要整篇追加 Markdown 时优先 doc update --mode append;显式 insert/标题块/列表块不能折叠",
"插入本地文件附件优先 doc media insert;不要为普通附件手写 attachment JSON",
"整段 Markdown 或长内容优先 doc update --mode append,不要拆成大量 block insert",
"删块用 block delete;改已有块用 block update"
],
"examples": [
@@ -383,7 +388,7 @@
"dws doc block insert --node <DOC_ID> --heading \"二级标题\" --level 2 --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐显式 block insert 的可观察操作语义和有序列表原生结构;不改变命令身份、参数契约、接口绑定或 Runtime 门禁。",
"source_refs": [
"CommandRegistry:canonical_path=doc.insert_document_block",
"cobra-help:dws doc block insert",
@@ -420,7 +425,7 @@
"doc.list_document_blocks": {
"agent_summary": "查询文档一级块元素列表",
"use_when": [
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时"
"查看文档一级块结构、拿 blockId,供 insert/update/delete 或划词评论定位时;用户在显式工作流中点名 list 时必须真实执行并使用返回结果"
],
"avoid_when": [
"只要全文 Markdown 用 doc read",
@@ -431,7 +436,7 @@
"dws doc block list --node <DOC_ID> --start-index 0 --end-index 5 --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 block list 的显式可观察步骤和真实 blockId 上下文传递;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=doc.list_document_blocks",
"cobra-help:dws doc block list",
@@ -606,10 +611,11 @@
"doc.reply_comment": {
"agent_summary": "回复文档评论",
"use_when": [
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 来自 list/create"
"回复已有评论(文字、可 @用户/@群,或 --emoji 表情);commentKey 必须来自本次 list/create 返回"
],
"avoid_when": [
"新建评论用 create/create-inline;删评论用 delete"
"新建评论用 create/create-inline;删评论用 delete",
"不要把 commentId、blockId 或示例占位符传给 --comment-key"
],
"examples": [
"dws doc comment reply --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"同意\" --mentioned-open-conversation-id <openConversationId> --format json",
@@ -739,15 +745,17 @@
]
},
"doc.update_document": {
"agent_summary": "更新文档内容(追加 / 覆盖;覆盖需 --yes)",
"agent_summary": "用原生自动分片管道追加或整篇覆盖 adoc 内容",
"use_when": [
"用户要向已有 adoc 追加内容时用 --mode append(更安全)",
"用户要向已有 adoc 追加长、多行或文件内容时用 --mode append + --content-file;CLI 自动分片",
"用户明确要求整篇覆盖替换时用 --mode overwrite(破坏性)",
"append 且要插到第 N 个 block 前时加 --index N(先 block list)"
"append 且要插到第 N 个 block 前时加 --index N(先 block list)",
"长文本、多行内容或表格优先写入 UTF-8 文件并使用 --content-file,避免 shell 转义损坏"
],
"avoid_when": [
"目标不是 adoc 或只要改单个块时改用 doc block update",
"覆盖模式用户未确认前不要执行;可先 --dry-run 预览",
"覆盖模式用户未确认前不要执行;先 --dry-run 预览,确认后再加 --yes",
"--content 与 --content-file 二选一;--index 仅用于 mode=append",
"创建新文档用 doc create,不要用 update 冒充创建"
],
"examples": [
@@ -755,7 +763,7 @@
"dws doc update --node <DOC_ID> --content-file ./body.md --mode overwrite --dry-run"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐 Runtime 的 10000 字符自动分片、append/overwrite 动态门禁与根 Skill 的写后回读流程;不改变参数和安全事实。",
"source_refs": [
"CommandRegistry:canonical_path=doc.update_document",
"cobra-help:dws doc update",
@@ -769,11 +777,12 @@
"doc.update_document_block": {
"agent_summary": "更新文档中的指定块",
"use_when": [
"修改已有块的文本/标题/样式(已知 blockId)时"
"修改已有块的文本/标题/样式时;blockId 必须来自目标文档本次 block list 结果"
],
"avoid_when": [
"插入新块用 block insert;删除用 block delete",
"改文档显示名用 rename;整篇覆盖用 update overwrite"
"改文档显示名用 rename;整篇覆盖用 update overwrite",
"不要跨文档复用 blockId,也不要把 nodeId/commentKey 当作 blockId"
],
"examples": [
"dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\" --format json"
@@ -905,14 +914,14 @@
"当你只记得文档的标题或主题词、需要先定位到某篇钉钉文档拿到它的 nodeId/URL 以便后续阅读或编辑时使用;可按关键词、扩展名、创建/访问时间、创建者等条件过滤,不传关键词则返回最近访问的文档,返回匹配的文档列表。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只有一个关键词且只要紧凑标题/URL/type/token 投影时优先 +find-doc;目标已给 nodeId/URL 时不要再搜索"
],
"examples": [
"dws doc +search --query \"会议纪要\"",
"dws doc +search --extensions pdf,docx"
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工区分 +search 的丰富过滤/最近访问能力与 +find-doc 的单关键词紧凑投影,避免两个同源搜索 Shortcut 互相争抢。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_search",
"cobra-help:dws doc +search",
@@ -925,14 +934,14 @@
"当你已知某个文档文件夹或知识库的 ID、想浏览它下面直接包含的文档与子文件夹(不递归深层)以便逐层导航时使用;输入 folder 或 workspace,返回该层级的子节点列表。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"全局或钉盘目录浏览优先 drive list,知识库节点树优先 wiki node list;本 Shortcut 只列已知 doc folder/workspace 的直接子节点"
],
"examples": [
"dws doc +list --folder DOC_FOLDER_NODE_ID",
"dws doc +list --workspace WS_ID --limit 20"
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工对齐 doc/drive/wiki 产品边界:保留已知文档文件夹/知识库的直接子节点投影,不替代常规目录管理。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_list",
"cobra-help:dws doc +list",
@@ -945,13 +954,13 @@
"当你想保留原件、在另一个文件夹或知识库里生成一份文档/文件副本(例如以某篇文档为模板另存)时使用;输入源 node 与目标 folder/workspace,会实际创建一个副本。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"常规文件管理优先 drive copy;要搬走原件用 move;复制后必须从真实返回取副本 nodeId,禁止继续编辑源文档"
],
"examples": [
"dws doc +copy --node DOC_ID --folder TARGET_FOLDER_NODE_ID"
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工对齐保形复制、真实副本 ID 与 drive 文件管理边界;保留 Runtime user_required 门禁。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_copy",
"cobra-help:dws doc +copy",
@@ -964,13 +973,13 @@
"当你要整理文档归属、把某篇文档/文件从当前位置挪到另一个文件夹或知识库(原位置不再保留)时使用;输入 node 与目标 folder/workspace,会实际改变文件的存放位置。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"常规文件管理优先 drive move;要保留原位置副本用 copy;目标位置不明确或未确认时不要移动"
],
"examples": [
"dws doc +move --node DOC_ID --folder TARGET_FOLDER_NODE_ID"
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工对齐 move 的原位置消失语义、目标位置确认与 drive 文件管理边界;保留 Runtime user_required 门禁。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_move",
"cobra-help:dws doc +move",
@@ -1041,13 +1050,13 @@
"当你想把在线文档导出成 docx/markdown/pdf 文件(例如离线保存或外发)时使用;这是异步任务的第一步,输入 node 与 export-format 提交导出,返回 jobId,随后用 +export-get 轮询结果。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"用户要直接拿到本地 docx/markdown/pdf 文件时使用一体化 dws doc export;不要默认手工编排 submit/get 轮询"
],
"examples": [
"dws doc +export-submit --node DOC_ID --export-format markdown"
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工对齐 atomic doc export 的一体化提交/轮询/下载路径,将 +export-submit 限定为明确异步控制场景。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_export_submit",
"cobra-help:dws doc +export-submit",
@@ -1117,13 +1126,13 @@
"当文档被误改、你想把它整体恢复到某个历史版本时使用;先用 +version-list 找到目标版本号,再输入 node 与 version,会实际把文档内容覆盖回该版本,属于高风险写操作,需谨慎确认。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"只查看历史用 +version-list,只保存当前快照用 +version-save;版本号未核实、用户未确认或只需改单块时不要回滚"
],
"examples": [
"dws doc +version-revert --node DOC_ID --version 3"
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工对齐 version list → 明确确认 → revert → 回读的 ID 与高风险边界;不改变 typed_yes 安全门禁。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_version_revert",
"cobra-help:dws doc +version-revert",
@@ -1174,14 +1183,14 @@
"当你只想往一篇钉钉文档的最后面补一段文字、又不想动原有内容时使用;内部用文档更新的“追加(append)”模式,把你给的文本安全地拼到文档末尾,不需要你先去查文档块列表、算末尾位置或手工拼块结构。会真实写入文档内容。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"长、多行、表格或文件内容使用 doc update --mode append --content-file;指定位置或富结构使用 block insert;不要跳过 user_required 确认"
],
"examples": [
"dws doc +doc-append --doc DOC_ID --text \"补充说明:本方案已评审通过。\"",
"dws doc +doc-append --doc \"https://alidocs.dingtalk.com/i/nodes/<DOC_UUID>\" --text \"追加一行备注\""
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工将 Shortcut 限定为短纯文本末尾追加,并对齐 user_required 与写后回读;长内容交给原生自动分片管道。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_doc_append",
"cobra-help:dws doc +doc-append",
@@ -1194,14 +1203,14 @@
"当你只记得云文档标题或内容里的某个关键词,想快速按关键词找到匹配的文档、拿到它的标题、URL、类型和 token 以便后续查看或编辑,却不想拿到一大坨原始字段时使用;内部调用云文档的 search_documents 工具,把 --query 作为搜索关键词(keyword),可选地用 --limit 限制返回条数(pageSize),再在本地把每条命中结果精简为「标题、URL、类型、token」四个字段后打印。这是纯只读操作,只做搜索与本地投影,不会创建、修改或删除任何文档;未命中时提示「没搜到文档」。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"需要最近访问、extension/时间/创建者等组合过滤时使用 +search;目标已给 nodeId/URL 时直接进入 info/read,不再搜索"
],
"examples": [
"dws doc +find-doc --query 季度汇报",
"dws doc +find-doc --query 合同 --limit 10"
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工将 +find-doc 定位为高频单关键词紧凑投影,并与 +search 的丰富过滤能力做互斥路由。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_find_doc",
"cobra-help:dws doc +find-doc",
@@ -1214,13 +1223,13 @@
"当你手上已经有一个文档链接、想直接私信发给某个人而不必先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,再用 openDingTalkId 把链接拼成一条 Markdown 消息发出去,姓名匹配到多人时会列出候选让你区分。只发链接、不读取或改动文档本身,会真实发出消息。"
],
"avoid_when": [
"需要该 Shortcut 未公开的底层参数、原始响应或不同执行语义时,改用对应原子命令"
"同名人员未消歧、缺少真实文档 URL 或用户未确认时不要发送;本命令不授予文档权限,授权应走 drive permission"
],
"examples": [
"dws doc +share-doc --to 张三 --url https://docs.dingtalk.com/xxx --note \"帮忙过一下\""
],
"reviewed": true,
"review_reason": "Agent-authored and reviewed from the built-in Shortcut intent, executable Cobra path, and declared outcome; it affects selection only and does not alter execution or safety facts.",
"review_reason": "人工对齐人员消歧、真实 docUrl、user_required 消息发送门禁与文档权限边界。",
"source_refs": [
"internal/cli/schema_command_registry.json#doc.shortcut_share_doc",
"cobra-help:dws doc +share-doc",
@@ -910,12 +910,15 @@
"agent_summary": "上传本地文件到钉盘或文档空间,或按节点 ID 确认覆盖已有文件",
"use_when": [
"用户要把本地文件上传到钉盘/我的文件(首选一条命令自动完成凭证+PUT+提交)时",
"上传到知识库/文档空间时加 --workspace;需要转在线文档时加 --convert",
"用户明确要求只上传或暂存附件、暂时不要发送到任何聊天会话时",
"用户只要保留原始文件供存储或下载时使用;上传到知识库/文档空间时可加 --workspace",
"用户明确要求用本地文件替换已有钉盘/文档空间文件时传 --node;该模式会覆盖远端内容并要求确认"
],
"avoid_when": [
"用户要把文件发送到群聊或单聊时使用 chat message send --msg-type file --file-path;drive upload 不会发送聊天消息",
"常规场景不要拆成 upload-info + 手动 PUT + commit;仅自定义流式上传才用三步",
"要把文件作为文档正文附件插入改用 dws doc media insert",
"Word/Excel/Markdown 要求上传后在线编辑、大家直接在线改或转钉钉文档时改用 dws doc import;普通 drive upload 返回 docx/xlsx 文件不能宣称可在线编辑",
"用户明确说文档空间且走 doc 兼容入口时可用 dws doc upload,默认仍推荐本命令",
"用户没有明确同意替换目标文件时不要使用 --node;新建上传应使用 --folder 或目标根目录"
],
@@ -924,7 +927,7 @@
"dws drive upload --file ./README.md --node <dentryUuid> --format json"
],
"reviewed": true,
"review_reason": "人工审阅:结合本机 dws schema 实时 MCP description/parameters(经 interface_ref)、产品 Skill、Cobra Long 与 Runtime 确认门禁,按飞书风格重写选型文案;不改变命令身份、参数契约或接口绑定。",
"review_reason": "人工对齐普通文件存储与在线文档导入边界:在线编辑需求硬路由 doc import,drive upload 不承诺转换结果;不改变命令身份、参数契约或接口绑定。",
"source_refs": [
"CommandRegistry:canonical_path=drive.upload",
"cobra-help:dws drive upload",
+18
View File
@@ -363,6 +363,24 @@ func TestSuggestBusinessHintChatRecovery(t *testing.T) {
}
}
func TestSuggestBusinessHintDocRecovery(t *testing.T) {
cases := []struct{ message, want string }{
{"nodeId not found", "drive search"},
{"blockId 不存在", "block list"},
{"commentKey invalid", "comment list/create"},
{"resourceId not found", "attachment"},
{"workspaceId invalid", "wiki space list"},
{"folderId invalid", "dentryId"},
{"templateId not found", "template search"},
{"version 不存在", "version list"},
}
for _, tc := range cases {
if got := SuggestBusinessHint(map[string]any{"message": tc.message}); !strings.Contains(got, tc.want) {
t.Errorf("SuggestBusinessHint(%q) = %q, want containing %q", tc.message, got, tc.want)
}
}
}
func TestCrossPlatformCoveragePATSerializationAndPolicyEdges(t *testing.T) {
oldHost := hostControlProvider
oldBrowser := patBrowserProvider
+16
View File
@@ -340,6 +340,22 @@ func suggestForBusinessErrorText(body map[string]any) string {
switch {
case strings.Contains(msg, "搜索内容不能为空"):
return "请提供非空搜索关键词: dws doc search --query \"关键词\""
case strings.Contains(msg, "nodeId") && (strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")):
return "目标文档 nodeId 不存在或当前账号不可见。请用 dws drive search 或 dws wiki node search 重新获取真实 nodeId;不要复用示例占位符。"
case strings.Contains(msg, "blockId") && (strings.Contains(msg, "not found") || strings.Contains(msg, "不存在")):
return "目标 blockId 不存在或已变化。请重新执行 dws doc block list --node <DOC_ID> --format json,并从本次结果取 blockId。"
case strings.Contains(msg, "commentKey") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "commentKey 无效或评论已不存在。请从 dws doc comment list/create 的当前返回结果中提取 commentKey,不要使用 commentId 或占位符。"
case strings.Contains(msg, "resourceId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "resourceId 无效。请用 dws doc block list 查找 attachment 块并读取真实 resourceId;不要把 blockId 当作 resourceId。"
case strings.Contains(msg, "workspaceId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "workspaceId 无效或无权访问。请用 dws wiki space list --type myWikiSpace --format json 获取当前账号可用的 workspaceId。"
case strings.Contains(msg, "folderId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "folderId 无效。doc 的 --folder 需要文档文件夹 nodeId/URL,不是 drive 的纯数字 dentryId;请重新列出目标空间节点。"
case strings.Contains(msg, "templateId") && (strings.Contains(msg, "invalid") || strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "templateId 无效或模板不可见。请先用 dws doc template search --query <关键词> --format json 获取当前账号可用的真实 templateId。"
case strings.Contains(msg, "version") && (strings.Contains(msg, "不存在") || strings.Contains(msg, "not found")):
return "目标版本不存在。请先执行 dws doc version list --node <DOC_ID> --format json,并从当前版本列表选择可回滚版本。"
case strings.Contains(msg, "User has no permission to access this email"):
return "请确认邮箱地址正确,查看可用邮箱: dws mail mailbox list"
case strings.Contains(msg, "频率超限") || strings.Contains(msg, "rate limit"):
+177 -2
View File
@@ -9,6 +9,7 @@ import (
"strconv"
"strings"
"time"
"unicode/utf8"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli"
"github.com/spf13/cobra"
@@ -132,6 +133,8 @@ func recordQueryFetchAll(toolArgs map[string]any, pageLimit int) error {
var allRecords []any
page := 0
lastCursor := ""
seenCursors := map[string]struct{}{}
stopReason := ""
for {
page++
@@ -201,6 +204,19 @@ func recordQueryFetchAll(toolArgs map[string]any, pageLimit int) error {
lastCursor = ""
break
}
if current, _ := toolArgs["cursor"].(string); cursor == current {
lastCursor = cursor
stopReason = "cursor_not_advanced"
fmt.Fprintf(os.Stderr, "[pagination] cursor did not advance (%q), stopping to avoid an infinite loop. Resume with --cursor %q after checking the service response.\n", cursor, cursor)
break
}
if _, exists := seenCursors[cursor]; exists {
lastCursor = cursor
stopReason = "cursor_cycle_detected"
fmt.Fprintf(os.Stderr, "[pagination] cursor cycle detected (%q), stopping to avoid an infinite loop. Resume with --cursor %q after checking the service response.\n", cursor, cursor)
break
}
seenCursors[cursor] = struct{}{}
// Check page limit (0 = unlimited)
if pageLimit > 0 && page >= pageLimit {
@@ -223,6 +239,10 @@ func recordQueryFetchAll(toolArgs map[string]any, pageLimit int) error {
if lastCursor != "" {
mergedData["cursor"] = lastCursor
mergedData["hasMore"] = true
if stopReason != "" {
mergedData["incomplete"] = true
mergedData["stopReason"] = stopReason
}
} else {
mergedData["hasMore"] = false
}
@@ -781,6 +801,20 @@ func isAitableRetryableError(err error) bool {
}
msg := strings.ToLower(err.Error())
// 确定性业务错误优先于外层 category/retryable 标记,避免对不存在的资源、
// 参数错误或权限错误做无意义重试。部分 MCP 响应会同时携带
// "category: internal" 或 "retryable: true",因此必须先判定终止类错误。
nonRetryablePatterns := []string{
"input_error", "user_error", "auth_error", "permission denied",
"invalid parameter", "invalid argument", "bad request",
"not found", "no record", "does not exist", "不存在",
}
for _, p := range nonRetryablePatterns {
if strings.Contains(msg, p) {
return false
}
}
// 网络瞬态错误
retryablePatterns := []string{
"timeout", "deadline exceeded", "connection reset",
@@ -814,17 +848,154 @@ func isAitableRetryableError(err error) bool {
func parseFieldsJSON(raw string) ([]any, error) {
var fields []any
if err := json.Unmarshal([]byte(raw), &fields); err == nil {
return fields, nil
return validateAitableCreateFields(fields)
}
var wrapper map[string]any
if err := json.Unmarshal([]byte(raw), &wrapper); err == nil {
if arr, ok := wrapper["fields"].([]any); ok {
return arr, nil
return validateAitableCreateFields(arr)
}
}
return nil, fmt.Errorf("--fields JSON parse failed: expect a JSON array [...]\n hint: example: '[{\"fieldName\":\"名称\",\"type\":\"text\"}]'")
}
// validateAitableCreateFields catches stable field-contract mistakes before MCP dispatch
// and returns a directly actionable correction. It intentionally does not normalize aliases:
// silently changing a field type can alter business semantics.
func validateAitableCreateFields(fields []any) ([]any, error) {
if len(fields) > 15 {
return nil, fmt.Errorf("--fields contains %d items; AI Table accepts at most 15 fields per request\n hint: split the fields into batches of 15 or fewer", len(fields))
}
for i, rawField := range fields {
field, ok := rawField.(map[string]any)
if !ok {
return nil, fmt.Errorf("--fields[%d] must be a JSON object\n hint: example: {\"fieldName\":\"状态\",\"type\":\"singleSelect\"}", i)
}
if _, hasAlias := field["fieldType"]; hasAlias {
return nil, fmt.Errorf("--fields[%d] uses unsupported key fieldType\n hint: use fieldName + type, for example {\"fieldName\":\"状态\",\"type\":\"singleSelect\"}", i)
}
fieldName, nameOK := field["fieldName"].(string)
if !nameOK || strings.TrimSpace(fieldName) == "" {
return nil, fmt.Errorf("--fields[%d].fieldName must be a non-empty string\n hint: example: {\"fieldName\":\"任务名称\",\"type\":\"text\"}", i)
}
if strings.ContainsAny(fieldName, "\r\n") {
return nil, fmt.Errorf("--fields[%d].fieldName must not contain line breaks\n hint: use a single-line field name with at most 100 characters", i)
}
if utf8.RuneCountInString(fieldName) > 100 {
return nil, fmt.Errorf("--fields[%d].fieldName is %d characters; maximum is 100\n hint: shorten the field name before retrying", i, utf8.RuneCountInString(fieldName))
}
fieldType, typeOK := field["type"].(string)
fieldType = strings.TrimSpace(fieldType)
if !typeOK || fieldType == "" {
return nil, fmt.Errorf("--fields[%d].type must be a non-empty string\n hint: common types: text, number, singleSelect, multipleSelect, date, user, attachment", i)
}
if strings.EqualFold(strings.TrimSpace(fieldType), "select") {
return nil, fmt.Errorf("--fields[%d].type %q is not a valid AI Table field type\n hint: use \"singleSelect\" for single choice or \"multipleSelect\" for multiple choice; do not use \"select\"", i, fieldType)
}
if !aitableFieldTypes[fieldType] {
return nil, fmt.Errorf("--fields[%d].type %q is unsupported\n hint: use a documented type such as text, number, singleSelect, multipleSelect, date, user, attachment, url, or richText", i, fieldType)
}
if fieldType == "primaryDoc" && i != 0 {
return nil, fmt.Errorf("--fields[%d] uses primaryDoc outside the first column\n hint: primaryDoc is allowed only as --fields[0]", i)
}
config, hasConfig := field["config"]
var configMap map[string]any
if hasConfig {
var configOK bool
configMap, configOK = config.(map[string]any)
if !configOK {
return nil, fmt.Errorf("--fields[%d].config must be a JSON object\n hint: example: {\"options\":[{\"name\":\"高\"},{\"name\":\"低\"}]}", i)
}
}
if err := validateAitableFieldConfig(i, fieldType, configMap); err != nil {
return nil, err
}
}
return fields, nil
}
var aitableFieldTypes = map[string]bool{
"text": true, "number": true, "singleSelect": true, "multipleSelect": true, "date": true,
"currency": true, "user": true, "department": true, "group": true, "progress": true,
"rating": true, "checkbox": true, "attachment": true, "url": true, "richText": true,
"telephone": true, "email": true, "idCard": true, "barcode": true, "geolocation": true,
"address": true, "primaryDoc": true, "formula": true, "filterUp": true, "lookup": true,
"unidirectionalLink": true, "bidirectionalLink": true, "creator": true, "lastModifier": true,
"createdTime": true, "lastModifiedTime": true,
}
func validateAitableFieldConfig(index int, fieldType string, config map[string]any) error {
if fieldType == "singleSelect" || fieldType == "multipleSelect" {
if config == nil {
return fmt.Errorf("--fields[%d].config.options is required for %s\n hint: use {\"options\":[{\"name\":\"选项A\"},{\"name\":\"选项B\"}]}", index, fieldType)
}
options, ok := config["options"].([]any)
if !ok || len(options) == 0 {
return fmt.Errorf("--fields[%d].config.options must be a non-empty JSON array\n hint: example: {\"options\":[{\"name\":\"高\"},{\"name\":\"低\"}]}", index)
}
for optionIndex, rawOption := range options {
option, ok := rawOption.(map[string]any)
if !ok {
return fmt.Errorf("--fields[%d].config.options[%d] must be a JSON object\n hint: each option must look like {\"name\":\"选项名\"}", index, optionIndex)
}
name, ok := option["name"].(string)
if !ok || strings.TrimSpace(name) == "" {
return fmt.Errorf("--fields[%d].config.options[%d].name must be a non-empty string\n hint: example: {\"name\":\"高\"}", index, optionIndex)
}
}
}
if config == nil {
if fieldType == "formula" {
return fmt.Errorf("--fields[%d].config.formula is required for formula\n hint: example: {\"formula\":\"[单价] * [数量]\"}", index)
}
if fieldType == "unidirectionalLink" || fieldType == "bidirectionalLink" {
return fmt.Errorf("--fields[%d].config.linkedTableId is required for %s\n hint: get the target table ID first, then use {\"linkedTableId\":\"<TABLE_ID>\",\"multiple\":true}", index, fieldType)
}
return nil
}
if formatter, ok := config["formatter"].(string); ok && formatter != "" {
allowed := map[string]map[string]bool{
"number": {"INT": true, "FLOAT_1": true, "FLOAT_2": true, "FLOAT_3": true, "FLOAT_4": true, "THOUSAND": true, "THOUSAND_FLOAT": true, "PERCENT": true, "PERCENT_FLOAT": true},
"date": {"YYYY-MM-DD": true, "YYYY-MM-DD HH:mm": true, "YYYY-MM-DD HH:mm:ss": true, "YYYY/MM/DD": true, "YYYY/MM/DD HH:mm": true},
"currency": {"INT": true, "FLOAT_1": true, "FLOAT_2": true, "FLOAT_3": true, "FLOAT_4": true},
"progress": {"PERCENT": true},
}
if typeAllowed, applies := allowed[fieldType]; applies && !typeAllowed[formatter] {
return fmt.Errorf("--fields[%d].config.formatter %q is invalid for %s\n hint: run 'dws aitable table create --help' and use a formatter listed for %s", index, formatter, fieldType, fieldType)
}
}
if fieldType == "currency" {
currency, _ := config["currencyType"].(string)
currencies := map[string]bool{"CNY": true, "HKD": true, "USD": true, "EUR": true, "GBP": true, "MOP": true, "VND": true, "JPY": true, "KRW": true, "AED": true, "AUD": true, "BRL": true, "CAD": true, "CHF": true, "INR": true, "IDR": true, "MXN": true, "MYR": true, "PHP": true, "PLN": true, "RUB": true, "SGD": true, "THB": true, "TRY": true, "TWD": true}
if currency != "" && !currencies[currency] {
return fmt.Errorf("--fields[%d].config.currencyType %q is unsupported\n hint: use an ISO currency from table create --help, for example CNY, USD, EUR, JPY, or HKD", index, currency)
}
}
if fieldType == "rating" {
if max, ok := config["max"].(float64); ok && (max < 1 || max > 10) {
return fmt.Errorf("--fields[%d].config.max must be between 1 and 10 for rating\n hint: example: {\"min\":1,\"max\":5,\"icon\":\"star\"}", index)
}
}
if multiple, exists := config["multiple"]; exists {
if _, ok := multiple.(bool); !ok {
return fmt.Errorf("--fields[%d].config.multiple must be true or false\n hint: use a JSON boolean without quotes, for example {\"multiple\":false}", index)
}
}
if fieldType == "formula" {
formula, _ := config["formula"].(string)
if strings.TrimSpace(formula) == "" {
return fmt.Errorf("--fields[%d].config.formula is required for formula\n hint: example: {\"formula\":\"[单价] * [数量]\"}", index)
}
}
if fieldType == "unidirectionalLink" || fieldType == "bidirectionalLink" {
linkedTableID, _ := config["linkedTableId"].(string)
if strings.TrimSpace(linkedTableID) == "" {
return fmt.Errorf("--fields[%d].config.linkedTableId is required for %s\n hint: get the target table ID first, then use {\"linkedTableId\":\"<TABLE_ID>\",\"multiple\":true}", index, fieldType)
}
}
return nil
}
// resolveFormUpdateTitle 折叠 form update 的 --title / --name 别名为单个 title 值。
// 优先 --title,未设置时回退到 --name;两者都未传返回 ""。
// 抽出独立函数便于单测覆盖。
@@ -1370,6 +1541,10 @@ config 结构参考:
} else {
return fmt.Errorf("must specify either --fields OR both --name and --type")
}
fields, err := validateAitableCreateFields(fields)
if err != nil {
return err
}
baseID, err := mustFlagOrFallback(cmd, "base-id", "base")
if err != nil {
@@ -88,6 +88,87 @@ func TestCrossPlatformCoverageAitableRetryWrappersExhaustAndRecover(t *testing.T
}
}
func TestAitableTableCreateInvalidFieldContractDoesNotDispatchMCP(t *testing.T) {
oldDeps, oldArgs := deps, os.Args
t.Cleanup(func() { deps, os.Args = oldDeps, oldArgs })
longName := strings.Repeat("字", 101)
manyFields := make([]string, 16)
for i := range manyFields {
manyFields[i] = fmt.Sprintf(`{"fieldName":"F%d","type":"text"}`, i)
}
cases := []struct {
name string
fields string
hint string
}{
{"field must be object", `[1]`, "must be a JSON object"},
{"fieldType key", `[{"fieldName":"状态","fieldType":"singleSelect"}]`, "fieldName + type"},
{"missing fieldName", `[{"type":"text"}]`, "fieldName must be"},
{"empty fieldName", `[{"fieldName":" ","type":"text"}]`, "fieldName must be"},
{"fieldName line break", `[{"fieldName":"任务\n名称","type":"text"}]`, "must not contain line breaks"},
{"fieldName too long", fmt.Sprintf(`[{"fieldName":%q,"type":"text"}]`, longName), "maximum is 100"},
{"missing type", `[{"fieldName":"任务"}]`, "type must be"},
{"unknown type", `[{"fieldName":"任务","type":"string"}]`, "type \"string\" is unsupported"},
{"select alias", `[{"fieldName":"状态","type":"select"}]`, "singleSelect"},
{"too many fields", `[` + strings.Join(manyFields, ",") + `]`, "at most 15"},
{"config scalar", `[{"fieldName":"任务","type":"text","config":"bad"}]`, "config must be a JSON object"},
{"single select missing options", `[{"fieldName":"状态","type":"singleSelect"}]`, "config.options is required"},
{"select options not array", `[{"fieldName":"状态","type":"singleSelect","config":{"options":{}}}]`, "non-empty JSON array"},
{"select options empty", `[{"fieldName":"状态","type":"singleSelect","config":{"options":[]}}]`, "non-empty JSON array"},
{"select option scalar", `[{"fieldName":"状态","type":"singleSelect","config":{"options":["高"]}}]`, "must be a JSON object"},
{"select option empty name", `[{"fieldName":"状态","type":"singleSelect","config":{"options":[{"name":""}]}}]`, "name must be a non-empty string"},
{"number formatter", `[{"fieldName":"金额","type":"number","config":{"formatter":"CURRENCY_YUAN"}}]`, "invalid for number"},
{"date formatter", `[{"fieldName":"日期","type":"date","config":{"formatter":"MM-DD-YYYY"}}]`, "invalid for date"},
{"currency formatter", `[{"fieldName":"金额","type":"currency","config":{"formatter":"THOUSAND"}}]`, "invalid for currency"},
{"currency type", `[{"fieldName":"金额","type":"currency","config":{"currencyType":"RMB"}}]`, "currencyType \"RMB\" is unsupported"},
{"progress formatter", `[{"fieldName":"进度","type":"progress","config":{"formatter":"PERCENT_FLOAT"}}]`, "invalid for progress"},
{"rating max", `[{"fieldName":"评分","type":"rating","config":{"max":11}}]`, "between 1 and 10"},
{"multiple must be boolean", `[{"fieldName":"负责人","type":"user","config":{"multiple":"false"}}]`, "must be true or false"},
{"formula missing formula", `[{"fieldName":"总价","type":"formula","config":{}}]`, "config.formula is required"},
{"link missing table", `[{"fieldName":"关联","type":"unidirectionalLink","config":{"multiple":true}}]`, "linkedTableId is required"},
{"primary doc not first", `[{"fieldName":"名称","type":"text"},{"fieldName":"文档","type":"primaryDoc"}]`, "only as --fields[0]"},
}
for _, test := range cases {
t.Run(test.name, func(t *testing.T) {
for _, args := range [][]string{
{"table", "create", "--base-id=b", "--name=n", "--fields=" + test.fields},
{"field", "create", "--base-id=b", "--table-id=t", "--fields=" + test.fields},
} {
caller := &aitableTestCaller{}
err := runAitableCoverageCommand(t, caller, args...)
if err == nil || !strings.Contains(err.Error(), test.hint) {
t.Fatalf("invalid fields error = %v, want hint %q", err, test.hint)
}
if len(caller.calls) != 0 {
t.Fatalf("invalid fields dispatched %d MCP call(s), want zero", len(caller.calls))
}
}
})
}
for _, test := range []struct {
name string
args []string
hint string
}{
{"typed unknown type", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=字段", "--type=string"}, "type \"string\" is unsupported"},
{"typed select alias", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=状态", "--type=select"}, "singleSelect"},
{"typed select no options", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=状态", "--type=singleSelect"}, "config.options is required"},
{"typed invalid formatter", []string{"field", "create", "--base-id=b", "--table-id=t", "--name=金额", "--type=number", `--config={"formatter":"CURRENCY_YUAN"}`}, "invalid for number"},
} {
t.Run(test.name, func(t *testing.T) {
caller := &aitableTestCaller{}
err := runAitableCoverageCommand(t, caller, test.args...)
if err == nil || !strings.Contains(err.Error(), test.hint) {
t.Fatalf("typed field error = %v, want hint %q", err, test.hint)
}
if len(caller.calls) != 0 {
t.Fatalf("typed invalid field dispatched %d MCP call(s), want zero", len(caller.calls))
}
})
}
}
func TestCrossPlatformCoverageAitableCommandValidationEdges(t *testing.T) {
oldDeps, oldArgs, oldStdin, oldSleep := deps, os.Args, os.Stdin, helperSleep
t.Cleanup(func() {
+24 -3
View File
@@ -180,7 +180,9 @@ func TestCrossPlatformCoverageAitableViewConfigAndHelpers(t *testing.T) {
for _, tc := range []struct {
err error
want bool
}{{nil, false}, {errors.New("timeout"), true}, {errors.New("SYSTEM_ERROR"), true}, {errors.New("retryable: true"), true}, {errors.New("bad request"), false}} {
}{{nil, false}, {errors.New("timeout"), true}, {errors.New("SYSTEM_ERROR"), true}, {errors.New("retryable: true"), true}, {errors.New("bad request"), false},
{errors.New(`category: internal_error, type: INPUT_ERROR, retryable: true, message: no record`), false},
{errors.New(`category: internal_error, retryable: true, message: resource not found`), false}} {
if got := isAitableRetryableError(tc.err); got != tc.want {
t.Errorf("isAitableRetryableError(%v) = %v", tc.err, got)
}
@@ -248,15 +250,26 @@ func TestCrossPlatformCoverageAitableViewConfigAndHelpers(t *testing.T) {
t.Fatalf("typed-only update = %#v, %v", merged, err)
}
for _, raw := range []string{`[1]`, `{"fields":[1]}`, `{}`, `{`} {
for _, raw := range []string{`[{"fieldName":"N","type":"text"}]`, `{"fields":[{"fieldName":"N","type":"text"}]}`, `{}`, `{`} {
fields, err := parseFieldsJSON(raw)
if (raw == `[1]` || strings.Contains(raw, "fields")) && (err != nil || len(fields) != 1) {
if (strings.HasPrefix(raw, `[{`) || strings.Contains(raw, "fields")) && (err != nil || len(fields) != 1) {
t.Errorf("parseFieldsJSON(%q) = %#v, %v", raw, fields, err)
}
if (raw == `{}` || raw == `{`) && err == nil {
t.Errorf("parseFieldsJSON(%q) should fail", raw)
}
}
for _, tc := range []struct {
raw string
hint string
}{
{`[{"fieldName":"状态","type":"select"}]`, "singleSelect"},
{`[{"fieldName":"状态","fieldType":"singleSelect"}]`, "fieldName + type"},
} {
if _, err := parseFieldsJSON(tc.raw); err == nil || !strings.Contains(err.Error(), tc.hint) {
t.Errorf("parseFieldsJSON(%q) error = %v, want hint %q", tc.raw, err, tc.hint)
}
}
}
func TestCrossPlatformCoverageAitableToolResponseAndPaginationHelpers(t *testing.T) {
@@ -312,4 +325,12 @@ func TestCrossPlatformCoverageAitableToolResponseAndPaginationHelpers(t *testing
if err := recordQueryFetchAll(map[string]any{}, 1); err == nil {
t.Fatal("first-page pagination error should fail")
}
caller = &aitableTestCaller{responses: []string{
`{"data":{"records":[{"id":1}],"nextCursor":"same"}}`,
`{"data":{"records":[{"id":2}],"nextCursor":"same"}}`,
}}
out = installAitableDeps(t, caller)
if err := recordQueryFetchAll(map[string]any{}, 0); err != nil || !strings.Contains(out.String(), `"stopReason"`) || !strings.Contains(out.String(), `"cursor_not_advanced"`) {
t.Fatalf("repeated cursor guard = %q, %v", out.String(), err)
}
}
+13 -12
View File
@@ -462,9 +462,12 @@ func guardGroupOwnerRemoval(ctx context.Context, openConversationID string, remo
if err != nil || ownerOpenID == "" {
return nil
}
ownerErr := fmt.Errorf(
"refusing to remove the group owner: 被移除列表包含群主,移出群主将导致群无群主(孤儿群)\n hint: 先执行 dws chat group transfer-owner --group %s --user <newOwnerUserId> 转让群主后再移除",
openConversationID,
ownerErr := apperrors.NewValidation(
"被移除列表包含群主,不能直接移出群主",
apperrors.WithReason("group_owner_in_remove_list"),
apperrors.WithHint("清理临时群成员时应从 --users 中移除群主,只移除本次加入的普通成员;只有用户明确要求变更群主时才单独执行 transfer-owner。"),
apperrors.WithActions("从 --users 中移除群主后重试", "若用户明确要求转让群主,先确认新群主再单独执行 transfer-owner"),
apperrors.WithExamples(fmt.Sprintf("dws chat group members remove --id %s --users <普通成员userId列表> --format json", openConversationID)),
)
userIDs, openDingTalkIDs := splitChatIDValues(removeValues)
for _, id := range openDingTalkIDs {
@@ -2238,7 +2241,7 @@ func newChatCommand() *cobra.Command {
hasCondition := false
for _, name := range []string{
"query", "keyword", "user", "users", "userId", "sender-ids", "senders", "sender",
"at-ids", "conversation-ids", "groups", "group", "message-type",
"at-ids", "conversation-ids", "conversation-id", "groups", "group", "message-type",
"conversation-type", "search-conv-type", "start", "end",
} {
if value, _ := cmd.Flags().GetString(name); strings.TrimSpace(value) != "" {
@@ -2291,6 +2294,8 @@ func newChatCommand() *cobra.Command {
convIds := ""
if v, _ := cmd.Flags().GetString("conversation-ids"); v != "" {
convIds = v
} else if v, _ := cmd.Flags().GetString("conversation-id"); v != "" {
convIds = v
} else if v, _ := cmd.Flags().GetString("groups"); v != "" {
convIds = v
} else if v, _ := cmd.Flags().GetString("group"); v != "" {
@@ -2581,7 +2586,6 @@ func newChatCommand() *cobra.Command {
_ = chatGroupMemberRemoveCmd.MarkFlagRequired("users")
chatGroupCmd.AddCommand(chatGroupCreateCmd, chatGroupMembersCmd, chatGroupRenameCmd)
chatGroupCmd.AddCommand(hintSubCmd("search", "use: dws chat search --query <关键词>"))
chatGroupMembersCmd.AddCommand(chatGroupMemberAddCmd, chatGroupMemberRemoveCmd, chatGroupMembersAddBotCmd)
// message 子命令 flags
@@ -2816,6 +2820,8 @@ func newChatCommand() *cobra.Command {
chatMessageSearchAdvancedCmd.Flags().Bool("at-me", false, "只搜索 @我 的消息(可选,默认 false)")
chatMessageSearchAdvancedCmd.Flags().String("at-ids", "", "@指定人的 openDingTalkId 列表,逗号分隔(可选)")
chatMessageSearchAdvancedCmd.Flags().String("conversation-ids", "", "会话 openConversationId 列表,逗号分隔(可选,群聊或单聊均可,不传则搜索所有会话)")
chatMessageSearchAdvancedCmd.Flags().String("conversation-id", "", "--conversation-ids 的单值兼容别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("conversation-id")
chatMessageSearchAdvancedCmd.Flags().String("groups", "", "--conversation-ids 的别名")
_ = chatMessageSearchAdvancedCmd.Flags().MarkHidden("groups")
chatMessageSearchAdvancedCmd.Flags().String("group", "", "")
@@ -3770,8 +3776,8 @@ flow-status 取值:1=处理中(PROCESSING),2=输入中(INPUTTING),3=完成
return err
}
iconMediaID := strings.TrimSpace(mustGetFlag(cmd, "icon-media-id"))
if iconMediaID == "" {
return fmt.Errorf("invalid --icon-media-id: mediaId 不能为空\n hint: 请使用上游媒体上传能力返回的有效 mediaId;DWS CLI 不提供本地文件到 mediaId 的上传命令")
if err := ValidateChatMediaID(iconMediaID); err != nil {
return fmt.Errorf("invalid --icon-media-id: %w", err)
}
return callMCPToolOnServer("im", "update_group_icon", map[string]any{
"openConversationId": mustGetFlag(cmd, "group"),
@@ -5736,10 +5742,5 @@ pl_PL, sv_SE, fi_FI, cs_CZ, ar_SA, tl_PH, he_IL, nl_NL, lo_LA, it_IT`,
root.AddCommand(chatChmodCmd, chatDataAuthCmd, chatGroupCmd, chatSearchCmd, chatSearchCommonCmd, chatMessageCmd, chatFileCmd, newChatMediaGroup(), chatBotCmd, chatMessageListTopConversationsCmd, chatConversationInfoCmd, chatCategoryCmd, chatGroupRoleCmd, chatMuteCmd, chatSetTopCmd, chatGroupMuteCmd, chatGroupMuteMemberCmd, chatHideCmd, chatMuteAtAllCmd, chatMuteRedEnvelopeCmd, chatMarkUnreadCmd, chatClearRedPointCmd, chatClearAllRedPointCmd, chatListAllConversationsCmd, chatClearMessagesCmd, chatMarkReadCmd, chatTextCmd)
// hint: dws chat send → dws chat message send
root.AddCommand(hintSubCmd("send", "use: dws chat message send"))
// hint: dws chat history → dws chat message list
root.AddCommand(hintSubCmd("history", "use: dws chat message list --group <GROUP_OPEN_CONVERSATION_ID>"))
return root
}
@@ -85,6 +85,22 @@ func TestCrossPlatformCoverageChatGroupUpdateIconRejectsBlankMediaID(t *testing.
}
}
func TestCrossPlatformCoverageChatGroupUpdateIconRejectsLocalPathBeforeMCP(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
t.Cleanup(func() { deps, os.Args = previousDeps, previousArgs })
caller := &productExampleCaller{}
err := runChatCoverageCommand(t, caller,
"group", "update-icon", "--group=cid", "--icon-media-id=./logo.png")
if err == nil {
t.Fatal("update group icon with local path succeeded, want validation error")
}
if caller.calls != 0 {
t.Fatalf("tool calls = %d, want 0", caller.calls)
}
}
func TestCrossPlatformCoverageChatCommandValidationAndSuccessEdges(t *testing.T) {
previousDeps, previousArgs := deps, os.Args
os.Args = []string{"dws", "chat"}
+8 -1
View File
@@ -10,6 +10,7 @@ import (
"strings"
"testing"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/spf13/cobra"
)
@@ -406,7 +407,13 @@ func TestCrossPlatformCoverageGuardGroupOwnerRemovalCoverage(t *testing.T) {
t.Run(tc.name, func(t *testing.T) {
caller := &scriptedToolCaller{steps: tc.steps}
installScriptedCaller(t, caller)
_ = guardGroupOwnerRemoval(context.Background(), "group", tc.remove)
err := guardGroupOwnerRemoval(context.Background(), "group", tc.remove)
if tc.name == "owner-open" {
var typed *apperrors.Error
if !errors.As(err, &typed) || typed.Reason != "group_owner_in_remove_list" || !strings.Contains(typed.Hint, "从 --users 中移除群主") || strings.Contains(typed.Hint, "先执行 dws chat group transfer-owner") {
t.Fatalf("owner removal hint = %v", err)
}
}
})
}
caller := &scriptedToolCaller{steps: []scriptedToolStep{{text: `{}`}}}
+24 -1
View File
@@ -4,7 +4,12 @@
package helpers
import "context"
import (
"context"
"fmt"
"path/filepath"
"strings"
)
// ConversationLocalFileMeta exposes the already-reviewed native chat upload
// metadata to built-in semantic Shortcuts. It remains an alias so the native
@@ -41,3 +46,21 @@ func BuildConversationFileContent(
) (string, error) {
return buildConversationFileContent(dentryID, spaceID, meta)
}
// ValidateChatMediaID rejects values that are deterministically not an
// uploaded DingTalk mediaId before an MCP request is dispatched.
func ValidateChatMediaID(value string) error {
value = strings.TrimSpace(value)
if value == "" {
return fmt.Errorf("mediaId 不能为空")
}
lower := strings.ToLower(value)
if strings.HasPrefix(lower, "file://") || filepath.IsAbs(value) ||
strings.ContainsAny(value, `/\\`) || strings.HasPrefix(lower, "dentry") {
return fmt.Errorf("%q 是本地文件路径或文件标识,不是 mediaId;请使用可信上游返回的 mediaId,DWS CLI 当前不能把本地图片转换为群头像 mediaId", value)
}
if value[0] != '@' && value[0] != '$' {
return fmt.Errorf("%q 不是有效 mediaId:群头像 mediaId 应以 @ 或 $ 开头;不要传本地路径、dentryId 或 uploadKey", value)
}
return nil
}
+25 -52
View File
@@ -33,6 +33,27 @@ func SetHTTPPutFile(fn func(ctx context.Context, url string, headers map[string]
httpPutFile = fn
}
func callDocCommentUpdate(toolArgs map[string]any) error {
text, err := callMCPToolReturnTextOnServer(context.Background(), "doc-comment", "update_comment", toolArgs)
if err != nil {
return err
}
trimmed := strings.TrimSpace(text)
if trimmed == "" || trimmed == "null" {
return &CLIError{
Code: CodeMCPToolError,
Message: "评论更新接口未返回可验证的更新结果,不能判定为成功",
Suggestion: "请用 dws doc comment list --node <DOC_ID> --format json 回查评论内容;若未变化,保留原 commentKey 并重试",
}
}
var payload any
if json.Unmarshal([]byte(trimmed), &payload) == nil {
return deps.Out.PrintJSON(payload)
}
deps.Out.PrintRaw(text)
return nil
}
func docVersionExists(ctx context.Context, nodeID string, version int) (bool, error) {
// 注意: 不传 maxResults —— 服务端实际接受的上限小于 schema 声明的 1-50,
// 传大值会直接报错 (与悟空实现一致: 默认分页大小 + 游标翻页)。
@@ -1070,9 +1091,6 @@ func newDocCommand() *cobra.Command {
})
}
if md != "" {
if name, ok := toolArgs["name"].(string); ok && name != "" {
md = stripDuplicateTitle(md, name)
}
toolArgs["markdown"] = md
}
if md != "" {
@@ -1652,7 +1670,8 @@ WARNING: --mode overwrite 为破坏性写入,会清空原文档全部内容。
updateCmd.Flags().String("markdown", "", "已弃用,请使用 --content 代替")
_ = updateCmd.Flags().MarkHidden("markdown")
updateCmd.Flags().String("mode", "", "更新模式: overwrite=覆盖, append=追加 (必填)")
_ = updateCmd.MarkFlagRequired("mode")
// Kept out of Cobra's generic required-flag validator so doc local
// preflight can return a structured, actionable error before MCP dispatch.
updateCmd.Flags().Int("index", -1, "插入位置(从 0 开始),仅在 mode=append 时生效。指定将内容插入到文档第几个 block 之前。不传时追加到末尾")
updateCmd.Flags().Bool("yes", false, "确认执行破坏性写入 (仅 --mode overwrite 需要)")
updateCmd.Flags().Bool("dry-run", false, "预览覆盖写入差异,不调用远端 update")
@@ -2046,7 +2065,7 @@ commentKey可从 dws doc comment create 或 dws doc comment list 返回结果中
if err := appendCommentGroupMentions(cmd, toolArgs); err != nil {
return err
}
return callMCPToolOnServer("doc-comment", "update_comment", toolArgs)
return callDocCommentUpdate(toolArgs)
},
}
commentUpdateCmd.Flags().String("node", "", "目标文档的标识,支持传入 URL 或 ID (必填)")
@@ -2902,6 +2921,7 @@ CLI 内部自动完成全部流程:
permissionCmd.Hidden = true
root.AddCommand(searchCmd, listCmd, infoCmd, readCmd, createCmd, updateCmd, uploadCmd, downloadCmd, copyCmd, moveCmd, renameCmd, deleteCmd, fileCmd, folderCmd, blockCmd, commentCmd, mediaCmd, permissionCmd, exportCmd, importCmd, versionCmd, templateCmd, newDocStyleCommand())
attachDocLocalPreflight(root)
return root
}
@@ -3218,53 +3238,6 @@ func pollDocExportJob(ctx context.Context, jobID string) (downloadURL string, er
return "", fmt.Errorf("导出任务超时:已轮询 %d 次仍在处理中 (jobId=%s),请稍后使用 dws doc export get --job-id %s 手动查询", maxPolls, jobID, jobID)
}
// stripDuplicateTitle removes the leading H1 heading from markdown content
// when it matches the document name (set via --name). This prevents the title
// from appearing twice: once as document metadata and once in the body.
func stripDuplicateTitle(markdown, name string) string {
trimmed := strings.TrimLeft(markdown, " \t\n\r")
if !strings.HasPrefix(trimmed, "# ") {
return markdown
}
newlineIdx := strings.Index(trimmed, "\n")
var headingRaw string
if newlineIdx < 0 {
headingRaw = trimmed[2:]
} else {
headingRaw = trimmed[2:newlineIdx]
}
if normalizeHeadingText(headingRaw) != normalizeHeadingText(name) {
return markdown
}
if newlineIdx < 0 {
return ""
}
rest := trimmed[newlineIdx+1:]
rest = strings.TrimLeft(rest, "\n")
return rest
}
// normalizeHeadingText strips trailing ATX hashes, inline markdown formatting
// markers, then returns a lowercased, trimmed string for comparison.
func normalizeHeadingText(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
if i := strings.LastIndexByte(s, ' '); i >= 0 {
suffix := s[i+1:]
if len(suffix) > 0 && strings.Trim(suffix, "#") == "" {
s = strings.TrimSpace(s[:i])
}
}
for _, m := range []string{"**", "__", "~~", "*", "_", "`"} {
s = strings.ReplaceAll(s, m, "")
}
return strings.TrimSpace(strings.ToLower(s))
}
// parseCommentMentionIds splits a comma-separated string of user IDs into a slice.
func parseCommentMentionIds(raw string) []string {
parts := strings.Split(raw, ",")
@@ -295,12 +295,6 @@ func TestCrossPlatformCoverageDocCreateUpdateAndBlockCommandEdges(t *testing.T)
})
}
for _, value := range []string{"plain", "# Other\nbody", "# Name", "# **Name** ###\n\nbody"} {
_ = stripDuplicateTitle(value, "Name")
}
for _, value := range []string{"", " Name ### ", "**Bold**", "__Under__ ~~Strike~~ `Code`"} {
_ = normalizeHeadingText(value)
}
for _, name := range []string{"file.pdf", "file.md", "file.unknown"} {
_ = inferMimeType(name)
}
@@ -18,6 +18,7 @@ import (
"io"
"os"
"reflect"
"strings"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
@@ -30,12 +31,29 @@ type docCommentMutationCall struct {
}
type docCommentMutationCaller struct {
calls []docCommentMutationCall
calls []docCommentMutationCall
response string
}
func (c *docCommentMutationCaller) CallTool(_ context.Context, productID, toolName string, args map[string]any) (*edition.ToolResult, error) {
c.calls = append(c.calls, docCommentMutationCall{productID: productID, toolName: toolName, args: args})
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: `{}`}}}, nil
response := c.response
if response == "" {
response = `{}`
}
return &edition.ToolResult{Content: []edition.ContentBlock{{Type: "text", Text: response}}}, nil
}
func TestDocCommentUpdateRejectsNullAcknowledgement(t *testing.T) {
caller := &docCommentMutationCaller{response: `null`}
err := executeDocCommentMutationCommand(t, caller, []string{"dws", "doc"},
"comment", "update", "--node", "doc-1", "--comment-key", "comment-1", "--content", "updated")
if err == nil || !strings.Contains(err.Error(), "未返回可验证的更新结果") {
t.Fatalf("error = %v, want unverifiable update error", err)
}
if len(caller.calls) != 1 {
t.Fatalf("remote calls = %d, want 1", len(caller.calls))
}
}
func (*docCommentMutationCaller) Format() string { return "json" }
@@ -0,0 +1,74 @@
package helpers
import (
"context"
"io"
"os"
"testing"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/pkg/edition"
)
type docCreateRecordingCall struct {
tool string
args map[string]any
}
type docCreateRecordingCaller struct {
calls []docCreateRecordingCall
}
func (c *docCreateRecordingCaller) CallTool(_ context.Context, _ string, tool string, args map[string]any) (*edition.ToolResult, error) {
copied := make(map[string]any, len(args))
for key, value := range args {
copied[key] = value
}
c.calls = append(c.calls, docCreateRecordingCall{tool: tool, args: copied})
return textToolResult(`{"nodeId":"node-1","success":true}`), nil
}
func (*docCreateRecordingCaller) Format() string { return "json" }
func (*docCreateRecordingCaller) DryRun() bool { return false }
func (*docCreateRecordingCaller) Fields() string { return "" }
func (*docCreateRecordingCaller) JQ() string { return "" }
func TestDocCreatePreservesExplicitLeadingH1MatchingName(t *testing.T) {
oldArgs := os.Args
os.Args = []string{"dws", "doc"}
t.Cleanup(func() { os.Args = oldArgs })
for _, content := range []string{
"# 需求清单",
"# 需求清单\n\n以上需求已与产品确认",
} {
t.Run(content, func(t *testing.T) {
previous := deps
caller := &docCreateRecordingCaller{}
InitDeps(caller)
deps.Out.w = io.Discard
deps.Out.errW = io.Discard
t.Cleanup(func() { deps = previous })
root := newDocCommand()
root.SilenceErrors = true
root.SilenceUsage = true
root.SetOut(io.Discard)
root.SetErr(io.Discard)
root.SetArgs([]string{"create", "--name", "需求清单", "--content", content})
if err := root.ExecuteContext(context.Background()); err != nil {
t.Fatal(err)
}
if len(caller.calls) != 1 {
t.Fatalf("tool calls = %#v, want one create_document call", caller.calls)
}
call := caller.calls[0]
if call.tool != "create_document" {
t.Fatalf("tool = %q, want create_document", call.tool)
}
if got := call.args["markdown"]; got != content {
t.Fatalf("markdown = %#v, want exact explicit body H1 %#v", got, content)
}
})
}
}
+446
View File
@@ -0,0 +1,446 @@
package helpers
import (
"fmt"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
)
// attachDocLocalPreflight wraps every doc leaf before its RunE reaches MCP.
// Only facts provable from local argv/filesystem state belong here; resource
// existence, permissions and business state remain server-owned.
func attachDocLocalPreflight(root *cobra.Command) {
var walk func(*cobra.Command)
walk = func(cmd *cobra.Command) {
for _, child := range cmd.Commands() {
walk(child)
}
if cmd.RunE == nil {
return
}
original := cmd.RunE
cmd.RunE = func(c *cobra.Command, args []string) error {
if err := validateDocLocalArgs(c); err != nil {
return err
}
return original(c, args)
}
}
walk(root)
}
func docLocalError(cmd *cobra.Command, code, message, suggestion string) error {
return &CLIError{Code: code, Message: message, Suggestion: suggestion, Operation: strings.TrimPrefix(cmd.CommandPath(), "dws ")}
}
func docRequire(cmd *cobra.Command, example string, names ...string) error {
missing := make([]string, 0, len(names))
for _, name := range names {
flag := cmd.Flags().Lookup(name)
if flag == nil || strings.TrimSpace(flag.Value.String()) == "" {
missing = append(missing, "--"+name)
}
}
if len(missing) == 0 {
return nil
}
return docLocalError(cmd, CodeMissingParam, "缺少必填参数: "+strings.Join(missing, ", "), "示例: "+example)
}
func docRequireNode(cmd *cobra.Command, example string) error {
if flagOrFallback(cmd, "node", "url", "id", "node-id", "doc-id", "file-id") != "" {
return nil
}
return docLocalError(cmd, CodeMissingParam, "缺少目标文档 --node(可传文档 URL 或 nodeId)", "先用 dws drive search 或 dws wiki node search 获取 nodeId。示例: "+example)
}
func docValidateLocalFile(cmd *cobra.Command, flagName, example string) error {
path, _ := cmd.Flags().GetString(flagName)
if strings.TrimSpace(path) == "" {
return docRequire(cmd, example, flagName)
}
info, err := os.Stat(path)
if err != nil {
return docLocalError(cmd, CodeFileNotFound, fmt.Sprintf("本地文件 %q 不可读取", path), "检查路径后重试。示例: "+example)
}
if info.IsDir() {
return docLocalError(cmd, CodeInvalidPath, fmt.Sprintf("%q 是目录,不是文件", path), "请传入具体文件路径。示例: "+example)
}
return nil
}
func docValidateWhere(cmd *cobra.Command, example string) error {
where, _ := cmd.Flags().GetString("where")
ref, _ := cmd.Flags().GetString("ref-block")
if where != "" && where != "before" && where != "after" {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--where %q 无效,仅支持 before 或 after", where), "示例: "+example)
}
if where != "" && ref == "" {
return docLocalError(cmd, CodeMissingParam, "使用 --where 时必须同时提供 --ref-block", "先用 dws doc block list --node <DOC_ID> 获取 blockId。示例: "+example)
}
return nil
}
func docValidateEnum(cmd *cobra.Command, name string, allowed []string, example string) error {
value, _ := cmd.Flags().GetString(name)
if value == "" {
return nil
}
for _, candidate := range allowed {
if strings.EqualFold(value, candidate) {
return nil
}
}
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--%s %q 无效,仅支持 %s", name, value, strings.Join(allowed, "、")), "示例: "+example)
}
func docValidateLimit(cmd *cobra.Command, max int, example string) error {
for _, name := range []string{"limit", "page-size", "max-results"} {
if flag := cmd.Flags().Lookup(name); flag != nil && cmd.Flags().Changed(name) {
value, err := cmd.Flags().GetInt(name)
if err == nil && (value < 1 || value > max) {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("--%s 必须在 1 到 %d 之间", name, max), "示例: "+example)
}
}
}
return nil
}
func docValidateBlockContent(cmd *cobra.Command, example string) error {
changed := make([]string, 0, 3)
for _, name := range []string{"text", "heading", "element"} {
if flag := cmd.Flags().Lookup(name); flag != nil && cmd.Flags().Changed(name) && strings.TrimSpace(flag.Value.String()) != "" {
changed = append(changed, "--"+name)
}
}
if len(changed) == 0 {
return docLocalError(cmd, CodeMissingParam, "必须提供一种块内容:--text、--heading 或 --element", "示例: "+example)
}
if len(changed) > 1 {
return docLocalError(cmd, CodeInvalidParam, "块内容参数不能同时使用: "+strings.Join(changed, ", "), "三选一。示例: "+example)
}
format, _ := cmd.Flags().GetString("content-format")
if format == "jsonml" && changed[0] != "--element" {
return docLocalError(cmd, CodeInvalidParam, "--content-format jsonml 必须通过 --element 提供 JSONML 节点", "示例: "+example)
}
return nil
}
func docValidatePermissionUsers(cmd *cobra.Command, example string) error {
raw := flagOrFallback(cmd, "users", "user")
users := parseCommentMentionIds(raw)
if len(users) == 0 {
return docLocalError(cmd, CodeMissingParam, "缺少至少一个用户 ID:--users", "示例: "+example)
}
if len(users) > 30 {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("单次最多处理 30 个用户,当前为 %d 个", len(users)), "请拆分为多次调用")
}
return nil
}
func validateDocLocalArgs(cmd *cobra.Command) error {
path := strings.TrimPrefix(cmd.CommandPath(), "dws ")
switch path {
case "doc info":
return docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --format json")
case "doc search":
if err := docValidateLimit(cmd, 30, "dws doc search --query \"周报\" --limit 10"); err != nil {
return err
}
createdFrom, _ := cmd.Flags().GetInt64("created-from")
createdTo, _ := cmd.Flags().GetInt64("created-to")
visitedFrom, _ := cmd.Flags().GetInt64("visited-from")
visitedTo, _ := cmd.Flags().GetInt64("visited-to")
if createdFrom < 0 || createdTo < 0 || visitedFrom < 0 || visitedTo < 0 {
return docLocalError(cmd, CodeInvalidParam, "时间过滤值必须是非负毫秒时间戳", "示例: dws doc search --created-from 1700000000000 --created-to 1710000000000")
}
if cmd.Flags().Changed("created-from") && cmd.Flags().Changed("created-to") && createdFrom > createdTo {
return docLocalError(cmd, CodeInvalidParam, "--created-from 不能晚于 --created-to", "请交换起止时间")
}
if cmd.Flags().Changed("visited-from") && cmd.Flags().Changed("visited-to") && visitedFrom > visitedTo {
return docLocalError(cmd, CodeInvalidParam, "--visited-from 不能晚于 --visited-to", "请交换起止时间")
}
case "doc list":
return docValidateLimit(cmd, 50, "dws doc list --workspace <WORKSPACE_ID> --limit 50")
case "doc read":
if err := docRequireNode(cmd, "dws doc read --node <DOC_ID> --format json"); err != nil {
return err
}
if depth, _ := cmd.Flags().GetInt("max-depth"); depth < 0 {
return docLocalError(cmd, CodeInvalidParam, "--max-depth 不能为负数", "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope outline --max-depth 3")
}
if scopeValue, _ := cmd.Flags().GetString("scope"); scopeValue != "" {
valid := false
for _, candidate := range []string{"outline", "range", "section", "tags"} {
if scopeValue == candidate {
valid = true
}
}
if !valid {
return docLocalError(cmd, CodeInvalidParam, fmt.Sprintf("invalid --scope %q: must be one of outline|range|section|tags", scopeValue), "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope outline")
}
}
format, _ := cmd.Flags().GetString("content-format")
scope, _ := cmd.Flags().GetString("scope")
tags, _ := cmd.Flags().GetString("tags")
startBlockID, _ := cmd.Flags().GetString("start-block-id")
if (scope == "range" || scope == "section") && strings.TrimSpace(startBlockID) == "" {
return docLocalError(cmd, CodeMissingParam, "--scope "+scope+" 必须提供 --start-block-id", "先用 dws doc block list --node <DOC_ID> --format json 获取真实 blockId")
}
if cmd.Flags().Changed("output") && format != "jsonml" {
return docLocalError(cmd, CodeInvalidParam, "--output 仅支持 --content-format jsonml", "Markdown 内容会直接显示在终端;如需保存为文件,请执行: dws doc read --node <DOC_ID> --content-format markdown --format raw > body.md")
}
if (scope != "" || tags != "") && format != "jsonml" {
return docLocalError(cmd, CodeInvalidParam, "--scope/--tags requires --content-format jsonml", "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope tags --tags h1,h2")
}
if scope == "tags" && strings.TrimSpace(tags) == "" {
return docLocalError(cmd, CodeMissingParam, "--tags is required when --scope=tags", "示例: dws doc read --node <DOC_ID> --content-format jsonml --scope tags --tags h1,h2")
}
if scope != "tags" && tags != "" {
if scope == "" {
return docLocalError(cmd, CodeInvalidParam, "--tags requires --scope tags", "移除 --tags 或改用 --scope tags")
}
return docLocalError(cmd, CodeInvalidParam, "--tags only works with --scope tags", "移除 --tags 或改用 --scope tags")
}
case "doc create":
if strings.TrimSpace(flagOrFallback(cmd, "name", "title")) == "" {
return docLocalError(cmd, CodeMissingParam, "缺少文档名称 --name", "示例: dws doc create --name \"项目周报\" --format json")
}
if cmd.Flags().Changed("content") && cmd.Flags().Changed("content-file") {
return docLocalError(cmd, CodeInvalidParam, "--content 与 --content-file 不能同时使用", "短文本用 --content;长文本或表格用 --content-file")
}
if cmd.Flags().Changed("content-file") {
return docValidateLocalFile(cmd, "content-file", "dws doc create --name \"周报\" --content-file ./weekly.md")
}
case "doc update":
if err := docRequireNode(cmd, "dws doc update --node <DOC_ID> --content \"追加内容\" --mode append"); err != nil {
return err
}
if cmd.Flags().Changed("content") && cmd.Flags().Changed("content-file") {
return docLocalError(cmd, CodeInvalidParam, "--content 与 --content-file 不能同时使用", "二选一;长内容优先 --content-file")
}
if cmd.Flags().Changed("content-file") {
if err := docValidateLocalFile(cmd, "content-file", "dws doc update --node <DOC_ID> --content-file ./body.md --mode append"); err != nil {
return err
}
}
mode, _ := cmd.Flags().GetString("mode")
if mode != "append" && mode != "overwrite" {
return docLocalError(cmd, CodeInvalidParam, "--mode 必须是 append 或 overwrite", "追加优先使用: dws doc update --node <DOC_ID> --content \"内容\" --mode append")
}
if idx, _ := cmd.Flags().GetInt("index"); cmd.Flags().Changed("index") && (mode != "append" || idx < 0) {
return docLocalError(cmd, CodeInvalidParam, "--index 仅支持 mode=append 且必须大于等于 0", "示例: dws doc update --node <DOC_ID> --content \"内容\" --mode append --index 0")
}
content := flagOrFallback(cmd, "content", "markdown")
if strings.TrimSpace(content) == "" && !cmd.Flags().Changed("content-file") {
return docLocalError(cmd, CodeMissingParam, "必须通过 --content 或 --content-file 提供非空内容", "示例: dws doc update --node <DOC_ID> --content \"追加内容\" --mode append")
}
dryRun, _ := cmd.Flags().GetBool("dry-run")
yes, _ := cmd.Flags().GetBool("yes")
if dryRun && mode != "overwrite" {
return docLocalError(cmd, CodeInvalidParam, "--dry-run 仅用于预览 overwrite 覆盖写入", "append 本身不覆盖全文,请移除 --dry-run")
}
if yes && mode != "overwrite" {
return docLocalError(cmd, CodeInvalidParam, "--yes 仅用于确认 overwrite 覆盖写入", "append 无需 --yes,请移除该参数")
}
if mode == "overwrite" && !dryRun && !yes {
return docLocalError(cmd, CodeInvalidParam, "--mode overwrite 必须加 --yes 确认,或使用 --dry-run 预览", "示例: dws doc update --node <DOC_ID> --content-file ./body.md --mode overwrite --dry-run")
}
case "doc block list":
if err := docRequireNode(cmd, "dws doc block list --node <DOC_ID> --format json"); err != nil {
return err
}
start, _ := cmd.Flags().GetInt("start-index")
end, _ := cmd.Flags().GetInt("end-index")
if start < 0 || end < 0 || (cmd.Flags().Changed("end-index") && end < start) {
return docLocalError(cmd, CodeInvalidParam, "块索引必须非负,且 --end-index 不能小于 --start-index", "示例: dws doc block list --node <DOC_ID> --start-index 0 --end-index 5")
}
case "doc block insert":
if err := docRequireNode(cmd, "dws doc block insert --node <DOC_ID> --text \"内容\""); err != nil {
return err
}
if err := docValidateWhere(cmd, "dws doc block insert --node <DOC_ID> --text \"内容\" --ref-block <BLOCK_ID> --where after"); err != nil {
return err
}
if level, _ := cmd.Flags().GetInt("level"); level < 1 || level > 6 {
return docLocalError(cmd, CodeInvalidParam, "--level 必须在 1 到 6 之间", "示例: dws doc block insert --node <DOC_ID> --heading \"标题\" --level 2")
}
if idx, _ := cmd.Flags().GetInt("index"); cmd.Flags().Changed("index") && idx < 0 {
return docLocalError(cmd, CodeInvalidParam, "--index 不能为负数", "示例: dws doc block insert --node <DOC_ID> --text \"内容\" --index 0")
}
return docValidateBlockContent(cmd, "dws doc block insert --node <DOC_ID> --text \"内容\"")
case "doc block update":
if err := docRequireNode(cmd, "dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\""); err != nil {
return err
}
if err := docRequire(cmd, "dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\"", "block-id"); err != nil {
return err
}
if level, _ := cmd.Flags().GetInt("level"); level < 1 || level > 6 {
return docLocalError(cmd, CodeInvalidParam, "--level 必须在 1 到 6 之间", "示例: dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --heading \"标题\" --level 2")
}
return docValidateBlockContent(cmd, "dws doc block update --node <DOC_ID> --block-id <BLOCK_ID> --text \"新内容\"")
case "doc block delete":
if err := docRequireNode(cmd, "dws doc block delete --node <DOC_ID> --block-id <BLOCK_ID> --yes"); err != nil {
return err
}
return docRequire(cmd, "dws doc block delete --node <DOC_ID> --block-id <BLOCK_ID> --yes", "block-id")
case "doc media download":
if err := docRequireNode(cmd, "dws doc media download --node <DOC_ID> --resource-id <RESOURCE_ID>"); err != nil {
return err
}
return docRequire(cmd, "dws doc media download --node <DOC_ID> --resource-id <RESOURCE_ID>", "resource-id")
case "doc media insert":
if err := docRequireNode(cmd, "dws doc media insert --node <DOC_ID> --file ./report.pdf"); err != nil {
return err
}
if err := docValidateWhere(cmd, "dws doc media insert --node <DOC_ID> --file ./report.pdf --ref-block <BLOCK_ID> --where after"); err != nil {
return err
}
return docValidateLocalFile(cmd, "file", "dws doc media insert --node <DOC_ID> --file ./report.pdf")
case "doc comment list":
if err := docRequireNode(cmd, "dws doc comment list --node <DOC_ID> --format json"); err != nil {
return err
}
if err := docValidateLimit(cmd, 50, "dws doc comment list --node <DOC_ID> --limit 50"); err != nil {
return err
}
if err := docValidateEnum(cmd, "type", []string{"global", "inline"}, "dws doc comment list --node <DOC_ID> --type inline"); err != nil {
return err
}
return docValidateEnum(cmd, "resolve-status", []string{"resolved", "unresolved"}, "dws doc comment list --node <DOC_ID> --resolve-status unresolved")
case "doc comment create":
if err := docRequireNode(cmd, "dws doc comment create --node <DOC_ID> --content \"评论\""); err != nil {
return err
}
return docRequire(cmd, "dws doc comment create --node <DOC_ID> --content \"评论\"", "content")
case "doc comment reply", "doc comment update":
if err := docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"内容\""); err != nil {
return err
}
if err := docRequire(cmd, "dws "+path+" --node <DOC_ID> --comment-key <COMMENT_KEY> --content \"内容\"", "comment-key", "content"); err != nil {
return err
}
if path == "doc comment reply" {
emoji, _ := cmd.Flags().GetBool("emoji")
groups, groupErr := commentGroupMentionIDs(cmd)
if groupErr != nil {
return groupErr
}
if emoji && len(groups) > 0 {
return docLocalError(cmd, CodeInvalidParam, "--emoji cannot be used with --mentioned-open-conversation-id: emoji replies do not support group mentions", "表情回复请移除群 @;需要 @群时使用普通文字回复")
}
}
case "doc comment delete":
if err := docRequireNode(cmd, "dws doc comment delete --node <DOC_ID> --comment-key <COMMENT_KEY> --yes"); err != nil {
return err
}
return docRequire(cmd, "dws doc comment delete --node <DOC_ID> --comment-key <COMMENT_KEY> --yes", "comment-key")
case "doc comment create-inline":
if err := docRequireNode(cmd, "dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 5 --content \"评论\""); err != nil {
return err
}
if err := docRequire(cmd, "dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 5 --content \"评论\"", "block-id", "content"); err != nil {
return err
}
start, _ := cmd.Flags().GetInt("start")
end, _ := cmd.Flags().GetInt("end")
if !cmd.Flags().Changed("start") || !cmd.Flags().Changed("end") || start < 0 || end <= start {
return docLocalError(cmd, CodeInvalidParam, "划词范围必须显式提供 --start/--end,且满足 0 <= start < end", "先读取块文本确认字符偏移。示例: dws doc comment create-inline --node <DOC_ID> --block-id <BLOCK_ID> --start 0 --end 5 --content \"评论\"")
}
case "doc export":
if err := docRequireNode(cmd, "dws doc export --node <DOC_ID> --export-format pdf --output ./report.pdf"); err != nil {
return err
}
if err := docRequire(cmd, "dws doc export --node <DOC_ID> --export-format pdf --output ./report.pdf", "output"); err != nil {
return err
}
return docValidateEnum(cmd, "export-format", []string{"docx", "markdown", "md", "pdf"}, "dws doc export --node <DOC_ID> --export-format pdf --output ./report.pdf")
case "doc export get":
return docRequire(cmd, "dws doc export get --job-id <JOB_ID>", "job-id")
case "doc import":
if err := docValidateLocalFile(cmd, "file", "dws doc import --file ./report.docx --workspace <WORKSPACE_ID>"); err != nil {
return err
}
if !deps.Caller.DryRun() && flagOrFallback(cmd, "folder", "folder-id") == "" && flagOrFallback(cmd, "workspace", "workspace-id") == "" {
return docLocalError(cmd, CodeMissingParam, "导入文档必须提供 --folder 或 --workspace 作为目标位置", "先用 dws wiki space list --type myWikiSpace --format json 获取 workspaceId")
}
case "doc import get":
return docRequire(cmd, "dws doc import get --task-id <TASK_ID>", "task-id")
case "doc version save", "doc version list", "doc version revert":
if err := docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --format json"); err != nil {
return err
}
if path == "doc version revert" && (!cmd.Flags().Changed("version")) {
return docLocalError(cmd, CodeMissingParam, "缺少目标版本 --version", "先用 dws doc version list --node <DOC_ID> 获取真实版本号")
}
if path == "doc version revert" {
version, _ := cmd.Flags().GetInt("version")
if version < 1 {
return docLocalError(cmd, CodeInvalidParam, "--version 必须是大于 0 的真实版本号", "先用 dws doc version list --node <DOC_ID> 获取版本号")
}
}
if path == "doc version list" {
return docValidateLimit(cmd, 50, "dws doc version list --node <DOC_ID> --limit 10")
}
case "doc permission add", "doc permission update":
if err := docRequireNode(cmd, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR"); err != nil {
return err
}
if err := docValidatePermissionUsers(cmd, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR"); err != nil {
return err
}
if err := docRequire(cmd, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR", "role"); err != nil {
return err
}
return docValidateEnum(cmd, "role", []string{"MANAGER", "EDITOR", "DOWNLOADER", "READER"}, "dws "+path+" --node <DOC_ID> --users uid1 --role EDITOR")
case "doc permission remove":
if err := docRequireNode(cmd, "dws doc permission remove --node <DOC_ID> --users uid1"); err != nil {
return err
}
return docValidatePermissionUsers(cmd, "dws doc permission remove --node <DOC_ID> --users uid1")
case "doc permission list":
if err := docRequireNode(cmd, "dws doc permission list --node <DOC_ID> --limit 30"); err != nil {
return err
}
if err := docValidateLimit(cmd, 200, "dws doc permission list --node <DOC_ID> --limit 30"); err != nil {
return err
}
if roles, _ := cmd.Flags().GetString("filter-role"); roles != "" {
for _, role := range parseRoleList(roles) {
valid := false
for _, candidate := range []string{"OWNER", "MANAGER", "EDITOR", "DOWNLOADER", "READER"} {
if role == candidate {
valid = true
}
}
if !valid {
return docLocalError(cmd, CodeInvalidParam, "--filter-role 包含非法角色 "+role, "仅支持 OWNER、MANAGER、EDITOR、DOWNLOADER、READER")
}
}
}
case "doc template list":
if err := docValidateEnum(cmd, "source", []string{"MY", "PUBLIC"}, "dws doc template list --source MY"); err != nil {
return err
}
return docValidateLimit(cmd, 50, "dws doc template list --limit 20")
case "doc template search":
if flagOrFallback(cmd, "query", "keyword", "name") == "" {
return docLocalError(cmd, CodeMissingParam, "缺少非空模板关键词 --query", "示例: dws doc template search --query \"周报\" --format json")
}
if err := docValidateEnum(cmd, "source", []string{"MY", "PUBLIC"}, "dws doc template search --query \"周报\" --source PUBLIC"); err != nil {
return err
}
return docValidateLimit(cmd, 50, "dws doc template search --query \"周报\" --limit 20")
case "doc template apply":
if flagOrFallback(cmd, "template-id", "template", "tpl-id") == "" {
return docLocalError(cmd, CodeMissingParam, "缺少模板 ID --template-id", "先用 dws doc template search --query \"关键词\" 获取真实 templateId")
}
}
return nil
}
func docLocalFileExtension(path string) string { return strings.ToLower(filepath.Ext(path)) }
@@ -0,0 +1,137 @@
package helpers
import (
"errors"
"strings"
"testing"
)
func TestDocLocalPreflightRejectsBeforeMCP(t *testing.T) {
cases := []struct {
name string
args []string
want string
}{
{"info-node", []string{"info"}, "--node"},
{"read-node", []string{"read"}, "--node"},
{"create-name", []string{"create"}, "--name"},
{"create-content-conflict", []string{"create", "--name", "x", "--content", "a", "--content-file", "b"}, "不能同时"},
{"create-file-missing", []string{"create", "--name", "x", "--content-file", "/definitely/not/found.md"}, "不可读取"},
{"update-node", []string{"update", "--content", "x", "--mode", "append"}, "--node"},
{"update-content-conflict", []string{"update", "--node", "n", "--content", "a", "--content-file", "b", "--mode", "append"}, "不能同时"},
{"update-mode-missing", []string{"update", "--node", "n", "--content", "a"}, "--mode"},
{"update-mode-invalid", []string{"update", "--node", "n", "--content", "a", "--mode", "merge"}, "append 或 overwrite"},
{"update-index-mode", []string{"update", "--node", "n", "--content", "a", "--mode", "overwrite", "--index", "1"}, "--index"},
{"block-list-node", []string{"block", "list"}, "--node"},
{"block-list-range", []string{"block", "list", "--node", "n", "--start-index", "4", "--end-index", "2"}, "不能小于"},
{"block-insert-node", []string{"block", "insert", "--text", "x"}, "--node"},
{"block-insert-where", []string{"block", "insert", "--node", "n", "--text", "x", "--where", "middle"}, "before 或 after"},
{"block-insert-ref", []string{"block", "insert", "--node", "n", "--text", "x", "--where", "before"}, "--ref-block"},
{"block-insert-level", []string{"block", "insert", "--node", "n", "--heading", "x", "--level", "7"}, "1 到 6"},
{"block-update-node", []string{"block", "update", "--block-id", "b", "--text", "x"}, "--node"},
{"block-update-id", []string{"block", "update", "--node", "n", "--text", "x"}, "--block-id"},
{"block-delete-id", []string{"block", "delete", "--node", "n"}, "--block-id"},
{"media-download-resource", []string{"media", "download", "--node", "n"}, "--resource-id"},
{"media-insert-file", []string{"media", "insert", "--node", "n"}, "--file"},
{"comment-create-content", []string{"comment", "create", "--node", "n"}, "--content"},
{"comment-reply-key", []string{"comment", "reply", "--node", "n", "--content", "x"}, "--comment-key"},
{"comment-update-content", []string{"comment", "update", "--node", "n", "--comment-key", "k"}, "--content"},
{"comment-delete-key", []string{"comment", "delete", "--node", "n"}, "--comment-key"},
{"inline-block", []string{"comment", "create-inline", "--node", "n", "--content", "x", "--start", "0", "--end", "1"}, "--block-id"},
{"inline-range", []string{"comment", "create-inline", "--node", "n", "--block-id", "b", "--content", "x", "--start", "2", "--end", "1"}, "start < end"},
{"export-output", []string{"export", "--node", "n"}, "--output"},
{"export-job", []string{"export", "get"}, "--job-id"},
{"import-file", []string{"import", "--workspace", "w"}, "--file"},
{"import-target", []string{"import", "--file", "/definitely/not/found.docx"}, "不可读取"},
{"import-task", []string{"import", "get"}, "--task-id"},
{"version-node", []string{"version", "list"}, "--node"},
{"version-number", []string{"version", "revert", "--node", "n"}, "--version"},
{"template-query", []string{"template", "search"}, "--query"},
{"template-id", []string{"template", "apply"}, "--template-id"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := newDocCommand()
cmd.SetArgs(tc.args)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want containing %q", err, tc.want)
}
var cliErr *CLIError
if !errors.As(err, &cliErr) || cliErr.ExitCode() != ExitValidation {
t.Fatalf("error = %#v, want validation CLIError", err)
}
})
}
}
func TestDocLocalPreflightSecondBatchRejectsBeforeMCP(t *testing.T) {
users31 := strings.TrimSuffix(strings.Repeat("u,", 31), ",")
cases := []struct {
name string
args []string
want string
}{
{"search-created-negative", []string{"search", "--created-from", "-1"}, "非负毫秒"},
{"search-created-range", []string{"search", "--created-from", "20", "--created-to", "10"}, "created-from"},
{"search-visited-negative", []string{"search", "--visited-to", "-1"}, "非负毫秒"},
{"search-visited-range", []string{"search", "--visited-from", "20", "--visited-to", "10"}, "visited-from"},
{"search-limit-zero", []string{"search", "--limit", "0"}, "1 到 30"},
{"search-limit-large", []string{"search", "--limit", "31"}, "1 到 30"},
{"list-limit-zero", []string{"list", "--limit", "0"}, "1 到 50"},
{"list-limit-large", []string{"list", "--limit", "51"}, "1 到 50"},
{"read-depth-negative", []string{"read", "--node", "n", "--max-depth", "-1"}, "不能为负"},
{"read-scope-invalid", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "all"}, "outline"},
{"read-scope-format", []string{"read", "--node", "n", "--scope", "outline"}, "content-format jsonml"},
{"read-tags-missing", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "tags"}, "--tags"},
{"read-tags-wrong-scope", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "outline", "--tags", "h1"}, "--tags only works"},
{"read-range-start-missing", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "range"}, "--start-block-id"},
{"read-section-start-missing", []string{"read", "--node", "n", "--content-format", "jsonml", "--scope", "section"}, "--start-block-id"},
{"read-markdown-output", []string{"read", "--node", "n", "--content-format", "markdown", "--output", "body.json"}, "Markdown 内容会直接显示在终端"},
{"create-whitespace-name", []string{"create", "--name", " "}, "--name"},
{"update-empty-content", []string{"update", "--node", "n", "--content", " ", "--mode", "append"}, "非空内容"},
{"update-dry-run-append", []string{"update", "--node", "n", "--content", "x", "--mode", "append", "--dry-run"}, "仅用于预览 overwrite"},
{"update-yes-append", []string{"update", "--node", "n", "--content", "x", "--mode", "append", "--yes"}, "仅用于确认 overwrite"},
{"update-overwrite-confirm", []string{"update", "--node", "n", "--content", "x", "--mode", "overwrite"}, "必须加 --yes"},
{"block-insert-content", []string{"block", "insert", "--node", "n"}, "必须提供一种块内容"},
{"block-insert-text-heading", []string{"block", "insert", "--node", "n", "--text", "x", "--heading", "h"}, "不能同时"},
{"block-insert-heading-element", []string{"block", "insert", "--node", "n", "--heading", "h", "--element", "{}"}, "不能同时"},
{"block-insert-jsonml-text", []string{"block", "insert", "--node", "n", "--content-format", "jsonml", "--text", "x"}, "必须通过 --element"},
{"block-insert-index", []string{"block", "insert", "--node", "n", "--text", "x", "--index", "-1"}, "--index 不能"},
{"block-update-content", []string{"block", "update", "--node", "n", "--block-id", "b"}, "必须提供一种块内容"},
{"block-update-text-element", []string{"block", "update", "--node", "n", "--block-id", "b", "--text", "x", "--element", "{}"}, "不能同时"},
{"block-update-level", []string{"block", "update", "--node", "n", "--block-id", "b", "--heading", "h", "--level", "0"}, "1 到 6"},
{"comment-limit-zero", []string{"comment", "list", "--node", "n", "--limit", "0"}, "1 到 50"},
{"comment-limit-large", []string{"comment", "list", "--node", "n", "--limit", "51"}, "1 到 50"},
{"comment-type", []string{"comment", "list", "--node", "n", "--type", "all"}, "global"},
{"comment-status", []string{"comment", "list", "--node", "n", "--resolve-status", "open"}, "resolved"},
{"reply-emoji-group", []string{"comment", "reply", "--node", "n", "--comment-key", "k", "--content", "x", "--emoji", "--mentioned-open-conversation-id", "g"}, "emoji replies do not support group mentions"},
{"permission-users-missing", []string{"permission", "add", "--node", "n", "--role", "READER"}, "--users"},
{"permission-users-large", []string{"permission", "add", "--node", "n", "--role", "READER", "--users", users31}, "最多处理 30"},
{"permission-role", []string{"permission", "add", "--node", "n", "--role", "OWNER", "--users", "u"}, "MANAGER"},
{"permission-filter-role", []string{"permission", "list", "--node", "n", "--filter-role", "ADMIN"}, "非法角色"},
{"export-format", []string{"export", "--node", "n", "--output", "x", "--export-format", "html"}, "docx"},
}
if len(cases) != 39 {
t.Fatalf("second batch has %d cases, want 39", len(cases))
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
cmd := newDocCommand()
cmd.SetArgs(tc.args)
cmd.SilenceUsage = true
cmd.SilenceErrors = true
err := cmd.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("error = %v, want containing %q", err, tc.want)
}
var cliErr *CLIError
if !errors.As(err, &cliErr) || cliErr.ExitCode() != ExitValidation {
t.Fatalf("error = %#v, want validation CLIError", err)
}
})
}
}
@@ -0,0 +1,27 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package helpers
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestMultiWikiSkillDoesNotRequireYesForAppend(t *testing.T) {
path := filepath.Join("..", "..", "skills", "multi", "dingtalk-wiki", "SKILL.md")
content, err := os.ReadFile(path)
if err != nil {
t.Fatalf("read wiki skill: %v", err)
}
text := string(content)
if strings.Contains(text, "--mode overwrite|append --content-file <tmp.md> --yes") {
t.Fatal("wiki skill still applies --yes to append and overwrite indiscriminately")
}
if !strings.Contains(text, "--mode append --content-file <tmp.md>") ||
!strings.Contains(text, "--mode overwrite --content-file <tmp.md> --yes") {
t.Fatal("wiki skill must publish separate append and confirmed overwrite examples")
}
}
+1
View File
@@ -326,6 +326,7 @@ func runImportCommand(cmd *cobra.Command, args []string, cfg importFlowConfig) e
documentType, _ := result["documentType"].(string)
finalResult := map[string]any{
"success": true,
"status": "completed",
"taskId": taskID,
"documentUrl": documentURL,
"documentName": documentName,
+1 -1
View File
@@ -181,7 +181,7 @@ func TestCrossPlatformCoverageSheetImportRunsSharedDocImportFlow(t *testing.T) {
if err := json.Unmarshal([]byte(output), &payload); err != nil {
t.Fatalf("sheet import stdout must be one JSON document: %v\n%s", err, output)
}
if payload["nodeId"] != "node-1" || payload["success"] != true {
if payload["nodeId"] != "node-1" || payload["success"] != true || payload["status"] != "completed" {
t.Fatalf("output missing success contract: %#v", payload)
}
}
+43 -1
View File
@@ -18,6 +18,8 @@ import (
"strconv"
"strings"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
)
@@ -82,6 +84,18 @@ var ChatMembersGet = shortcut.Shortcut{
{Kind: shortcut.ConstraintExactlyOne, Flags: []string{"users", "open-dingtalk-ids"}},
},
Tips: []string{`dws chat +chat-members-get --id <openConversationId> --users odid1,odid2`},
Validate: func(rt *shortcut.RuntimeContext) error {
for _, value := range rt.StrSliceFirst("users", "open-dingtalk-ids") {
value = strings.TrimSpace(value)
if value == "" || !isOpenID(value) {
return apperrors.NewValidation(fmt.Sprintf(
"--users/--open-dingtalk-ids 只接受成员 openDingTalkId;%q 不是 openDingTalkId。请先查询人员并传返回的 openDingTalkId",
value,
))
}
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
conversationID := rt.StrFirst("id", "group", "chat-id", "conversation-id", "open-conversation-id")
return rt.CallMCP("list_group_member_by_ids", map[string]any{
@@ -186,6 +200,12 @@ var ChatUpdateIcon = shortcut.Shortcut{
{Name: "icon-media-id", Type: shortcut.FlagString, Desc: "群头像 mediaId(以 @ 开头)", Required: true},
},
Tips: []string{`dws chat +chat-update-icon --group <openConversationId> --icon-media-id <mediaId>`},
Validate: func(rt *shortcut.RuntimeContext) error {
if err := helpers.ValidateChatMediaID(rt.Str("icon-media-id")); err != nil {
return apperrors.NewValidation("invalid --icon-media-id: " + err.Error())
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_group_icon", map[string]any{
"openConversationId": rt.Str("group"),
@@ -207,7 +227,8 @@ var ChatUpdateSettings = shortcut.Shortcut{
{Name: "setting-key", Type: shortcut.FlagString, Desc: "群设置项 key,如 searchable / onlyAdminCanAtAll", Required: true},
{Name: "status", Type: shortcut.FlagInt, Desc: "设置值:0=关闭,1=开启", Required: true},
},
Tips: []string{`dws chat +chat-update-settings --group <openConversationId> --setting-key searchable --status 1`},
Tips: []string{`dws chat +chat-update-settings --group <openConversationId> --setting-key searchable --status 1`},
Validate: validateChatUpdateSettings,
Execute: func(rt *shortcut.RuntimeContext) error {
return rt.CallMCP("update_group_settings", map[string]any{
"openConversationId": rt.Str("group"),
@@ -217,6 +238,27 @@ var ChatUpdateSettings = shortcut.Shortcut{
},
}
var supportedChatSettingKeys = map[string]struct{}{
"authority": {}, "joinValidation": {}, "onlyAdminCanAtAll": {}, "searchable": {},
"addFriendForbidden": {}, "toolbarStatus": {}, "pluginCustomizeVerify": {},
"onlyAdminCanDING": {}, "allMembersCanCreateMcsConf": {}, "onlyAdminCanSetMsgTop": {},
"onlyAdminCanPinMsg": {}, "onlyAdminCanSendFile": {}, "allMembersCanCreateCalendar": {},
"groupEmailDisabled": {}, "groupRedEnvelopeSwitch": {}, "groupLiveAuthority": {},
"groupBillAuthority": {},
}
func validateChatUpdateSettings(rt *shortcut.RuntimeContext) error {
key := strings.TrimSpace(rt.Str("setting-key"))
if _, ok := supportedChatSettingKeys[key]; !ok {
return apperrors.NewValidation(fmt.Sprintf("不支持的 --setting-key %q;请使用 chat group update-settings --help 中列出的设置项", key))
}
status := rt.Int("status")
if status != 0 && status != 1 {
return apperrors.NewValidation("--status 只允许 0(关闭)或 1(开启)")
}
return nil
}
// ChatDismiss dismisses (destroys) a group (dismiss_group, im).
var ChatDismiss = shortcut.Shortcut{
Service: "chat",
+38
View File
@@ -54,6 +54,15 @@ func TestMessagesSendPublishesCompleteIdentityConstraintInputs(t *testing.T) {
}
}
func TestMessagesSendUploadTargetUsesUploadInterfaceFields(t *testing.T) {
if got := messagesSendUploadTarget("cid-1", ""); !reflect.DeepEqual(got, map[string]any{"openConversationId": "cid-1"}) {
t.Fatalf("group upload target = %#v", got)
}
if got := messagesSendUploadTarget("", "D-open-1"); !reflect.DeepEqual(got, map[string]any{"openDingTalkId": "D-open-1"}) {
t.Fatalf("direct upload target = %#v", got)
}
}
func TestCrossPlatformCoverageSafeResourceDownloadsStayReadOnly(t *testing.T) {
for _, command := range []shortcut.Shortcut{MessagesMget, MessagesResourceDownload} {
if command.Risk != shortcut.RiskRead {
@@ -155,6 +164,9 @@ func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileFlow(t *testing.T)
fake.calls[2].tool != "send_personal_message" {
t.Fatalf("file flow calls = %#v", fake.calls)
}
if fake.calls[0].args["openConversationId"] != "cid" || fake.calls[1].args["openConversationId"] != "cid" {
t.Fatalf("upload target args = %#v / %#v, want openConversationId", fake.calls[0].args, fake.calls[1].args)
}
send := fake.calls[2]
if send.args["msgType"] != "file" || send.args["openConversationId"] != "cid" ||
send.args["uuid"] != "file-key" {
@@ -177,6 +189,32 @@ func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileFlow(t *testing.T)
}
}
func TestChatShortcutInvalidInputsStopBeforeMCP(t *testing.T) {
tests := []struct {
name string
args []string
}{
{name: "member numeric user id", args: []string{"chat", "+chat-members-get", "--id", "cid", "--users", "489149"}},
{name: "icon local path", args: []string{"chat", "+chat-update-icon", "--group", "cid", "--icon-media-id", "./logo.png", "--yes"}},
{name: "setting unknown key", args: []string{"chat", "+chat-update-settings", "--group", "cid", "--setting-key", "unknown", "--status", "1", "--yes"}},
{name: "setting invalid status", args: []string{"chat", "+chat-update-settings", "--group", "cid", "--setting-key", "searchable", "--status", "2", "--yes"}},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fake := &larkAlignmentCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tc.args)
if err := root.Execute(); err == nil {
t.Fatal("invalid command succeeded, want validation error")
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want zero", fake.calls)
}
})
}
}
func TestCrossPlatformCoverageMessagesSendCurrentUserLocalFileDryRunAndFailures(t *testing.T) {
t.Chdir(t.TempDir())
if err := os.WriteFile("fixture.bin", []byte("x"), 0o600); err != nil {
+8 -2
View File
@@ -420,8 +420,7 @@ func executeMessagesSendUserFile(
if err != nil {
return err
}
targetArgs := map[string]any{}
addMessagesSendUserTarget(targetArgs, group, openID)
targetArgs := messagesSendUploadTarget(group, openID)
idempotencyKey := messagesSendIdempotencyKey(rt)
if rt.DryRun() {
return rt.Output(map[string]any{
@@ -490,6 +489,13 @@ func executeMessagesSendUserFile(
})
}
func messagesSendUploadTarget(group, openID string) map[string]any {
if group != "" {
return map[string]any{"openConversationId": group}
}
return map[string]any{"openDingTalkId": openID}
}
func addMessagesSendUserTarget(params map[string]any, group, openID string) {
if group != "" {
params["openConversationId"] = group
+18 -1
View File
@@ -65,7 +65,7 @@ var AtMe = shortcut.Shortcut{
`dws chat +at-me`,
`dws chat +at-me --days 3`,
},
Validate: chatshortcut.ValidateMessageResourceDownload,
Validate: validateAtMe,
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — look-back window [now-Nd, now] in epoch millis. days defaults
// to 7; guard against non-positive overrides so the window stays sane.
@@ -109,6 +109,23 @@ var AtMe = shortcut.Shortcut{
},
}
func validateAtMe(rt *shortcut.RuntimeContext) error {
if err := chatshortcut.ValidateMessageResourceDownload(rt); err != nil {
return err
}
days := rt.Int("days")
if days <= 0 {
return localChatOptionError("invalid_lookback_window", "+at-me 的 --days 必须大于 0", "--days")
}
if days > 3650 {
return localChatOptionError("lookback_window_too_large", "+at-me 的 --days 超出支持范围 1-3650", "--days")
}
if rt.Int("limit") <= 0 {
return localChatOptionError("invalid_page_size", "+at-me 的 --limit 必须大于 0", "--limit")
}
return nil
}
// atMeMessageItems locates the message list inside a search_at_me_message
// response, probing common container keys at the top level and nested under
// "result". Returns nil when no list is found.
@@ -22,6 +22,22 @@ func TestPreferExactGroupMatches(t *testing.T) {
}
}
func TestLooksLikeOpenConversationID(t *testing.T) {
for _, value := range []string{
"cidayZx5r0T+UiMi6NrO1048A==",
"cidr0zKX5dvj/c0wDO+wupqyg==",
} {
if !looksLikeOpenConversationID(value) {
t.Errorf("looksLikeOpenConversationID(%q) = false", value)
}
}
for _, value := range []string{"项目冲刺", "cid项目群", "city project"} {
if looksLikeOpenConversationID(value) {
t.Errorf("looksLikeOpenConversationID(%q) = true", value)
}
}
}
func TestResolveMemberTypes(t *testing.T) {
users, bots, err := resolveMemberTypes(nil)
if err != nil || !users || !bots {
+48 -4
View File
@@ -17,6 +17,7 @@ import (
"strings"
"time"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
@@ -66,8 +67,10 @@ var ChatMessages = shortcut.Shortcut{
{Name: "user", Type: shortcut.FlagString, Desc: "单聊对方的 userId,与 --group 互斥"},
{Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊对方的 openDingTalkId,与 --group/--user 互斥"},
{Name: "time", Type: shortcut.FlagString, Desc: "时间边界,如 \"2025-03-01 00:00:00\";省略时从当前时间向前读取最近消息"},
{Name: "before", Type: shortcut.FlagString, Desc: "--time <值> --direction older 的兼容写法", Hidden: true},
{Name: "limit", Type: shortcut.FlagInt, Desc: "每页拉取的消息条数(可选)"},
{Name: "size", Type: shortcut.FlagInt, Desc: "--limit 的旧版别名", Hidden: true},
{Name: "page-all", Type: shortcut.FlagBool, Desc: "暂不支持;请按返回游标显式翻页", Hidden: true},
{Name: "direction", Type: shortcut.FlagString, Enum: []string{"newer", "older"}, Desc: "时间方向 newer/older;省略时为 older,从时间边界向前读取"},
{Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"},
}, chatshortcut.MessageResourceDownloadFlags()...),
@@ -79,7 +82,7 @@ var ChatMessages = shortcut.Shortcut{
`dws chat +chat-messages --user <userId> --time "2025-03-01 00:00:00" --limit 50`,
`dws chat +chat-messages --group <openconversation_id> --direction older`,
},
Validate: chatshortcut.ValidateMessageResourceDownload,
Validate: validateChatMessages,
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — build params and pick the right tool. Param keys
// (openconversation_id / userId / time / forward / limit) match the MCP server
@@ -88,8 +91,8 @@ var ChatMessages = shortcut.Shortcut{
params := map[string]any{}
fallbackConversationID := ""
if rt.Changed("time") && rt.Str("time") != "" {
params["time"] = rt.Str("time")
if boundary := rt.StrFirst("time", "before"); boundary != "" {
params["time"] = boundary
} else {
params["time"] = formatDingTalkMessageBoundary(time.Now())
}
@@ -99,7 +102,9 @@ var ChatMessages = shortcut.Shortcut{
// direction newer/older maps to the tools' boolean `forward` param
// (newer -> forward=true, older -> forward=false), matching chat.go's
// resolveMessageForward.
if rt.Changed("direction") {
if rt.Changed("before") {
params["forward"] = false
} else if rt.Changed("direction") {
switch strings.TrimSpace(strings.ToLower(rt.Str("direction"))) {
case "newer":
params["forward"] = true
@@ -148,6 +153,45 @@ var ChatMessages = shortcut.Shortcut{
},
}
func validateChatMessages(rt *shortcut.RuntimeContext) error {
if err := chatshortcut.ValidateMessageResourceDownload(rt); err != nil {
return err
}
if rt.Changed("time") && rt.Changed("before") {
return localChatOptionError("conflicting_time_options", "+chat-messages 的 --time 与 --before 不能同时使用", "--time", "--before")
}
if rt.Changed("before") && rt.Changed("direction") && strings.ToLower(rt.Str("direction")) != "older" {
return localChatOptionError("incompatible_time_direction", "+chat-messages 的 --before 不能与 --direction newer 同时使用", "--before", "--direction")
}
if groupID := rt.StrFirst("group", "conversation-id", "id"); groupID != "" && looksLikeHumanGroupName(groupID) {
flag := changedConversationIDFlag(rt)
return localChatOptionError("group_name_used_as_conversation_id", "+chat-messages 的 "+flag+" 需要 openConversationId,当前值像群名", flag)
}
if (rt.Changed("limit") && rt.Int("limit") <= 0) || (rt.Changed("size") && rt.Int("size") <= 0) {
flag := "--limit"
if rt.Changed("size") {
flag = "--size"
}
return localChatOptionError("invalid_page_size", "+chat-messages 的 "+flag+" 必须大于 0", flag)
}
if boundary := rt.StrFirst("time", "before"); boundary != "" && !validChatTime(boundary) {
flag := "--time"
if rt.Changed("before") {
flag = "--before"
}
return localChatOptionError("invalid_time_boundary", "+chat-messages 的 "+flag+" 格式无效", flag)
}
if rt.Bool("page-all") {
return apperrors.NewValidation(
"+chat-messages 暂不支持 --page-all",
apperrors.WithReason("page_all_not_supported"),
apperrors.WithActions("先执行单页查询,再使用返回的游标继续翻页"),
apperrors.WithExamples(`dws chat +chat-messages --group <openConversationId> --time "2026-07-30 16:51:39" --direction older --format json`),
)
}
return nil
}
// chatMessageItems defensively unwraps the message list from the response,
// tolerating the common container keys and one level of nesting under a
// "result"/"data" wrapper.
@@ -260,3 +260,117 @@ func TestCrossPlatformCoverageCompatibilityAliases(t *testing.T) {
})
}
}
func TestSendToGroupRejectsNonNameInputsBeforeMCP(t *testing.T) {
tests := []struct {
name string
argv []string
want string
}{
{
name: "conversation id used as group name",
argv: []string{"chat", "+send-to-group", "--group", "cidayZx5r0T+UiMi6NrO1048A==", "--text", "你好", "--yes"},
want: "只接受群名关键词",
},
{
name: "at all belongs to messages send",
argv: []string{"chat", "+send-to-group", "--group", "项目冲刺", "--text", "你好", "--at-all", "--yes"},
want: "--at-all",
},
{
name: "idempotency belongs to messages send",
argv: []string{"chat", "+send-to-group", "--group", "项目冲刺", "--text", "你好", "--idempotency-key", "case-1", "--yes"},
want: "--idempotency-key",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tc.argv)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("Execute() error = %v, want %q", err, tc.want)
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want none", fake.calls)
}
})
}
}
func TestChatMessagesBeforeAliasAndPageAllGuard(t *testing.T) {
t.Run("before maps to time and older", func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid-1", "--before", "2026-07-30 16:51:39"})
if err := root.Execute(); err != nil {
t.Fatal(err)
}
call := fake.calls[len(fake.calls)-1]
if call.args["time"] != "2026-07-30 16:51:39" || call.args["forward"] != false {
t.Fatalf("args = %#v", call.args)
}
})
t.Run("page all is blocked before MCP", func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs([]string{"chat", "+chat-messages", "--group", "cid-1", "--page-all"})
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), "暂不支持 --page-all") {
t.Fatalf("Execute() error = %v", err)
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want none", fake.calls)
}
})
}
func TestRelatedChatOptionsRejectBeforeMCP(t *testing.T) {
tests := []struct {
name string
argv []string
want string
}{
{"group members rejects cid as name", []string{"chat", "+group-members", "--group", "cidayZx5r0T+UiMi6NrO1048A=="}, "--group"},
{"members list rejects cid as name", []string{"chat", "+chat-members-list", "--group", "cidayZx5r0T+UiMi6NrO1048A=="}, "--group"},
{"members list rejects name as cid", []string{"chat", "+chat-members-list", "--conversation-id", "测试群"}, "--conversation-id"},
{"members list reports id alias", []string{"chat", "+chat-members-list", "--id", "测试群"}, "--id"},
{"members list rejects unknown member type", []string{"chat", "+chat-members-list", "--group", "测试群", "--member-types", "admin"}, "--member-types"},
{"unread chats rejects nonpositive count", []string{"chat", "+unread-chats", "--count", "0"}, "--count"},
{"messages rejects time and before", []string{"chat", "+chat-messages", "--group", "cid", "--time", "2026-08-01", "--before", "2026-08-02"}, "--time"},
{"messages rejects before newer", []string{"chat", "+chat-messages", "--group", "cid", "--before", "2026-08-02", "--direction", "newer"}, "--direction"},
{"messages rejects group name as cid", []string{"chat", "+chat-messages", "--group", "测试群"}, "--group"},
{"messages rejects nonpositive limit", []string{"chat", "+chat-messages", "--group", "cid", "--limit", "0"}, "--limit"},
{"messages reports size alias", []string{"chat", "+chat-messages", "--group", "cid", "--size", "0"}, "--size"},
{"messages rejects invalid time", []string{"chat", "+chat-messages", "--group", "cid", "--time", "yesterday-ish"}, "--time"},
{"messages reports before alias", []string{"chat", "+chat-messages", "--group", "cid", "--before", "yesterday-ish"}, "--before"},
{"messages reports conversation id alias", []string{"chat", "+chat-messages", "--conversation-id", "测试群"}, "--conversation-id"},
{"thread replies rejects group name as cid", []string{"chat", "+thread-replies", "--group", "测试群", "--thread-id", "thread-1"}, "--group"},
{"thread replies rejects nonpositive limit", []string{"chat", "+thread-replies", "--group", "cid", "--thread-id", "thread-1", "--limit", "0"}, "--limit"},
{"thread replies rejects invalid time", []string{"chat", "+thread-replies", "--group", "cid", "--thread-id", "thread-1", "--time", "last-week"}, "--time"},
{"at me rejects nonpositive days", []string{"chat", "+at-me", "--days", "0"}, "--days"},
{"at me rejects oversized days", []string{"chat", "+at-me", "--days", "3651"}, "--days"},
{"at me rejects nonpositive limit", []string{"chat", "+at-me", "--limit", "0"}, "--limit"},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
fake := &platformCoverageCaller{}
helpers.InitDeps(fake)
root := newPlatformCoverageRoot()
root.SetArgs(tc.argv)
err := root.Execute()
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("Execute() error = %v, want %q", err, tc.want)
}
if len(fake.calls) != 0 {
t.Fatalf("MCP calls = %#v, want none", fake.calls)
}
})
}
}
+26
View File
@@ -47,6 +47,12 @@ var GroupMembers = shortcut.Shortcut{
{Name: "group", Type: shortcut.FlagString, Desc: "群名称(搜群关键词,用群名里连续的核心词)", Required: true},
},
Tips: []string{`dws chat +group-members --group 项目冲刺`},
Validate: func(rt *shortcut.RuntimeContext) error {
if looksLikeOpenConversationID(rt.Str("group")) {
return localChatOptionError("group_name_expected", "+group-members 的 --group 需要群名,当前值像 openConversationId", "--group")
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
groupName := rt.Str("group")
@@ -121,6 +127,7 @@ var ChatMembersList = shortcut.Shortcut{
`dws chat +chat-members-list --group "项目冲刺"`,
`dws chat +chat-members-list --conversation-id <openConversationId> --member-types user,bot`,
},
Validate: validateChatMembersList,
Execute: func(rt *shortcut.RuntimeContext) error {
groupID := strings.TrimSpace(rt.StrFirst("conversation-id", "id", "chat-id", "open-conversation-id"))
groupName := strings.TrimSpace(rt.Str("group"))
@@ -194,6 +201,25 @@ var ChatMembersList = shortcut.Shortcut{
},
}
func validateChatMembersList(rt *shortcut.RuntimeContext) error {
groupName := strings.TrimSpace(rt.Str("group"))
groupID := strings.TrimSpace(rt.StrFirst("conversation-id", "id", "chat-id", "open-conversation-id"))
if groupName != "" && looksLikeOpenConversationID(groupName) {
return localChatOptionError("conversation_id_used_as_group_name", "+chat-members-list 的 --group 需要群名,当前值像 openConversationId", "--group")
}
if groupID != "" && looksLikeHumanGroupName(groupID) {
flag := changedConversationIDFlag(rt)
return localChatOptionError("group_name_used_as_conversation_id", "+chat-members-list 的 "+flag+" 需要 openConversationId,当前值像群名", flag)
}
if rt.Changed("member-types") {
users, bots, err := resolveMemberTypes(rt.StrSlice("member-types"))
if err != nil || (!users && !bots) {
return localChatOptionError("invalid_member_types", "+chat-members-list 的 --member-types 包含不支持的值", "--member-types")
}
}
return nil
}
func resolveGroupName(rt *shortcut.RuntimeContext, groupName string) (string, error) {
data, err := rt.CallMCPData("im", "search_groups", map[string]any{
"keyword": groupName,
@@ -0,0 +1,67 @@
// Copyright 2026 Alibaba Group
// Licensed under the Apache License, Version 2.0 (the "License");
package smart
import (
"strings"
"time"
"unicode"
apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
)
func looksLikeOpenConversationID(value string) bool {
value = strings.TrimSpace(value)
if !strings.HasPrefix(strings.ToLower(value), "cid") {
return false
}
return len(value) >= 24 || strings.ContainsAny(value, "/+=")
}
func looksLikeHumanGroupName(value string) bool {
value = strings.TrimSpace(value)
if value == "" || looksLikeOpenConversationID(value) || strings.EqualFold(value, "cid") {
return false
}
for _, r := range value {
if unicode.Is(unicode.Han, r) || unicode.IsSpace(r) {
return true
}
}
return false
}
func validChatTime(value string) bool {
value = strings.TrimSpace(value)
for _, layout := range []string{time.RFC3339, "2006-01-02 15:04:05", "2006-01-02"} {
if _, err := time.Parse(layout, value); err == nil {
return true
}
}
return false
}
func changedConversationIDFlag(rt *shortcut.RuntimeContext) string {
for _, name := range []string{"group", "conversation-id", "id", "chat-id", "open-conversation-id"} {
if rt.Changed(name) {
return "--" + name
}
}
return "会话 ID 参数"
}
func localChatOptionError(reason, message string, flags ...string) error {
flagText := strings.Join(flags, "、")
action := "修正参数后重试,或查看当前命令帮助"
if flagText != "" {
action = "检查 " + flagText + " 后重试,或查看当前命令帮助"
}
return apperrors.NewValidation(
message,
apperrors.WithReason(reason),
apperrors.WithActions(action),
apperrors.WithExamples("dws chat --help"),
)
}
+33 -1
View File
@@ -45,9 +45,12 @@ var SendToGroup = shortcut.Shortcut{
Flags: []shortcut.Flag{
{Name: "group", Type: shortcut.FlagString, Desc: "群名称(搜群关键词,用群名里连续的核心词)", Required: true},
{Name: "text", Type: shortcut.FlagString, Desc: "消息内容(支持 Markdown)", Required: true},
{Name: "at-all", Type: shortcut.FlagBool, Desc: "不支持;需要 @所有人时改用 +messages-send", Hidden: true},
{Name: "idempotency-key", Type: shortcut.FlagString, Desc: "不支持;需要幂等键时改用 +messages-send", Hidden: true},
shortcut.AIMessageTagFlag(),
},
Tips: []string{`dws chat +send-to-group --group 项目冲刺 --text "今天 5 点前提交进度"`},
Tips: []string{`dws chat +send-to-group --group 项目冲刺 --text "今天 5 点前提交进度"`},
Validate: validateSendToGroup,
Execute: func(rt *shortcut.RuntimeContext) error {
groupName := rt.Str("group")
text := rt.Str("text")
@@ -82,6 +85,35 @@ var SendToGroup = shortcut.Shortcut{
},
}
func validateSendToGroup(rt *shortcut.RuntimeContext) error {
group := rt.Str("group")
if looksLikeOpenConversationID(group) {
return apperrors.NewValidation(
"+send-to-group 的 --group 只接受群名关键词,不能传 openConversationId",
apperrors.WithReason("conversation_id_used_as_group_name"),
apperrors.WithActions("已有群 openConversationId 时改用 chat +messages-send --chat-id"),
apperrors.WithExamples(`dws chat +messages-send --chat-id <openConversationId> --text "消息内容" --format json`),
)
}
if rt.Changed("at-all") || rt.Changed("idempotency-key") {
unsupported := make([]string, 0, 2)
if rt.Changed("at-all") {
unsupported = append(unsupported, "--at-all")
}
if rt.Changed("idempotency-key") {
unsupported = append(unsupported, "--idempotency-key")
}
flagText := strings.Join(unsupported, "、")
return apperrors.NewValidation(
"+send-to-group 不支持当前使用的发送选项:"+flagText,
apperrors.WithReason("unsupported_send_to_group_option"),
apperrors.WithActions("移除 "+flagText+",或改用支持该选项的消息发送命令"),
apperrors.WithExamples(`dws chat +messages-send --help`),
)
}
return nil
}
// sendGroupMatch is a single group candidate resolved from a name search.
type sendGroupMatch struct {
id string
+19 -1
View File
@@ -14,6 +14,8 @@
package smart
import (
"strings"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut"
chatshortcut "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chat"
"github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg"
@@ -63,7 +65,7 @@ var ThreadReplies = shortcut.Shortcut{
`dws chat +thread-replies --group <openconversationId> --thread-id <threadId>`,
`dws chat +thread-replies --group <openconversationId> --thread-id <threadId> --time "2025-03-01 00:00:00" --limit 20`,
},
Validate: chatshortcut.ValidateMessageResourceDownload,
Validate: validateThreadReplies,
Execute: func(rt *shortcut.RuntimeContext) error {
// Step 1 — fetch the topic replies. Param keys (openconversationId /
// topicId / startTime / pageSize) are copied verbatim from chat.go's
@@ -105,6 +107,22 @@ var ThreadReplies = shortcut.Shortcut{
},
}
func validateThreadReplies(rt *shortcut.RuntimeContext) error {
if err := chatshortcut.ValidateMessageResourceDownload(rt); err != nil {
return err
}
if looksLikeHumanGroupName(rt.Str("group")) {
return localChatOptionError("group_name_used_as_conversation_id", "+thread-replies 的 --group 需要 openConversationId,当前值像群名", "--group")
}
if rt.Changed("limit") && rt.Int("limit") <= 0 {
return localChatOptionError("invalid_page_size", "+thread-replies 的 --limit 必须大于 0", "--limit")
}
if value := strings.TrimSpace(rt.Str("time")); value != "" && !validChatTime(value) {
return localChatOptionError("invalid_time_boundary", "+thread-replies 的 --time 格式无效", "--time")
}
return nil
}
// threadReplyItems defensively unwraps the reply list from the response,
// tolerating the common container keys and one level of nesting under a
// "result"/"data" wrapper.
+6
View File
@@ -59,6 +59,12 @@ var UnreadChats = shortcut.Shortcut{
`dws chat +unread-chats --count 20`,
`dws chat +unread-chats --exclude-muted`,
},
Validate: func(rt *shortcut.RuntimeContext) error {
if rt.Changed("count") && rt.Int("count") <= 0 {
return localChatOptionError("invalid_page_size", "+unread-chats 的 --count 必须大于 0", "--count")
}
return nil
},
Execute: func(rt *shortcut.RuntimeContext) error {
// Build params exactly like chatMessageListUnreadConversationsCmd: count is
// only sent when > 0, excludeMuted only when true.
+19 -2
View File
@@ -55,7 +55,12 @@ PRODUCT_END = "<!-- VISIBLE_SHORTCUTS_END -->"
# only after verifying that the product skill has its own reviewed routing
# section and intent table; compacting a sparse skill without an alternative
# route would make its shortcuts harder to discover.
COMPACT_PRODUCT_SERVICES = {"chat"}
COMPACT_PRODUCT_SERVICES = {"aitable", "chat", "doc"}
# These compact products have every public Shortcut curated into Runtime
# Schema. Keep this separate from COMPACT_PRODUCT_SERVICES because Chat still
# has reviewed exclusions pending curation.
FULLY_CURATED_COMPACT_PRODUCT_SERVICES = {"doc"}
def md_escape(value: Any) -> str:
text = str(value or "")
@@ -135,10 +140,22 @@ def product_section(service: str, rows: list[dict[str, Any]]) -> str:
def compact_product_section(service: str, rows: list[dict[str, Any]]) -> str:
if service in FULLY_CURATED_COMPACT_PRODUCT_SERVICES:
inventory = (
f"`{md_escape(service)}` 当前有 {len(rows)} 条公开 Shortcut,"
"已全部进入 Runtime Schema。完整清单保留在 Runtime Shortcut "
"Catalog,根 Skill 不重复展开;单条参数与安全契约按需查询 leaf Schema。"
)
else:
inventory = (
f"`{md_escape(service)}` 当前有 {len(rows)} 条公开 shortcut。"
"完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation "
"的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。"
)
return f"""{PRODUCT_START}
## Shortcut 发现(按需)
`{md_escape(service)}` 当前有 {len(rows)} 条公开 shortcut。完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation 的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
{inventory}已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service {md_escape(service)} --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
{PRODUCT_END}"""
+47 -1
View File
@@ -7,8 +7,12 @@ cd "$ROOT"
python3 scripts/gen_skill_shortcut_sections.py --check
chat_skill="skills/multi/dingtalk-chat/SKILL.md"
aitable_skill="skills/multi/dingtalk-aitable/SKILL.md"
doc_skill="skills/multi/dingtalk-doc/SKILL.md"
mono_skill="skills/mono/SKILL.md"
chat_max_bytes=14000
aitable_max_bytes=11000
doc_max_bytes=9500
chat_bytes="$(wc -c < "$chat_skill" | tr -d ' ')"
if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
@@ -17,6 +21,20 @@ if [ "$chat_bytes" -gt "$chat_max_bytes" ]; then
exit 1
fi
aitable_bytes="$(wc -c < "$aitable_skill" | tr -d ' ')"
if [ "$aitable_bytes" -gt "$aitable_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $aitable_skill is ${aitable_bytes} bytes (max ${aitable_max_bytes})" >&2
exit 1
fi
doc_bytes="$(wc -c < "$doc_skill" | tr -d ' ')"
if [ "$doc_bytes" -gt "$doc_max_bytes" ]; then
printf '%s\n' \
"skill context budget exceeded: $doc_skill is ${doc_bytes} bytes (max ${doc_max_bytes})" >&2
exit 1
fi
shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
@@ -31,6 +49,34 @@ if [ "$shortcut_rows" -ne 0 ]; then
exit 1
fi
aitable_shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
/<!-- VISIBLE_SHORTCUTS_END -->/ { in_block = 0 }
in_block && /^\|[[:space:]]*`/ { count++ }
END { print count + 0 }
' "$aitable_skill"
)"
if [ "$aitable_shortcut_rows" -ne 0 ]; then
printf '%s\n' \
"skill context budget exceeded: $aitable_skill re-expanded $aitable_shortcut_rows shortcut rows" >&2
exit 1
fi
doc_shortcut_rows="$(
awk '
/<!-- VISIBLE_SHORTCUTS_START -->/ { in_block = 1; next }
/<!-- VISIBLE_SHORTCUTS_END -->/ { in_block = 0 }
in_block && /^\|[[:space:]]*`/ { count++ }
END { print count + 0 }
' "$doc_skill"
)"
if [ "$doc_shortcut_rows" -ne 0 ]; then
printf '%s\n' \
"skill context budget exceeded: $doc_skill re-expanded $doc_shortcut_rows shortcut rows" >&2
exit 1
fi
if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
printf '%s\n' \
"skill context budget regression: $mono_skill requires full product-reference loading" >&2
@@ -38,4 +84,4 @@ if grep -Fq "充分阅读产品参考文件" "$mono_skill"; then
fi
printf '%s\n' \
"skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes shortcut_rows=$shortcut_rows)"
"skill context budget: ok (chat_bytes=$chat_bytes max=$chat_max_bytes shortcut_rows=$shortcut_rows; aitable_bytes=$aitable_bytes max=$aitable_max_bytes shortcut_rows=$aitable_shortcut_rows; doc_bytes=$doc_bytes max=$doc_max_bytes shortcut_rows=$doc_shortcut_rows)"
@@ -2,6 +2,12 @@
> 通用规范见 [_common/conventions.md](_common/conventions.md)。
## 显式工作流
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
- `--name` 不替代用户显式要求的正文 H1;新建资源返回 ID 后,同一请求的指代绑定该新资源,禁止搜索同名旧资源替换。
- Word/Excel 需要“在线编辑/直接在线改”时使用 `doc import`,普通 `drive upload` 只保留原文件。
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| write-doc | 1. 按[「多源并行采集」](_common/conventions.md#多源并行采集公共模式)执行<br>2. **先把内容写入临时文件**(Linux/Mac `/tmp/<name>.md`,Windows `%TEMP%\<name>.md`)—— 含多行/表格/长文本必须走文件,不要把 markdown 直接作为命令行字符串<br>3. **单步创建**(< 200KB):`doc create --name "<文档名>" --content-file <tmp> [--folder <DOC_FOLDER_NODE_ID>] [--workspace <WS_ID>]`(`--folder` 只传文档文件夹 nodeId / alidocs 文件夹 URL,不传数字 dentryId)<br>4. **超长兜底**(> 200KB):**必须先向用户提示截断风险**(详见下方「分块 append 截断风险提示」),用户确认后再执行:`doc create --name "<文档名>" [--folder/--workspace]` → `nodeId` → 按段落切 ≤200KB 片段(不断表格) → 每片 `doc update --node <nodeId> --content-file <part> --mode append`<br>5. **回读校验**(必须):所有写入完成后,执行 `doc read --node <nodeId>` 回读文档,校验关键标题/段落是否完整写入(详见下方「doc update 回读校验规范」)<br>备选(仅短内容 <2KB 且无换行/表格):`doc create --name "..." --content "..."` |
+9 -7
View File
@@ -665,8 +665,8 @@ Flags:
- 知识库内 → `dws wiki node create --workspace <WS_ID> --type folder`(`doc folder create` / `doc file create --type folder` 已弃用)
用户说"上传文件/传文件/上传到文档/上传到知识库":
- 上传 → `upload`(需本地文件路径)
- 上传并转换 → `upload --convert`
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
- 用户明确要求“在线编辑/大家直接在线改/转在线文档” → `doc import --file <本地路径>`;不得用普通 upload 的成功响应宣称可在线编辑
用户说"导入文件/导入为在线文档/导入 Word/导入 Excel/导入 xmind/导入 Markdown/把本地文件转在线文档":
- 导入并转换为在线文档 → `doc import --file <本地路径>`
@@ -742,8 +742,8 @@ Flags:
关键区分: doc(文档编辑/阅读) vs aitable(数据表格操作) vs drive(钉盘文件管理)
用户说"上传文件/传文件/上传到文档/上传到知识库":
- 上传 → `upload`(需本地文件路径)
- 上传并转换 → `upload --convert`
- 仅保留原始文件用于存储/下载 → `drive upload`(需本地文件路径)
- 要转换为可在线编辑文档 → `doc import --file <本地路径>`,导入后验证在线类型与目标文件夹
用户说"下载文件/导出文件/下载到本地":
- 下载 → `download`(需文件节点 ID 或 URL)
@@ -1050,14 +1050,16 @@ EOF
- `read` 返回的内容中,文档里的附件会以 OSS 临时下载链接形式给出(如 `https://alidocs2.oss-cn-zhangjiakou.aliyuncs.com/res/.../att/<resourceId>.ext?Expires=...`),该链接会过期。链接过期后,可从 URL 路径中提取 `<resourceId>`(即 `/att/` 后、扩展名前的 UUID 部分),然后使用 `media download --node <DOC_ID> --resource-id <resourceId>` 重新获取下载链接
- `create` 不传 `--folder` 和 `--workspace` 时,默认创建在"我的文档"根目录
- `create` 只能建"文档"(adoc);要建表格/脑图/白板/多维表/演示,用 `dws wiki node create --workspace <id> --type <type>`(`doc file create` 已弃用);建普通文件夹用 `dws drive mkdir`
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;简单内容追加建议用 `update --mode append`
- `block list/insert/update/delete` 是块级精细编辑,适合结构化修改;只有用户未指定块操作的纯文本追加才建议 `update --mode append`。用户点名 list/insert/update/append 时必须逐项真实调用,不得折叠进 create
- `block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型 (table, callout 等) 使用 `--element` JSON
- 用户要求“有序列表块”时必须写真实列表结构(JSONML `p.list.isOrdered=true` 或等价 orderedList element),普通 Markdown/数字前缀段落不算完成
- `--content` 参数中的换行必须使用**真实换行符**(即实际的换行字符,Unicode `U+000A`),而不是字面量字符串 `\n`(反斜杠加字母 n)。在通过程序或大模型构造此参数时,请确保字符串在发送前已正确反转义。如果传入的是两个字符的字面量 `\n`,所有内容将渲染在同一行,导致标题、段落和表格格式全部错乱。**含多行/表格/长文本时优先用 `--content-file path.md` 或 `--content -`(stdin),不经过 shell escape,换行和表格都保持原样**(详见下方「长 Markdown 写入」)。
- 块类型包括: paragraph, heading, blockquote, callout, columns, orderedList, unorderedList, table, sheet, attachment, slot
- 关键区分: doc(文档内容级操作) vs wiki(知识库空间级管理) vs aitable(数据表格操作) vs drive(钉盘文件管理)
- wiki 是知识库容器,doc 是知识库中的文档内容;需要 `workspaceId` 时,先用 `dws wiki space list/search` 获取,再传给 doc 的 `--workspace` 参数
- `doc upload vs drive upload`:用户提到"知识库/文档空间/workspace" → `doc upload`;提到"钉盘/网盘/我的文件" → `drive upload`;未明确目标时默认 `drive upload`
- `upload` 支持上传任意类型文件 (PDF、Office、图片等) 到钉钉文档空间或知识库;`--convert` 可将 Office 文件转换为钉钉在线文档
- `drive upload` / `doc upload` 是普通文件存储路径;用户要求 Word/Excel “在线编辑/直接在线改”时硬路由到 `doc import`,并验证导入后的在线类型和文件夹。只有用户明确同时要原文件与在线版时才分别 upload + import
- 同一请求中新建、复制或导入返回的 `nodeId` 必须绑定后续“这篇/刚才那篇/上次那篇”;禁止搜索同名旧资源覆盖绑定
- `--name` 只是文档外壳标题,不能替代用户显式要求的正文 H1;用户说“正文先起一级标题”时必须写入或插入真实 H1
- `upload` 是三步自动完成的流程 (获取凭证 → OSS 上传 → 提交入库),无需手动分步操作
- `download` 是两步自动完成的流程 (获取下载链接 → HTTP GET 下载),支持自动推断文件名;`--output` 可指定文件路径或目录
- `media insert` 是三步自动完成的流程 (获取附件上传凭证 → OSS 上传 → 插入附件块到文档),无需手动分步操作
@@ -1,15 +1,11 @@
# doc block(块级精细编辑:list / insert / update / delete)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML validator 行为)
> 3. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — JSONML 范例(含 callout / 分栏 / 表格 / 标题等节点的完整命令)
> 4. [`./format/doc-jsonml-schema.md`](./format/doc-jsonml-schema.md) — JSONML 节点结构字段定义
>
> **同任务常配合**:[`doc-update.md`](./doc-update.md)(整篇 overwrite / 末尾追加纯文本)/ [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)(JSONML 复制范例)
> 本文件自包含简单 list/insert/update/delete 契约,不要递归预读路由或 style reference。只有实际构造复杂 JSONML 节点时,才读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);字段仍不确定时再查 [`doc-jsonml-schema.md`](./format/doc-jsonml-schema.md)。整篇 overwrite 或纯文本 append 才转读 [`doc-update.md`](./doc-update.md)。
> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。
> **显式块操作不可折叠**:用户说“先 create,再 list/insert/update/append”时按原顺序真实调用;不能因为最终正文相似,就把后续块操作合并进 create 或一次 Markdown 写入。
---
## doc block list(查询块元素)
@@ -173,7 +169,8 @@ dws doc block delete --node DOC_ID --block-id UUID
- **块类型**:paragraph、heading、blockquote、callout、columns、orderedList、unorderedList、table、sheet、attachment、slot。
- **快捷 vs --element**:`block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型(table、callout、columns 等)使用 `--element` JSON 或 `--content-format jsonml`。
- **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。
- **有序列表块**:用户明确要求 ordered list / 有序列表块时,必须用 JSONML `p` 节点的 `list.isOrdered=true`(同一 `listId`;仅首项设 `start:1`)或等价原生 orderedList element;带 `1.` 前缀的普通段落、普通 Markdown 或一次 create 不满足要求。
- **简单内容追加**:用户只说追加纯文本且不强调块操作时可用 [`./doc-update.md`](./doc-update.md) `--mode append`;用户明确说 block insert / 插入段落 / 插入标题 / 插入列表块时必须走 block insert。
- **JSONML validator**(写入端默认行为):
- 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。
- `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。
@@ -241,6 +238,12 @@ dws doc block list --node <DOC_ID> --content-format jsonml --block-id <UUID>
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"新段落"]]]'
# 插入有序列表块(3 项共用 listId,仅首项有 start)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol1","list":{"listId":"actions","level":0,"isOrdered":true,"start":1}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第一项"]]]'
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol2","list":{"listId":"actions","level":0,"isOrdered":true}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第二项"]]]'
# 插入 callout(colorBlocks)
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["container",{"uuid":"co1","subType":"colorBlocks","metadata":{"bgcolor":"#FDE2E0","border":"#F5C2C7"}},["p",{"uuid":"co1p1"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"高风险操作,先备份"]]]]'
@@ -1,9 +1,6 @@
# doc comment(文档评论:list / create / reply / update / delete / create-inline)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
>
> **同任务常配合**:`dws contact user search`(查 `--mention` 用 userId)/ `dws chat search`(查群用 openConversationId)/ [`doc-block.md`](./doc-block.md)(划词评论必须先取 blockId 与 paragraph 文本)
> 本文件自包含评论命令契约。仅在需要 mention 时查询真实 userId/openConversationId;仅在划词评论尚无 blockId 与 paragraph 文本时读取 [`doc-block.md`](./doc-block.md) 并执行 block list。
---
@@ -1,11 +1,6 @@
# doc create(创建文档)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-create-workflow.md`](./style/doc-create-workflow.md) — 创建工作流(标题、位置、骨架、回读校验)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范(草稿元素清单、骨架样板)
> 4. [`./doc-update.md` §内容写入管道](./doc-update.md#内容写入管道createupdate-共用) — 长内容自动分片、`--content-file` vs `--content` 选择
> 5. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
> 本文件自包含普通 Markdown 创建契约,不要递归预读 `doc.md`、style 或 update reference。仅当用户要求复杂版式并实际选择 JSONML 时,读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);需要文档骨架建议时才读取对应 style 章节。
## 创建路由前置判断(必看)
@@ -40,7 +35,7 @@ Flags:
## 关键说明
- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。
- **标题优先级**:`--name` 是文档外壳标题,默认可视作 H1;但它不能替代用户显式要求的正文一级标题。用户说“正文写 `# ...`”“正文先起个一级标题”时,必须在初始内容中保留该 `#` H1;用户未要求正文 H1 时,正文默认从 `##` 开始以避免重复。
- 不传 `--folder` 和 `--workspace` 时,默认创建在「我的文档」根目录。
- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。
- 输入方式选择见 [`./doc-update.md` §内容写入管道](./doc-update.md#内容写入管道createupdate-共用)(与 update 共用)。短文本字面量可 `--content`,多行/表格/特殊字符必须 `--content-file` 或 `--content -`。
@@ -54,6 +49,12 @@ Flags:
| `docUrl` | 最终交付给用户的链接;缺失时用 [`./doc-info.md`](./doc-info.md) 补查 |
| `chunksWritten` | 判断是否触发自动分片;> 1 时重点检查章节顺序 |
同一请求后续出现“这篇/刚才那篇/上次那篇”时,直接续用本次 create 返回的 `nodeId`;禁止先搜索同名文档再把后续操作指向旧节点。
## 显式操作序列
用户点名 `block list`、插入、追加、更新等后续动作时,必须按原顺序逐项执行。`doc create` 只写用户指定的初始内容,不能为了减少调用把后续标题、列表或段落提前塞进 create。例:`创建 → 查看块结构 → 末尾插入段落` 必须真实执行 create、block list、block insert 三步。
## 回读验收(必读)
CLI **不会**自动回读校验。**每次创建后**都必须执行 `doc read --node <nodeId>` 校验关键标题、段落首句、表格表头是否完整。详见 [`./style/doc-create-workflow.md` «回读验收»](./style/doc-create-workflow.md)。
@@ -1,7 +1,6 @@
# doc export(在线文档导出为 docx)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 export 契约。已有当前请求返回的 adoc nodeId 时直接导出;目标类型未知时只执行一次 info 检查,不要递归读取 `doc.md`。
> **路由前置判断**:用户说「下载/导出」时**必须**先用 [`./doc-info.md`](./doc-info.md) `info --node <ID> --format json` 查 `contentType`:
> - `contentType` 为 `ALIDOC`(在线文档)→ **必须用 `export`**,禁止用 `download`
@@ -45,6 +44,7 @@ Flags:
## 关键说明
- 同一请求中刚执行 create/copy/import 并紧接着说“这篇/刚才那篇/上次那篇”时,`--node` 必须使用该写操作真实返回的新 `nodeId`;不得预先搜索同名文档,也不得用搜索结果中的旧节点替换它。
- `export` 是一体化命令,一条命令自动完成提交→轮询→下载,**无需手动编排轮询**。CLI 内部使用渐进式退避轮询(最多约 5 分钟)。
- `export` 超时或中断后,CLI 会输出 `jobId`,可用 `dws doc export get --job-id <jobId>` 手动查询任务状态。
- `export` 当前仅支持钉钉在线文档(alidocs,`contentType=ALIDOC`)导出为 `docx`,**在线表格导出请使用其他命令**。
@@ -1,7 +1,6 @@
# doc 文件操作(upload / download / copy / move / rename / delete + folder create)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **按需使用**:本文件自包含弃用命令的兼容说明,不要求先读总路由;优先按下方提示改用 `drive` / `wiki`。
> **弃用提示(文件管理命令正在迁移到 drive / wiki)**:本文所列 `doc` 文件管理命令虽仍能跑,但执行时会打印弃用警告,请优先改用 `drive` / `wiki` 对应命令:
> - `doc download` → **`dws drive download`**(下载已有文件;在线文档导出 docx 仍走 `doc export`)
@@ -32,6 +31,7 @@ Flags:
- `upload` 是三步自动完成的流程(获取凭证 → OSS 上传 → 提交入库),无需手动分步操作。
- 支持上传任意类型文件(PDF、Office、图片等)到钉钉文档空间或知识库。
- `--convert` 可将 Office 文件转换为钉钉在线文档。
- **在线编辑硬路由**:用户说“大家直接在线改/上传后在线编辑/转成钉钉文档”时使用 [`./doc-import.md`](./doc-import.md) `doc import`,并回查在线类型;普通 `doc/drive upload` 只用于保留文件,不能据此承诺可在线编辑。
- **`doc upload` vs `drive upload`**:用户提到「知识库 / 文档空间 / workspace」→ `doc upload`;提到「钉盘 / 网盘 / 我的文件」→ `drive upload`;未明确目标时默认 `drive upload`。
- 与 [`./doc-media.md`](./doc-media.md) `media insert` 的区别:`upload` 上传到文档空间作为**独立文件**;`media insert` 作为**附件块插入到文档正文中**。
@@ -6,6 +6,8 @@
不要先读取文件内容再调用 `doc create` 或 `doc update`。`doc import` 会按文件格式走导入任务,保留更完整的原始结构。
> **在线编辑硬路由**:用户说“上传后在线编辑/大家直接在线改/转成钉钉文档”时必须使用 `doc import`。`drive upload` 只保留原始 `.docx/.xlsx/...` 普通文件,不能据此宣称已可在线编辑。若用户明确要同时保留原文件和在线版,才先 `drive upload`,再单独 `doc import`,并分别验证两个返回节点。
## 命令
```bash
@@ -38,6 +40,7 @@ dws doc import get --task-id <TASK_ID> --format json
3. 执行 `dws doc import --file ... --format json`。
4. 正常情况下 CLI 会自动提交、上传并轮询导入任务。
5. 如果命令超时或中断,从输出中提取 `taskId`,再执行 `dws doc import get --task-id <TASK_ID> --format json`。
6. 用返回的 `documentUrl`/`nodeId` 执行 `drive info` 或 `doc info`,确认在线类型和目标文件夹;验证通过后才能说“可直接在线编辑”。
## 上下文传递
@@ -1,8 +1,6 @@
# doc info(获取文档元信息 + URL 解析)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`../../url-patterns.md`](../../url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时
> 本文件自包含已知 nodeId/URL 的 info 契约。只有原始 alidocs URL 类型仍不明确时,才读取 [`url-patterns.md`](../../url-patterns.md);不要递归读取 `doc.md`。
>
> **同任务常配合**:`dws drive search` / `dws wiki node search`(先定位 nodeId)/ [`doc-read.md`](./doc-read.md)(确认是 ALIDOC 后读正文)
@@ -1,7 +1,6 @@
# doc media(附件 / 图片:download / insert)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 media insert/download 契约。nodeId、文件路径或 resourceId 已知时直接执行;只在需要相对块定位且 blockId 未知时读取 [`doc-block.md`](./doc-block.md)。
> ⚠️ **图片插入硬规则**:
> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert`
@@ -1,7 +1,6 @@
# doc permission(文档权限:add / update / list)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> **按需使用**:本文件自包含文档节点权限命令;不要求先读总路由。知识库整体成员权限改读 `dingtalk-wiki`。
> **关键区分**:
> - "把**某篇文档**授权给某人" → `doc permission add`(节点级,包括「我的文档」下的文档都支持)
@@ -1,10 +1,6 @@
# doc read(读取文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-info.md`](./doc-info.md)(先解析 URL,确认 contentType=ALIDOC、extension=adoc)/ [`doc-update.md`](./doc-update.md)(读后改写)/ [`doc-block.md`](./doc-block.md)(块级精修前先读结构)
> 本文件自包含普通 read 契约。用户已给当前 adoc nodeId/URL 时直接读取;类型未知时才先执行 info。选择 JSONML 只为获取结构,不要求预读 cookbook;实际构造 JSONML 写入时再按需加载。
## 命令格式
@@ -1,12 +1,6 @@
# doc update(更新文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、分片 append、回读验收)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范
> 4. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-read.md`](./doc-read.md)(改写前必读,jsonml 模式拿当前结构;担心被并发覆盖时再取 revision)/ [`doc-block.md`](./doc-block.md)(单 block 改写优先;本命令更适合追加 / 整篇 overwrite)
> 本文件自包含普通 append/overwrite 契约,不要递归预读路由或 style reference。纯文本 append 可直接执行;overwrite 先 read/dry-run/确认。只有保真改写或复杂 JSONML 才读取 [`doc-update-workflow.md`](./style/doc-update-workflow.md) 与 cookbook;单块修改改用 [`doc-block.md`](./doc-block.md)。
## 命令格式
@@ -4,11 +4,9 @@
> 改写已有文档见 [doc-update-workflow.md](./doc-update-workflow.md)。排版规范见 [doc-style-guideline.md](./doc-style-guideline.md)。
## 前置必读
## 按需使用
> **同时读取 [doc-style-guideline.md](./doc-style-guideline.md):**
> - **§2.0 类型判断决策表** → 锁定文档类型(决策型 / 执行型 / 说明型 / 知识沉淀型)和骨架
> - **§1 硬规则** → 全程生效(`--name` 已是 H1、不编造 URL、Markdown 草稿不写 callout 等)
普通 Markdown 创建不需要先读本文件或 style guideline。只有用户要求设计文档骨架或复杂版式时,才查看下方对应章节;实际选择 JSONML 后再读取 cookbook。需要按文档类型选骨架时,按需读取 [doc-style-guideline.md](./doc-style-guideline.md) 的对应一节,不要通读。
### 关键词速查(用户意图 → 起稿路径)
@@ -45,7 +43,7 @@
| 项目 | 要求 |
|------|------|
| 标题 | 用 `--name` 传入;正文不要再重复同名一级标题 |
| 标题 | 用 `--name` 传入;用户显式要求正文 H1 时原样保留,未要求时正文默认从 H2 开始 |
| 位置 | 默认创建到我的文档;指定目录时只接受文档文件夹 `nodeId` 或 alidocs 文件夹 URL |
| 正文 | 多行、表格、代码块、特殊字符或长度 >= 2KB 时必须写入 UTF-8 临时 `.md` 文件 |
| 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 |
@@ -226,7 +224,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。
> **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。
> **JSONML 条件加载**:仅在确定使用 JSONML 后读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md);其中“决策型文档骨架范例”可直接改写。
> 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
### ⚠️ JSONML 降级约束
@@ -274,7 +272,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
```
- 根节点固定 `"root"`(不是 `"body"`)
- `--name` 已是 H1,JSONML 从 `h2` 开始
- 用户未要求正文 H1 时,JSONML 从 `h2` 开始;用户明确要求“正文一级标题/插入一级标题”时必须构造 `h1`
- 表格结构是 `table → tr → tc`(无 `th`/`td`)
- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色
- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增)
@@ -315,7 +313,7 @@ dws doc read --node <nodeId> --content-format jsonml --output /tmp/<name>-readba
- 只使用用户已提供或对话中已确认的正文素材。
- 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。
- **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。
- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。
- **`--name` 是外壳标题,不覆盖显式正文 H1**:用户未要求正文一级标题时从 `##` 开始;用户明确给出 `# ...` 或要求“先起一级标题”时,正文必须保留该 H1。
- 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。
- 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。
- 临时文件必须保留真实换行,不能把换行写成字面量 `\n`。
@@ -25,7 +25,7 @@
## 一、硬规则
1. **`--name` 是 H1**:正文从 `##` 开始;正文内不写 `#`(除非确需正文内再造一级 H1 并说明动机)
1. **用户显式正文 H1 优先**:`--name` 是文档外壳标题;用户未要求正文一级标题时从 `##` 开始。用户明确说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时必须保留或插入真实 H1,不得用 `--name` 代替
2. **同类信息同表达**:风险、状态、行动项、证据,每类只用一种元素 + 一种视觉语义(见 §5)
3. **Markdown 草稿阶段只用稳定元素**:标题、段落、列表、checklist、表格、代码块;callout / 分栏 / 附件 / 复杂嵌套留到创建后用 `doc block insert` / `doc media insert` 精修
4. **引用块只用于原文**:用户原话、会议摘录、外部材料原文;不许包装作者自己的结论
@@ -209,7 +209,7 @@
### 4.1 标题与段落
- 正文从 `##` 开始(H1 已被 `--name` 占用)
- 用户未要求正文 H1 时从 `##` 开始;用户明确要求正文 H1 时按原文使用 `#` 或 heading level 1
- 标题层级 ≤ 4 层(§7)
- 单段过长先拆段,再考虑换元素
@@ -217,6 +217,7 @@
- 普通列表:并列要点
- 有序列表:顺序步骤
- 用户明确要求“有序列表块”时必须使用真实列表结构;JSONML 为带 `list.isOrdered=true` 的多个 `p` 节点,不能只写带数字前缀的普通段落或以整篇 Markdown 代替显式 block insert
- checklist:待办状态(含 `- [ ]` / `- [x]`)
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
+150 -152
View File
@@ -1,181 +1,179 @@
#!/usr/bin/env python3
"""
在指定目录创建文档并写入 Markdown 内容(一键完成)
"""用原生 dws 写入管道创建文档,并回读验证。"""
用法:
python doc_create_and_write.py \
--name "项目周报" \
--content "# 本周总结\n\n## 完成事项\n- 任务A"
from __future__ import annotations
python doc_create_and_write.py \
--name "会议纪要" \
--content-file notes.md
python doc_create_and_write.py \
--name "知识库文档" --content "# 内容" --folder FOLDER_ID
python doc_create_and_write.py --name "test" --content "hello" --dry-run
"""
import sys
import json
import time
import subprocess
import argparse
import json
import shlex
import subprocess
import sys
import tempfile
from pathlib import Path
from typing import List, Any, Optional
from typing import Any, Optional, Sequence
def run_dws(
args: List[str], dry_run: bool = False,
) -> Optional[Any]:
cmd = ['dws'] + args
class ScriptError(RuntimeError):
"""可预期的脚本执行错误。"""
def decode_json_output(output: str) -> Any:
"""解析 JSON;兼容长内容写入前置的进度行。"""
text = output.strip()
if not text:
raise ScriptError("dws 未返回 JSON")
try:
return json.loads(text)
except json.JSONDecodeError:
decoder = json.JSONDecoder()
for offset, character in enumerate(text):
if character not in "[{":
continue
try:
value, end = decoder.raw_decode(text, offset)
except json.JSONDecodeError:
continue
if not text[end:].strip():
return value
raise ScriptError("dws 返回的不是合法 JSON")
def run_dws(args: Sequence[str], dry_run: bool = False) -> Any:
"""执行一条 dws 命令,并把命令/业务失败统一转成 ScriptError。"""
command = ["dws", *args]
if dry_run:
print(f"[dry-run] {' '.join(cmd)}")
return {'dry_run': True}
print(f"[dry-run] {shlex.join(command)}")
return {"dry_run": True}
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
command,
capture_output=True,
text=True,
timeout=120,
check=False,
)
if result.returncode != 0:
print(f" ✗ 错误:{result.stderr.strip()}")
return None
return json.loads(result.stdout)
except (subprocess.TimeoutExpired, json.JSONDecodeError,
FileNotFoundError) as e:
print(f" ✗ 错误:{e}")
return None
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
raise ScriptError(f"执行 dws 失败:{exc}") from exc
if result.returncode != 0:
detail = result.stderr.strip() or result.stdout.strip()
raise ScriptError(
f"dws 命令失败:{detail or f'退出码 {result.returncode}'}"
)
data = decode_json_output(result.stdout)
if isinstance(data, dict) and data.get("success") is False:
detail = data.get("errorMsg") or data.get("message") or "未知错误"
raise ScriptError(f"dws 业务调用失败:{detail}")
return data
def run_dws_with_retry(
args: List[str],
dry_run: bool = False,
max_retries: int = 3,
retry_delay: float = 1.0,
) -> Optional[Any]:
"""带重试机制的 dws 命令执行"""
last_error = None
for attempt in range(1, max_retries + 1):
result = run_dws(args, dry_run=dry_run)
if result is not None:
return result
if attempt < max_retries:
print(f" ⚠️ 第 {attempt} 次尝试失败,{retry_delay}秒后重试...")
time.sleep(retry_delay)
retry_delay *= 1.5 # 指数退避
return None
def first_value(payload: Any, keys: Sequence[str]) -> str:
"""从嵌套响应中提取第一个非空稳定字段。"""
if isinstance(payload, dict):
for key in keys:
value = payload.get(key)
if value is not None and str(value).strip():
return str(value).strip()
for value in payload.values():
found = first_value(value, keys)
if found:
return found
elif isinstance(payload, list):
for value in payload:
found = first_value(value, keys)
if found:
return found
return ""
def main():
def run(argv: Optional[Sequence[str]] = None) -> int:
parser = argparse.ArgumentParser(
description='创建文档并写入内容'
description="使用 dws doc create 创建文档并回读验证"
)
parser.add_argument('--name', required=True, help='文档名称')
parser.add_argument('--content', default='', help='Markdown 内容')
parser.add_argument('--content-file', default='', help='内容文件')
parser.add_argument('--folder', default='', help='目标文件夹 ID 或 URL')
parser.add_argument('--workspace', default='', help='目标知识库 ID')
parser.add_argument(
'--mode', default='append', choices=['overwrite', 'append'],
help='写入模式: overwrite=覆盖, append=追加 (默认 append)',
parser.add_argument("--name", required=True, help="文档名称")
content_group = parser.add_mutually_exclusive_group(required=True)
content_group.add_argument("--content", help="Markdown 内容")
content_group.add_argument("--content-file", help="UTF-8 Markdown 文件")
location_group = parser.add_mutually_exclusive_group()
location_group.add_argument(
"--folder", default="", help="目标文档文件夹 ID 或 URL"
)
parser.add_argument(
'--max-retries', type=int, default=3,
help='每块写入失败时的最大重试次数 (默认 3)',
location_group.add_argument(
"--workspace", default="", help="目标知识库 ID 或 URL"
)
parser.add_argument('--dry-run', action='store_true')
args = parser.parse_args()
parser.add_argument("--dry-run", action="store_true")
args = parser.parse_args(argv)
content = args.content
supplied_path: Optional[Path] = None
temporary_path: Optional[Path] = None
if args.content_file:
p = Path(args.content_file)
if not p.exists():
print(f"错误:文件不存在: {p}")
sys.exit(1)
content = p.read_text(encoding='utf-8')
if not content:
print('错误:需要 --content 或 --content-file')
sys.exit(1)
chunk_size = 30000
supplied_path = Path(args.content_file)
if not supplied_path.is_file():
raise ScriptError(f"内容文件不存在:{supplied_path}")
elif not args.content or not args.content.strip():
raise ScriptError("--content 不能为空")
create_args = ['doc', 'create', '--name', args.name, '--format', 'json']
if args.folder:
create_args.extend(['--folder', args.folder])
if args.workspace:
create_args.extend(['--workspace', args.workspace])
try:
if supplied_path is None and not args.dry_run:
with tempfile.NamedTemporaryFile(
mode="w", encoding="utf-8", suffix=".md", delete=False
) as handle:
handle.write(args.content)
temporary_path = Path(handle.name)
supplied_path = temporary_path
print(f'\n📝 创建文档: {args.name}')
create_data = run_dws(create_args, dry_run=args.dry_run)
content_path = str(supplied_path) if supplied_path else "<TEMP_CONTENT.md>"
create_args = [
"doc", "create",
"--name", args.name,
"--content-file", content_path,
"--content-format", "markdown",
"--format", "json",
]
if args.folder:
create_args.extend(["--folder", args.folder])
if args.workspace:
create_args.extend(["--workspace", args.workspace])
node_id = None
if not args.dry_run:
if not create_data:
sys.exit(1)
node_id = (create_data.get('nodeId')
or create_data.get('dentryUuid')
or create_data.get('id', ''))
print(f" ✓ 文档已创建 (ID: {node_id})")
created = run_dws(create_args, dry_run=args.dry_run)
node_id = "<NODE_ID>" if args.dry_run else first_value(
created, ("nodeId", "dentryUuid")
)
if not node_id:
raise ScriptError("文档创建响应缺少 nodeId,无法验证")
if len(content) <= chunk_size:
mode_label = '追加' if args.mode == 'append' else '覆盖'
print(f'\n✍️ 写入内容 (模式: {mode_label}, {len(content)} 字符)...')
write_data = run_dws([
'doc', 'update',
'--node', node_id or '<NODE_ID>',
'--content', content,
'--mode', args.mode,
'--format', 'json',
], dry_run=args.dry_run)
if write_data:
print(f" ✓ 内容已写入 ({len(content)} 字符)")
else:
chunks = []
pos = 0
while pos < len(content):
end = min(pos + chunk_size, len(content))
if end < len(content):
newline_pos = content.rfind('\n', pos, end)
if newline_pos > pos:
end = newline_pos + 1
chunks.append(content[pos:end])
pos = end
info = run_dws(
["doc", "info", "--node", node_id, "--format", "json"],
dry_run=args.dry_run,
)
run_dws(
["doc", "read", "--node", node_id, "--format", "json"],
dry_run=args.dry_run,
)
if args.dry_run:
return 0
total_chunks = len(chunks)
print(f'\n✍️ 内容较长 ({len(content)} 字符), 分 {total_chunks} 块写入...')
success_chunks = 0
for idx, chunk in enumerate(chunks):
chunk_mode = args.mode if idx == 0 else 'append'
write_data = run_dws_with_retry(
[
'doc', 'update',
'--node', node_id or '<NODE_ID>',
'--content', chunk,
'--mode', chunk_mode,
'--format', 'json',
],
dry_run=args.dry_run,
max_retries=args.max_retries,
)
if write_data:
print(f" ✓ 块 {idx + 1}/{total_chunks} 已写入 ({len(chunk)} 字符)")
success_chunks += 1
elif not args.dry_run:
# 写入失败,报告部分写入状态
print(f"\n❌ 块 {idx + 1}/{total_chunks} 写入失败(已重试 {args.max_retries} 次)")
print(f"\n⚠️ 文档处于部分写入状态:")
print(f" - 文档 ID: {node_id}")
print(f" - 已写入: {success_chunks}/{total_chunks} 块")
print(f" - 失败位置: 第 {idx + 1} 块")
if args.mode == 'overwrite':
print(f" - 模式: 覆盖模式,文档可能包含不完整内容")
print(f" - 建议: 手动检查文档内容,或删除后重新创建")
else:
print(f" - 模式: 追加模式,已写入内容已保存")
print(f" - 建议: 可手动补充剩余内容,或重新运行脚本")
sys.exit(1)
print('\n✅ 完成!')
summary = {
"success": True,
"nodeId": node_id,
"docUrl": first_value(info, ("docUrl", "documentUrl", "url"))
or first_value(created, ("docUrl", "documentUrl", "url")),
"chunksWritten": first_value(created, ("chunksWritten",)),
"verified": True,
}
print(json.dumps(summary, ensure_ascii=False))
return 0
finally:
if temporary_path is not None:
temporary_path.unlink(missing_ok=True)
if __name__ == '__main__':
def main() -> None:
try:
raise SystemExit(run())
except ScriptError as exc:
print(f"错误:{exc}", file=sys.stderr)
raise SystemExit(1) from exc
if __name__ == "__main__":
main()
+65 -125
View File
@@ -1,6 +1,6 @@
---
name: dingtalk-aitable
description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/数据表/base/table/建表/查记录/写数据/字段/记录增删改查/筛选/排序/公式/模板搜索/批量导入CSV或JSON/导出/仪表盘/图表/上传附件到表格/按字段类型建表。不做电子表格单元格读写(走 dingtalk-misc)、文档编辑(走 dingtalk-doc);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。
description: 钉钉 AI 表格(多维表)。Use when 用户说 AI表格/多维表/Base/数据表/字段/记录增删改查/筛选排序/公式与跨表引用/视图表单/仪表盘图表/高级权限/自动化工作流/模板/CSV或JSON批量导入/Excel导入导出/记录附件。不做电子表格单元格读写与工作表公式(走 dingtalk-sheet)、普通文档编辑(走 dingtalk-doc)或钉盘文件管理(走 dingtalk-drive);听记待办入表先用 dingtalk-minutes 提取,再由本 skill 写入。命令前缀:dws aitable。
metadata:
cli_version: ">=0.2.14"
category: product
@@ -15,146 +15,86 @@ metadata:
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dws-shared`](../dws-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
> 命令参考:[aitable.md](references/aitable.md);复杂命令按需加载 `references/aitable/*.md`;剧本:[06-data-analytics.md](references/06-data-analytics.md)。
## 加载与路由顺序
1. 命中下方高频意图时直接使用精确骨架,不先查 Help 或产品级 Schema。
2. 路由优先级固定为:精确 recipe / 可运行脚本 > 匹配的公开 Shortcut > 原子命令。命令已确定且参数清楚时直接执行。
3. 参数、约束或安全语义不确定时只读 leaf Schema:`dws schema --cli-path "aitable <leaf>" --format json`;只有当前 Cobra flag 不确定时才读对应 `--help`。
4. 复杂字段、筛选、导入导出、视图、权限或工作流任务,按“低频能力与 Reference”只加载相关文件,不预读整个 `references/aitable/`。
5. 现有骨架和 reference 都无法定位能力时,才用 Runtime Shortcut Catalog 做最后发现;不得猜 `cli_path` 或 flag。
6. Schema、Help、reference 与实际返回冲突时采用更安全的解释并报告契约漂移;`confirmation=user_required` 时先确认,再添加 `--yes`。
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcuts(无专用脚本/recipe 时优先)
## Shortcut 发现(按需)
以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 leaf Schema(例如 `dws schema --cli-path "aitable +<shortcut>" --format json`),在当前 Cobra flags 不确定时读取 `dws aitable <shortcut> --help`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service aitable --format json` 批量发现。
`aitable` 当前有 29 条公开 shortcut。完整清单保留在 Runtime Shortcut Catalog;已完成 Schema curation 的子集可通过 leaf Schema 查询。高频产品根 Skill 不重复展开完整清单。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
| Shortcut | 风险 | 适用场景 |
|---|---|---|
| `dws aitable +base-get` | read | 获取指定 Base 的目录信息(tables / dashboards summary) |
| `dws aitable +base-list` | read | 获取当前用户可访问的 AI 表格 Base 列表(最近访问,支持游标分页) |
| `dws aitable +base-search` | read | 按名称关键词搜索 AI 表格 Base |
| `dws aitable +chart-get` | read | 获取指定 chart 的详细信息 |
| `dws aitable +chart-widgets-example` | read | 获取所有图表类型的 widget config 示例 |
| `dws aitable +dashboard-config-example` | read | 获取 dashboard config 的结构示例 |
| `dws aitable +dashboard-get` | read | 获取指定 dashboard 的详细信息(含 charts summary) |
| `dws aitable +field-get` | read | 批量获取字段详情(含类型相关完整配置) |
| `dws aitable +find-record` | read | 在指定多维表里按关键词查记录(只读) |
| `dws aitable +form-field-list` | read | 列出表单视图当前可见的字段及其配置 |
| `dws aitable +form-list` | read | 列出指定数据表下的所有表单视图 |
| `dws aitable +form-share-get` | read | 读取视图当前的分享表单配置 |
| `dws aitable +list-tables` | read | 列出某个多维表(base)里的所有数据表(只读,投影 tableId/tableName) |
| `dws aitable +record-history-list` | read | 按 recordId 查询单条记录的变更历史 |
| `dws aitable +record-query` | read | 查询表格记录(按 ID 取 / 条件筛选 / 关键词 / 分页) |
| `dws aitable +record-query-empty` | read | 扫描并过滤出完全没填用户字段的空行 |
| `dws aitable +record-share-links` | read | 批量(可 >20 条)获取多维表记录分享链接:去重+分片+合并 |
| `dws aitable +record-share-url` | read | 按 recordId 批量获取记录分享链接,单次最多 20 条 |
| `dws aitable +resolve-base` | read | 按名称搜索多维表 Base 并解析出唯一 baseId(只读) |
| `dws aitable +resolve-table` | read | 在某个多维表 Base 内按名称解析出唯一的数据表 tableId(只读) |
| `dws aitable +role-list` | read | 列出指定 Base 下的全部角色 |
| `dws aitable +section-list-empty` | read | 列出指定 Base 下所有没有子节点的空文件夹 |
| `dws aitable +section-list-nodes` | read | 列出指定 Base 当前版本下的全部 nsheet 节点 |
| `dws aitable +table-get` | read | 批量获取指定数据表的表级信息、字段目录与视图目录 |
| `dws aitable +template-search` | read | 按名称关键词搜索 AI 表格模板 |
| `dws aitable +view-get` | read | 获取视图完整信息(列顺序、筛选、排序、分组等) |
| `dws aitable +view-get-frozen-cols` | read | 获取视图当前冻结的左侧列数 |
| `dws aitable +view-get-lock` | read | 获取视图锁定状态 |
| `dws aitable +view-get-row-height` | read | 获取视图单元格行高(像素) |
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service aitable --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## 意图表
## 核心对象与 ID
| 用户说 | 命令 |
|--------|------|
| "搜表格 / 找一个 base" | `dws aitable base search --query "<名>"` |
| "创建 AI 表格 / 多维表" | `dws aitable base create --name "<名称>" [--template-id <id>]` |
| "查数据表 / 建数据表" | `dws aitable table get --base-id <baseId>` / `dws aitable table create --base-id <baseId> --name "<表名>" --fields '[...]'` |
| "查字段 / 字段类型" | `dws aitable field get --base-id <id> --table-id <id>` |
| "查记录 / 搜索记录" | `dws aitable record query --base-id <baseId> --table-id <tableId> [--filters '...']` |
| "写记录 / 更新记录 / 删除记录" | `dws aitable record create/update/delete --base-id <baseId> --table-id <tableId> ...` |
| "筛选 / 排序 / 公式 / 跨表引用" | 先读 `references/aitable/aitable-filter-sort.md` / `aitable-formula-guide.md` |
| "批量导入 JSON / CSV" | `python scripts/import_records.py <baseId> <tableId> data.csv\|data.json` |
| "批量加字段" | `python scripts/bulk_add_fields.py --base-id <id> --table-id <id> --fields fields.json` |
| "导入 / 导出表格" | 先读 `references/aitable/aitable-export-import.md`;导出优先 `python scripts/aitable_export_via_task.py <baseId> --scope table --table-id <tableId>` |
| "仪表盘 / 图表" | 先读 `references/aitable/aitable-dashboard-chart.md` |
| "上传附件到记录" | 先读 `references/aitable/aitable-attachment.md`;可用 `python scripts/upload_attachment.py --base-id <id> --file <path>` |
| 对象 | 标识与执行边界 |
|---|---|
| Base | `baseId` 标识一个 AI 表格文件;名称只用于搜索或消歧,不能当 ID |
| Table | `tableId` 标识 Base 内的数据表;必须来自 `+resolve-table` / `+table-get` / 创建返回 |
| Field | `fieldId` 标识列;写入、筛选、排序和字段变更优先使用真实 `fieldId` |
| Record | `recordId` 标识行;更新、删除和分享前必须先查询得到真实 ID |
| View / Dashboard / Chart | `viewId` / `dashboardId` / `chartId` 各自绑定当前 Base/Table,不跨对象复用 |
| 异步任务 | `taskId` / `importId` 只用于对应导出或导入任务,不能替代业务对象 ID |
## 标准 SOP(必遵流程)
所有下游 ID 都从当前链路的结构化返回中提取;同名多候选必须让用户消歧,不默认取第一项,也不复用未经本轮校验的旧 ID。
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 flag/ID。每条命令必须带 `--format json`,执行后必须按"解析"步取真实字段,不得凭返回结构猜测。`baseId`/`tableId`/`fieldId`/`recordId` 一律先查后用,**禁止默认/编造**。
## 核心意图与执行骨架
### SOP-1 定位 Base 与 Table(list / search → table get)
| 用户意图 | 首选骨架 | 必须保留的执行边界 |
|---|---|---|
| 按名称找 Base | `dws aitable +resolve-base --name "<名称>" --format json` | 唯一命中才继续;多候选停止并消歧 |
| 浏览最近访问 | `dws aitable +base-list --format json` | 只代表最近访问,不得宣称全量 |
| 按名称找 Table | `dws aitable +resolve-table --base <baseId> --name "<表名>" --format json` | `baseId` 必须来自上一步真实返回 |
| 取表、字段与视图目录 | `dws aitable +table-get --base-id <baseId> [--table-ids <tableId>] --format json` | `tables[].fields[]` 是字段目录;完整类型/config 再用 `+field-get` |
| 取字段完整配置 | `dws aitable +field-get --base-id <baseId> --table-id <tableId> [--field-ids <ids>] --format json` | 写入前核对类型、只读性和 select options;按需展开以控制返回体 |
| 查/搜/筛记录 | `dws aitable +record-query --base-id <baseId> --table-id <tableId> [--query <词>\|--filters '<JSON>'\|--record-ids <ids>] --format json` | ID 模式忽略 filter/sort;全量结论必须完整分页 |
| 新增记录 | `dws aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":<值>}}]' --format json` | 单次最多 100;取 `data.newRecordIds[]` 后立即按 ID 回读 |
| 更新记录 | `dws aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"<id>","cells":{"<fieldId>":<值>}}]' --format json` | 先 query 拿 recordId;只传需改字段;取 `data.recordIds[]` 后回读 |
| 删除记录 | 先 `dws aitable +record-query ...` 定位,再 `dws aitable record delete --base-id <baseId> --table-id <tableId> --record-ids <ids>` | 展示目标与影响,得到明确确认后才加 `--yes` |
| 创建 Base / Table | `dws aitable base create --name "<名>"` / `dws aitable table create --base-id <id> --name "<名>" --fields '[...]'` | 使用创建返回的真实 ID;系统改名/加后缀时不得继续猜原名 |
| 批量追加 CSV / JSON 到已有表 | `python3 scripts/import_records.py <baseId> <tableId> <file> [batch_size]` | CSV 表头必须是 fieldId;脚本返回不完整 ledger 时不得宣称全成功 |
| 文件导入为新数据表 | `python3 scripts/aitable_import_via_task.py <baseId> <file>` | 与“追加已有 table”不同;走 prepare → PUT → import task |
| 批量创建字段 | `python3 scripts/bulk_add_fields.py <baseId> <tableId> fields.json` | 单次最多 15;逐项检查成功/失败结果 |
| 导出 Base / Table / View | `python3 scripts/aitable_export_via_task.py <baseId> --scope all\|table\|view [...]` | 保存路径、覆盖与异步未完成状态必须显式处理 |
| 上传记录附件 | `python3 scripts/upload_attachment.py <baseId> <file>` | 返回 `fileToken` 后仍需按字段格式写入记录并回读 |
**触发**:找/打开某张 AI 表格、不知 baseId 或 tableId。
## 记录读写不变量
1. **选源(必须)**:有名称/关键词 → `dws aitable base search --query "<名称>"`;列最近访问 → `dws aitable base list`。`base list` 仅返回最近访问,不是全部,**禁止**当作全量清单。
2. **执行(必须)**:`dws aitable base search --query "<完整名>" --format json`(或 `dws aitable base list --format json`)。
3. **解析(必须)**:从 JSON 取真实 `baseId`;**多候选必须输出让用户选,禁止默认取第一个**。
4. **取 tableId(必须)**:`dws aitable table get --base-id <baseId> --format json` → 从 `data.tables[].tableId` 取目标表 ID,并记录 `views[]`。枚举模式不返回 `fields[]`;需要字段目录时必须继续执行 SOP-2 的 `field get`。若只核对某张表,可显式加 `--table-ids <tableId>` 控制返回体。
5. **失败(必须)**:`base list` 为空或不命中 → 换 `base search --query` 关键词重试一次;仍无果**必须如实告知**,禁止臆造 baseId/tableId。
- `record create/update` 前必须获取目标字段的 `fieldId`、`type` 与 `config`;`filterUp`、`lookup` 等只读字段不可写。完整格式只在需要时读 [aitable-cell-value.md](references/aitable/aitable-cell-value.md)。
- 筛选和排序字段使用 `fieldId`;`--filters` 最外层是 `and|or + operands`,`--sort` 使用 `direction: asc|desc`。日期和跨表字段规则按需读 [aitable-filter-sort.md](references/aitable/aitable-filter-sort.md)。
- `record query --all` 仍受 `--page-limit` 约束;分页中断或局部富化失败时保留已有结果,输出 completeness 与逐项失败 ledger,不把部分结果描述为全量。
- 创建、更新、导入、批量建字段等写操作必须检查业务 `status`、逐项结果与返回 ID;普通写入按用户明确要求执行后回读,不能只凭退出码宣称成功。
- 长 JSON 使用 `--records-file` / 任务文件;不得为绕过字段错误而静默丢列、改类型或删除失败项。
**禁止**:跳过 `table get` 直接用字段名写记录、用模糊名匹配当 baseId、用旧会话里的 ID 不再校验。
## 低频能力与 Reference
### SOP-2 拿字段定义(field get,写记录/改字段前置)
| 场景 | 按需读取 |
|---|---|
| 完整命令索引、对象 URL 与一级路由 | [aitable.md](references/aitable.md) |
| 记录 query/create/update/delete/upsert/history/share | 对应 `references/aitable/aitable-record-*.md` |
| 字段创建、字段 config、cellValue、公式与跨表引用 | [aitable-field.md](references/aitable/aitable-field.md)、[aitable-field-properties.md](references/aitable/aitable-field-properties.md)、[aitable-cell-value.md](references/aitable/aitable-cell-value.md)、[aitable-formula-guide.md](references/aitable/aitable-formula-guide.md) |
| 筛选、排序、统计、全量分析 | [aitable-filter-sort.md](references/aitable/aitable-filter-sort.md)、[aitable-data-analysis-sop.md](references/aitable/aitable-data-analysis-sop.md) |
| 导入导出、附件 | [aitable-export-import.md](references/aitable/aitable-export-import.md)、[aitable-attachment.md](references/aitable/aitable-attachment.md) |
| 视图、表单、仪表盘与图表 | [aitable-view-config.md](references/aitable/aitable-view-config.md)、[aitable-view-extras.md](references/aitable/aitable-view-extras.md)、[aitable-form.md](references/aitable/aitable-form.md)、[aitable-dashboard-chart.md](references/aitable/aitable-dashboard-chart.md) |
| 高级权限、自动化工作流、导航节点 | [aitable-advperm.md](references/aitable/aitable-advperm.md)、[aitable-workflow.md](references/aitable/aitable-workflow.md)、[aitable.md](references/aitable.md) 的 section 路由 |
**触发**:建/改/写记录、改字段名或 options、按字段类型拼写入参前。
## 错误恢复
1. **前置(必须)**:先按 SOP-1 拿到 `baseId` + `tableId`。
2. **执行(必须)**:`dws aitable field get --base-id <baseId> --table-id <tableId> --format json`(仅展开需要的字段时加 `--field-ids fld1,fld2`,单次最多 10 个)。
3. **解析(必须)**:取每个目标字段的 `fieldId`、`type`、`config`(如 singleSelect/multipleSelect 的 `options[].id|name`);写入 cells 的 key **必须用 `fieldId`**,不是字段中文名;select 字段过滤/写入传**选项名称字面量**,不传 option ID。
4. **衔接(必须)**:拿到字段定义 → 进入 SOP-3 写记录、或 `dws aitable field update --field-id <fieldId> --name <新名>|--config <JSON> --format json` 改字段。
5. **失败(必须)**:字段不存在或类型不符 → 重新 `field get` 核对,**禁止**凭旧名称/旧类型继续写入。
**禁止**:用字段中文名当 cells key、跳过 `field get` 直接 `record create/update`、对 select 字段传 option ID 当写入值。
### SOP-3 写/批量写记录(record create)
**触发**:新增记录、批量加数据、CSV/JSON 入表。
1. **前置(必须)**:SOP-1 取 `baseId`/`tableId` + SOP-2 取 `fieldId`/类型。
2. **执行(必须)**:`dws aitable record create --base-id <baseId> --table-id <tableId> --records '[{"cells":{"<fieldId>":<值>}}]' --format json`;单次最多 100 条,超长用 `--records-file ./data.json`。
3. **写入格式(必须)**:按 `record create --help` 类型表严格传值(text→字符串、number→数值、singleSelect→"选项名"、date→RFC3339、url→`{"text","link"}`、group→`{"cid"}` 等);`filterUp`/`lookup` 字段只读不可写。
4. **解析与验证(必须)**:从返回 `data.newRecordIds[]` 取全部新记录 ID;不要读取不存在的标量 `recordId`。立即执行 `dws aitable record query --base-id <baseId> --table-id <tableId> --record-ids <id1,id2,...> --format json` 回读写入值。
5. **失败(必须)**:类型/格式错误按返回报错修正后重试,**禁止**降级丢弃字段;不确定格式先 `field get` 复核 config。
**禁止**:编造 fieldId/recordId、跳过 `field get` 凭中文名写、把 URL 字符串直接塞给 url 字段。
### SOP-4 查/筛/排记录(record query)
**触发**:查记录、按条件筛选、排序、取关联记录、定位待改/待删的 recordId。
1. **前置(必须)**:SOP-1 拿 `baseId`/`tableId`。
2. **执行(必须)**:`dws aitable record query --base-id <baseId> --table-id <tableId> --format json`;已知 ID 直取加 `--record-ids rec1,rec2`(忽略 filters/sort,单次≤100)。
3. **筛选/排序(必须)**:`--filters` 最外层必须 `{"operator":"and|or","operands":[...]}`,select 字段值传**选项名字面量**;日期只能用 `date_eq/before/after/not_before/not_after`,范围用 `not_before`+`not_after` 组合,**禁止** `eq`/区间/相对时间。`--sort` 用 `[{"fieldId":"..","direction":"asc|desc"}]`(**必须用 `direction`**)。公式/引用/关联字段默认不返回,需显式 `--field-ids` 指定。
4. **解析(必须)**:取真实 `recordId` 与字段值;分页用 `--cursor`,全表用 `--all --page-limit N`。
5. **衔接(必须)**:拿到 recordId → SOP-5 更新、`record delete --record-ids --yes` 删除(删前确认)。
**禁止**:用字段名做 filter/sort key、对日期用 `eq`、漏掉 `direction` 用旧 `order` 字段、用本地过滤替代服务端 filter。
### SOP-5 更新记录(record update)
**触发**:改记录字段值、批量更新状态、单字段重命名需求之外的记录改动。
1. **前置(必须)**:SOP-1 拿 `baseId`/`tableId`;SOP-2 拿字段类型;SOP-4 拿目标 `recordId`。
2. **执行(必须)**:`dws aitable record update --base-id <baseId> --table-id <tableId> --records '[{"recordId":"recXXX","cells":{"<fieldId>":<新值>}}]' --format json`(每条必含 `recordId`+`cells`,单次≤100;超长用 `--records-file`);只传需改字段,未传保持原值。
3. **解析与验证(必须)**:写入格式同 SOP-3;从返回 `data.recordIds[]` 取实际更新的记录 ID。更新响应不返回“受影响字段”,必须立即用 `record query --record-ids <id1,id2,...> --format json` 回读目标字段确认。
4. **失败(必须)**:recordId 不存在或类型不符 → 回 SOP-4 重新定位,**禁止**编造 ID 强写。
**禁止**:省略 `recordId`、用字段中文名当 cells key、凭空猜测 recordId 直接 update。
## 危险操作
`base delete` / `table delete` / `field delete` / `record delete` 不可逆,必须先向用户确认再加 `--yes`。
## 高频硬约束
- 创建/改字段/写记录是多轮连续任务时,不能在"让我执行/先获取 ID"后停下;必须实际调用对应 `dws aitable` 命令并验证结果。
- 字段重命名使用 `dws aitable field update --base-id <baseId> --table-id <tableId> --field-id <fieldId> --name "<新名称>" --format json`;先 `field get` 找真实 `fieldId`,不要猜字段名能直接更新。
- 写记录前必须 `field get` 获取 `fieldId` 与类型;`record create/update` 的 `cells` key 用 `fieldId`,不是字段中文名。长 JSON 使用 `--records-file`。
- 表或字段创建返回名称被系统自动加后缀时,后续必须使用返回的真实 `tableId`/`fieldId`,不要继续按原名称猜。
- `record update/delete` 先 `record query/list` 定位 `recordId`;删除必须确认,普通新增/更新按用户明确要求可直接执行后读回验证。
- `record query/create/update/delete`、`field create`、导入导出、图表和附件场景必须先读对应 `references/aitable/*.md`,不要凭旧单文件参数猜 flag。
## 字段类型规则
详见本 skill 的 [field-rules.md](references/field-rules.md)。
- 路径或 flag 错误:按既定的 leaf Schema → leaf Help 顺序校正一次;仍失败则停止,不连续尝试猜测别名。
- 命令非零、输出非 JSON、业务 `status != success`、必需 ID 缺失、批处理部分失败均视为失败;保留成功项与 ledger,禁止吞错。
- 同名歧义、权限不足、资源不存在、字段类型漂移、分页无法推进或 Schema/Help 冲突时停止并报告。具体恢复动作按需读 [aitable-error-recovery.md](references/aitable/aitable-error-recovery.md)。
- 每次重试都从最新实际输出重新提取下游 ID;删除和其他 `confirmation=user_required` 操作不得自动重试或静默确认。
## 跨产品协作
- 单元格 / 工作表 / 公式 → 切到 `dingtalk-misc`(`references/sheet.md`,命令前缀:`dws sheet`)
## 局部意图
- [局部意图消歧](references/intent-guide.md)。
- 电子表格工作表、单元格与公式 → `dingtalk-sheet`;结构化 Base/Table/Field/Record 才走本 skill。
- 普通文档内容 → `dingtalk-doc`;钉盘普通文件与文件夹 → `dingtalk-drive`;记录附件上传仍走本 skill。
- 用户直接提供类型不明的 alidocs URL 时,按 `dws-shared` 的 URL 预检导航确认 `extension=able` 后再执行。
- 听记内容入表:先用 `dingtalk-minutes` 提取结构化结果,再按本 skill 的字段与记录规则写入。
@@ -2,6 +2,8 @@
> **渐进式文档**:本文件为路由层(索引 + 意图判断),各命令的详细参数、示例和踩坑说明在 [aitable/](./aitable/) 目录下按需加载。
已知高频意图优先使用根 Skill 的精确 Shortcut/脚本骨架;本文件只在需要完整一级命令索引、对象 URL 或低频分支导航时加载。参数与安全不确定时读 leaf Schema,Cobra flag 不确定时才读 leaf Help,不要把本文件当作参数事实源。
## 文档地址 (URI)
| 资源 | URI 格式 |
@@ -35,7 +37,7 @@
| 命令 | 用途 | 必填参数 | 路由提醒 |
|------|------|----------|----------|
| `table get` | 获取数据表/视图目录 | `--base-id` | 不传 `--table-ids` 枚举全部表,但不返回字段;字段目录使用 `field get` |
| `table get` | 获取表级信息、字段目录与视图目录 | `--base-id` | 不传 `--table-ids` 枚举全部表并返回精简 `fields[]`/`views[]`;字段完整 config 使用 `field get` |
| `table create` | 创建数据表 | `--base-id` `--name` `--fields` | fields 为 JSON 数组,至少 1 个 |
| `table update` | 修改表名 / 备注 / 行命名规则 | `--base-id` `--table-id` + 三选一(`--name` / `--description` / `--record-name-key`) | `--record-name-key` 是固定枚举(如 task/project/event/customer/ji_lu 等),非字段 ID |
| `table delete` | 删除表 | `--base-id` `--table-id` | 不可逆 |
@@ -427,17 +429,18 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
## 核心工作流
```bash
# 1. 搜索/列出 Base — 提取 baseId
dws aitable base search --query "项目" --format json
# 1. 按名称解析唯一 Base — 提取 baseId;多候选必须消歧
dws aitable +resolve-base --name "项目" --format json
# 2. 获取 Base 信息 — 提取 tableId
dws aitable base get --base-id <BASE_ID> --format json
# 2. 按名称解析唯一 Table — 提取 tableId
dws aitable +resolve-table --base <BASE_ID> --name "任务" --format json
# 3. 获取字段目录 — 提取 fieldId
dws aitable field get --base-id <BASE_ID> --table-id <TABLE_ID> --format json
# 3. 获取字段目录;需要完整类型 config 时再调用 field get
dws aitable +table-get --base-id <BASE_ID> --table-ids <TABLE_ID> --format json
dws aitable +field-get --base-id <BASE_ID> --table-id <TABLE_ID> --format json
# 4. 查询记录
dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> --format json
dws aitable +record-query --base-id <BASE_ID> --table-id <TABLE_ID> --format json
# 5. 新增记录 (cells 用 fieldId 作 key)
dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
@@ -452,8 +455,8 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
| `base create` | `baseId` | 后续命令 + 文档 URI |
| `base get` | `tables[].tableId` | --table-id,拼接指定数据表 URI |
| `table create` | `tableId` | 后续命令 + 拼接指定数据表 URI |
| `table get` | `tables[].tableId`、视图目录 | 定位数据表和视图;字段需继续调用 `field get` |
| `field get` | `fields[].fieldId` | record 操作的 cells key, field update/delete |
| `table get` | `tables[].tableId`、精简 `fields[]`、`views[]` | 定位数据表、字段目录和视图;完整字段 config 再用 `field get` |
| `field get` | `fields[].fieldId/type/config` | record 操作的 cells key、类型校验、field update/delete |
| `record query` | `recordId` | record update/delete;按 ID 反查字段值用 `record get` |
| `template search` | `templateId` | base create --template-id,拼接模板预览 URI |
@@ -480,6 +483,7 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
|------|------|
| [bulk_add_fields.py](../scripts/bulk_add_fields.py) | 批量添加字段 |
| [import_records.py](../scripts/import_records.py) | 从 JSON/CSV 批量导入记录 |
| [aitable_import_via_task.py](../scripts/aitable_import_via_task.py) | 导入 CSV/XLS/XLSX 并新建数据表(prepare + PUT + import) |
| [aitable_export_via_task.py](../scripts/aitable_export_via_task.py) | 文件导出(export_data 轮询 + 下载) |
| [upload_attachment.py](../scripts/upload_attachment.py) | 上传附件到 AI 表格记录 |
@@ -32,6 +32,7 @@ dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \
```
> `uploadUrl` 有时效性(`expiresAt`),脚本会自动在获取后立即上传。
> 脚本只接受成功的业务响应和 HTTPS `uploadUrl`;它返回 `fileToken` 不等于记录已经写入,后续仍需执行 record create/update 并按 recordId 回读。
## 手动流程(不使用脚本)
@@ -15,6 +15,7 @@
- `status: "error"` 表示操作失败
- `summary` 包含错误摘要信息
- `trace_id` 用于问题追踪
- 命令退出码为 0 但 `status != "success"`、必需 ID 缺失或逐项结果含失败,也属于业务失败
## 2. 常见错误与恢复动作
@@ -42,7 +43,7 @@
|-------------------|------|---------|
| `base not found` | base-id 错误或无权限 | 确认 base-id 正确;尝试 `base list` 或 `base search` 重新定位 |
| `table not found` | table-id 错误 | 用 `table get --base-id <baseId>` 不带 table-ids 查看所有表 |
| 表名重复 | 同 Base 下已存在同名表 | 系统会自动续号(如"原名 1"),无需额外处理 |
| 表名重复 | 同 Base 下已存在同名表 | 使用创建返回的真实 `tableId/tableName`;禁止继续按原名猜测目标 |
### 2.4 视图操作错误
@@ -57,7 +58,7 @@
|-------------------|------|---------|
| filters 无效被忽略 | 根节点不是 and/or,或 operands 格式错误 | 确保 filters 根节点是 `{"operator":"and"/"or", "operands":[...]}` 结构 |
| sort 无效 | fieldId 不存在 | 先 `field get` 确认字段 ID |
| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;注意 singleSelect 筛选值用 option name 或 id |
| 筛选结果为空 | 条件过严或字段值不匹配 | 放宽条件验证;singleSelect/multipleSelect 筛选值使用 option name 字面量 |
### 2.6 导入导出错误
@@ -65,6 +66,7 @@
|-------------------|------|---------|
| 导出任务超时 | 数据量大,异步任务未完成 | 用 `export data --task-id <taskId>` 轮询直到完成 |
| 导入文件格式错误 | 不支持的文件格式或文件损坏 | 确认文件为 .xlsx 格式且未加密 |
| 批处理只成功一部分 | 某批或某个字段返回失败 | 保留成功 ID,输出失败 batch/item ledger,以非零状态结束;不要整批重放 |
## 3. 重试策略
@@ -106,18 +108,6 @@ dws aitable record create \
`--verbose` 会输出请求/响应的详细信息,帮助定位问题。
### 4.2 使用 --dry-run 预览
```bash
dws aitable record create \
--base-id <baseId> \
--table-id <tableId> \
--records '[...]' \
--dry-run --format json
```
`--dry-run` 只预览不执行,适合在不确定参数是否正确时先验证。
## 5. 错误预防最佳实践
1. **写记录前先读字段结构** — `field get` 确认字段类型和 ID
@@ -4,6 +4,8 @@
`export data` 为异步任务:首次调用可能只返回 `taskId`,需要继续轮询。
优先使用 `python3 scripts/aitable_export_via_task.py <baseId> --scope all|table|view [...]`:它检查业务状态、持续轮询、要求 HTTPS 下载地址,并在本地文件已存在时停止而不是静默覆盖。只有需要控制底层轮询参数时才走下面的原子命令。
> ⚠️ **`--format` 冲突警告**:`export data` 的 `--format` 是**导出格式**(excel/attachment 等),不是全局输出格式。**此命令禁止追加全局 `--format json`**,否则会覆盖导出格式导致 `INVALID_EXPORT_FORMAT` 错误。输出默认就是 JSON,无需额外指定。
```bash
@@ -28,6 +30,8 @@ dws aitable export data --base-id <BASE_ID> --task-id <TASK_ID> --timeout-ms 300
> **无需手动解析 CSV/Excel 再逐条 record create**,效率极低且容易出错。
新建数据表导入优先使用 `python3 scripts/aitable_import_via_task.py <baseId> <file>`,脚本封装 prepare → PUT → import 并检查每一步业务状态。追加到已有表且需要字段级类型控制时,使用 `python3 scripts/import_records.py <baseId> <tableId> <file> [batch_size]`;二者语义不同,不要自动互换。
```bash
# 第 1 步:申请上传凭证
dws aitable import upload --base-id <BASE_ID> \
@@ -14,7 +14,7 @@ Flags:
--table-id string Table ID (必填)
```
返回字段的完整配置(含 options 等)。不要假设未指定 `--table-ids` 的 `table get` 枚举结果含字段;字段目录和配置以 `field get` 返回为准。
返回字段的完整配置(含 options 等)。`table get` 会返回精简字段目录,但写入、筛选或修改前需要类型相关完整 `config` 时,以 `field get` 返回为准。
## field create — 创建字段
@@ -47,6 +47,8 @@ dws aitable record query --base-id <BASE_ID> --table-id <TABLE_ID> \
创建成功以 `data.newRecordIds[]` 为 ID 来源;不要把整个 `data` 当作单个 recordId,也不要只以退出码作为写入成功证据。
批量追加本地 JSON/CSV 到已有表时可使用 `python3 scripts/import_records.py <baseId> <tableId> <file> [batch_size]`。脚本会逐批检查业务状态、收集 `newRecordIds` 并按 ID 回读;任何批次失败或回读不完整都会输出 ledger 并以非零状态结束,已有成功批次仍会保留在结果中。
## cells 写入格式
各字段类型的写入格式见 [aitable-cell-value.md](./aitable-cell-value.md)。
@@ -60,7 +60,7 @@ dws aitable record query --base-id X --table-id Y --all --cursor "上次返回
{"operator":"and","operands":[{"operator":"eq","operands":["<fieldId>","<value>"]}]}
```
> **singleSelect/multipleSelect 过滤**:filters 中可传 option id 或 option name,但建议优先用 **option id**(通过 `field get` 获取),更可靠。
> **singleSelect/multipleSelect 过滤**:filters 中传选项名称字面量,不传 option id。选项名称先通过 `field get` 的 `config.options[]` 核对。
## 减少响应体积
@@ -17,23 +17,17 @@ Flags:
只需传入需修改的字段,未传入的保持原值。每条记录必须含 recordId 和 cells。
## cells key:优先使用 fieldId,也支持唯一字段名
## cells key:自动化与 Agent 路径使用 fieldId
`cells` 的 key 有两种写法:
公开稳定的 Agent/自动化写法是 `fieldId`:它不受字段重命名或重名影响,并可通过 `field get` 获取。
- fieldId(推荐):不受字段重命名或重名影响,通过 `field get` 获取。
- 当前表内唯一的字段名:按名称精确匹配;如果存在同名字段,必须改用 fieldId。
同一字段同时通过 fieldId 和字段名传入时,fieldId 对应的值优先。
Runtime 仍兼容当前表内唯一字段名,但这属于便捷兼容路径:重名或重命名会改变解析结果,不能用于脚本、跨步骤调用或从旧上下文重放。字段名与 fieldId 同时出现时也不要依赖覆盖顺序。
```bash
# 推荐:fieldId
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"fldStatusId":"已完成"}}]' --format json
# 便捷写法:当前表内唯一字段名
dws aitable record update --base-id <BASE_ID> --table-id <TABLE_ID> \
--records '[{"recordId":"recXXX","cells":{"状态":"已完成"}}]' --format json
```
## 推荐参数形式
@@ -54,15 +54,20 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
def normalize_download_url(url: str) -> str:
if url.startswith("http://") or url.startswith("https://"):
return url
return f"https://{url}"
normalized = url if "://" in url else f"https://{url}"
parsed = urlparse(normalized)
if parsed.scheme != "https" or not parsed.hostname:
raise ValueError("downloadUrl 必须是有效的 HTTPS URL")
return normalized
def download_file(url: str, output_path: Path) -> Tuple[bool, str]:
req = Request(url, method="GET")
try:
with urlopen(req, timeout=180) as resp:
redirected = urlparse(resp.geturl())
if redirected.scheme != "https" or not redirected.hostname:
return False, "download redirect is not HTTPS"
if resp.status != 200:
return False, f"download http status: {resp.status}"
output_path.write_bytes(resp.read())
@@ -79,6 +84,20 @@ def fail(msg: str, code: int = 1) -> None:
sys.exit(code)
def resolve_output_path(value: Optional[str], file_name: str, overwrite: bool) -> Path:
root = Path.cwd().resolve()
candidate = Path(value) if value else Path(file_name).name
output_path = candidate.resolve() if candidate.is_absolute() else (root / candidate).resolve()
try:
output_path.relative_to(root)
except ValueError as exc:
raise ValueError("输出路径必须位于当前工作目录内") from exc
if output_path.exists() and not overwrite:
raise ValueError(f"输出文件已存在:{output_path};如需覆盖请显式传 --overwrite")
output_path.parent.mkdir(parents=True, exist_ok=True)
return output_path
def build_start_args(args: argparse.Namespace) -> list[str]:
cmd = [
"aitable",
@@ -113,6 +132,7 @@ def main() -> None:
parser.add_argument("--output", help="本地保存路径(不传则按 fileName 保存到当前目录)")
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径,默认 dws")
parser.add_argument("--no-download", action="store_true", help="仅返回 downloadUrl,不下载文件")
parser.add_argument("--overwrite", action="store_true", help="允许覆盖当前工作目录内的已有输出文件")
args = parser.parse_args()
if not validate_resource_id(args.base_id):
@@ -121,6 +141,10 @@ def main() -> None:
fail("scope=table/view 时必须传 --table-id")
if args.scope == "view" and not args.view_id:
fail("scope=view 时必须传 --view-id")
if args.table_id and not validate_resource_id(args.table_id):
fail("无效的 tableId 格式")
if args.view_id and not validate_resource_id(args.view_id):
fail("无效的 viewId 格式")
print("[1/2] start export task", file=sys.stderr)
rc, out, err = run_dws(args.dws, build_start_args(args), timeout_sec=120)
@@ -132,7 +156,7 @@ def main() -> None:
data = obj.get("data", {}) or {}
status = obj.get("status")
if status == "error":
if status != "success":
fail(f"export_data 返回失败: {json.dumps(obj, ensure_ascii=False)}")
download_url = data.get("downloadUrl")
@@ -163,7 +187,7 @@ def main() -> None:
obj2 = parse_json_output(out2)
if not obj2:
fail(f"export_data 轮询返回非 JSON: {out2[:300]}")
if obj2.get("status") == "error":
if obj2.get("status") != "success":
fail(f"export_data 轮询返回失败: {json.dumps(obj2, ensure_ascii=False)}")
d2 = obj2.get("data", {}) or {}
download_url = d2.get("downloadUrl") or download_url
@@ -194,8 +218,11 @@ def main() -> None:
print(json.dumps(result, ensure_ascii=False, indent=2))
return
norm_url = normalize_download_url(download_url)
output_path = Path(args.output).expanduser().resolve() if args.output else Path.cwd() / file_name
try:
norm_url = normalize_download_url(download_url)
output_path = resolve_output_path(args.output, file_name, args.overwrite)
except ValueError as exc:
fail(str(exc))
ok, dl_err = download_file(norm_url, output_path)
if not ok:
fail(f"downloadUrl 下载失败: {dl_err}")
@@ -22,6 +22,7 @@ import sys
from pathlib import Path
from typing import Any, Dict, Optional, Tuple
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
from urllib.request import Request, urlopen
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
@@ -52,6 +53,9 @@ def parse_json_output(raw: str) -> Optional[Dict[str, Any]]:
def put_file(upload_url: str, file_path: Path) -> Tuple[bool, str]:
parsed = urlparse(upload_url)
if parsed.scheme != "https" or not parsed.hostname:
return False, "uploadUrl must be a valid HTTPS URL"
payload = file_path.read_bytes()
req = Request(upload_url, data=payload, method="PUT")
# 关键:清空 Content-Type,避免 SignatureDoesNotMatch。
@@ -1,273 +1,276 @@
#!/usr/bin/env python3
"""
批量添加字段到钉钉 AI 表格数据表(新版 schema)
"""批量添加字段到钉钉 AI 表格数据表。
用法:
python bulk_add_fields.py <baseId> <tableId> fields.json
python3 bulk_add_fields.py <baseId> <tableId> fields.json
fields.json 格式:
[
{"fieldName": "字段 1", "type": "text"},
{"fieldName": "字段 2", "type": "number", "config": {"formatter": "INT"}},
{"fieldName": "字段 3", "type": "singleSelect", "config": {"options": [{"name": "高"}]}}
]
兼容写法:
- name 会自动映射为 fieldName
- phone 会自动映射为 telephone
脚本检查业务状态和逐项结果,并回读成功字段。部分成功会输出 ledger,但整体以非零
状态结束。fields.json 单次最多 15 个字段;name 会映射为 fieldName,phone 会映射为
telephone。
"""
import sys
from __future__ import annotations
import argparse
import json
import subprocess
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Union, List, Dict, Any, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union
JsonData = Union[List[Any], Dict[str, Any]]
MAX_FILE_SIZE = 10 * 1024 * 1024
ALLOWED_FILE_EXTENSIONS = ['.json']
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
ALLOWED_FIELD_TYPES = {
'text', 'number', 'singleSelect', 'multipleSelect', 'date', 'currency',
'user', 'department', 'group', 'progress', 'rating', 'checkbox',
'attachment', 'url', 'richText', 'telephone', 'email', 'idCard',
'barcode', 'geolocation', 'address', 'primaryDoc', 'formula',
'unidirectionalLink', 'bidirectionalLink', 'lookup', 'filterUp',
'creator', 'lastModifier', 'createdTime', 'lastModifiedTime',
}
FIELD_TYPE_ALIASES = {
'phone': 'telephone',
"text", "number", "singleSelect", "multipleSelect", "date", "currency",
"user", "department", "group", "progress", "rating", "checkbox",
"attachment", "url", "richText", "telephone", "email", "idCard",
"barcode", "geolocation", "address", "primaryDoc", "formula",
"unidirectionalLink", "bidirectionalLink", "lookup", "filterUp",
"creator", "lastModifier", "createdTime", "lastModifiedTime",
}
def resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path:
if allowed_root is None:
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
allowed_root = Path(allowed_root).resolve()
target_path = (
Path(path).resolve()
if Path(path).is_absolute()
else (Path.cwd() / path).resolve()
)
root = Path(allowed_root or os.environ.get("OPENCLAW_WORKSPACE", os.getcwd())).resolve()
candidate = Path(path)
target = candidate.resolve() if candidate.is_absolute() else (Path.cwd() / candidate).resolve()
try:
target_path.relative_to(allowed_root)
return target_path
except ValueError:
raise ValueError(
f"路径超出允许范围:{path}\n"
f"目标路径:{target_path}\n"
f"允许根目录:{allowed_root}\n"
f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内"
)
target.relative_to(root)
except ValueError as exc:
raise ValueError(f"路径超出允许范围:{path}(允许根目录:{root})") from exc
return target
def validate_resource_id(resource_id: str) -> bool:
return bool(resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip()))
return bool(resource_id and RESOURCE_ID_PATTERN.fullmatch(resource_id.strip()))
def validate_file_extension(filename: str, allowed_extensions: list) -> bool:
return any(filename.lower().endswith(ext) for ext in allowed_extensions)
def safe_json_load(file_path: Path, max_size: int = MAX_FILE_SIZE) -> JsonData:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def safe_json_load(file_path: Path) -> JsonData:
if file_path.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"文件过大(限制 {MAX_FILE_SIZE:,} 字节)")
with file_path.open("r", encoding="utf-8") as stream:
return json.load(stream)
def normalize_field_config(field: Dict[str, Any]) -> Dict[str, Any]:
normalized = dict(field)
if 'fieldName' not in normalized and 'name' in normalized:
normalized['fieldName'] = normalized.pop('name')
normalized['type'] = FIELD_TYPE_ALIASES.get(
normalized.get('type', 'text'), normalized.get('type', 'text')
)
if "fieldName" not in normalized and "name" in normalized:
normalized["fieldName"] = normalized.pop("name")
if normalized.get("type") == "phone":
normalized["type"] = "telephone"
return normalized
def validate_field_config(field: Dict[str, Any]) -> Tuple[bool, str]:
def validate_field_config(field: Any) -> Tuple[bool, str]:
if not isinstance(field, dict):
return False, '字段配置必须是对象'
field = normalize_field_config(field)
if 'fieldName' not in field:
return False, '缺少必需字段:fieldName'
if not isinstance(field['fieldName'], str) or not field['fieldName'].strip():
return False, 'fieldName 必须是非空字符串'
field_type = field.get('type', 'text')
return False, "字段配置必须是对象"
normalized = normalize_field_config(field)
name = normalized.get("fieldName")
if not isinstance(name, str) or not name.strip():
return False, "fieldName 必须是非空字符串"
field_type = normalized.get("type", "text")
if field_type not in ALLOWED_FIELD_TYPES:
return False, f"不支持的字段类型:{field_type}"
config = field.get('config')
config = normalized.get("config")
if config is not None and not isinstance(config, dict):
return False, 'config 必须是对象'
if field_type in {'singleSelect', 'multipleSelect'}:
options = (config or {}).get('options')
if not options or not isinstance(options, list):
return False, (
'singleSelect / multipleSelect 必须提供 config.options 数组'
)
if field_type in {'unidirectionalLink', 'bidirectionalLink'}:
linked_table_id = (config or {}).get('linkedTableId')
if not linked_table_id or not validate_resource_id(linked_table_id):
return False, (
'关联字段必须提供合法的 config.linkedTableId(目标 Table ID)'
)
if field_type == 'lookup':
cfg = config or {}
if not cfg.get('associateField'):
return False, 'lookup 必须提供 config.associateField(本表关联字段的 fieldId)'
if not cfg.get('valuesField'):
return False, 'lookup 必须提供 config.valuesField(关联目标表中要取值的字段 fieldId)'
if not cfg.get('aggregator'):
return False, 'lookup 必须提供 config.aggregator(SUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE)'
if field_type == 'filterUp':
cfg = config or {}
if not cfg.get('targetSheet'):
return False, 'filterUp 必须提供 config.targetSheet(目标 Table ID)'
filters = cfg.get('filters')
if not filters or not isinstance(filters, list):
return False, 'filterUp 必须提供 config.filters(至少一条筛选规则)'
if not cfg.get('valuesField'):
return False, 'filterUp 必须提供 config.valuesField(目标表中要取值的字段 fieldId)'
if not cfg.get('aggregator'):
return False, 'filterUp 必须提供 config.aggregator(SUM/AVERAGE/COUNT/MAX/MIN/CONCATENATE)'
return True, ''
return False, "config 必须是对象"
ai_config = normalized.get("aiConfig")
if ai_config is not None and not isinstance(ai_config, dict):
return False, "aiConfig 必须是对象"
if field_type in {"singleSelect", "multipleSelect"}:
options = (config or {}).get("options")
if not isinstance(options, list) or not options:
return False, "singleSelect / multipleSelect 必须提供 config.options 数组"
if field_type in {"unidirectionalLink", "bidirectionalLink"}:
linked_table_id = (config or {}).get("linkedTableId")
if not validate_resource_id(str(linked_table_id or "")):
return False, "关联字段必须提供合法的 config.linkedTableId"
if field_type == "lookup":
required = ("associateField", "valuesField", "aggregator")
missing = [name for name in required if not (config or {}).get(name)]
if missing:
return False, f"lookup 缺少 config.{missing[0]}"
if field_type == "filterUp":
required = ("targetSheet", "filters", "valuesField", "aggregator")
missing = [name for name in required if not (config or {}).get(name)]
if missing:
return False, f"filterUp 缺少 config.{missing[0]}"
if not isinstance((config or {}).get("filters"), list):
return False, "filterUp config.filters 必须是数组"
return True, ""
def build_fields_json(fields: List[Dict[str, Any]]) -> str:
"""构建 --fields 参数的 JSON 字符串。"""
payload_fields = []
def build_fields_payload(fields: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
payload: List[Dict[str, Any]] = []
for field in fields:
normalized = normalize_field_config(field)
item: Dict[str, Any] = {
'fieldName': normalized['fieldName'].strip(),
'type': normalized.get('type', 'text'),
"fieldName": normalized["fieldName"].strip(),
"type": normalized.get("type", "text"),
}
if 'config' in normalized and normalized['config'] is not None:
item['config'] = normalized['config']
payload_fields.append(item)
return json.dumps(payload_fields, ensure_ascii=False)
for key in ("config", "aiConfig"):
if normalized.get(key) is not None:
item[key] = normalized[key]
payload.append(item)
return payload
def run_dws(args: List[str]) -> Optional[Dict[str, Any]]:
if not args:
print('错误:空命令')
return None
cmd = ['dws'] + args
def run_dws(dws_bin: str, args: List[str], timeout_sec: int = 120) -> Tuple[Optional[Dict[str, Any]], str]:
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=60
[dws_bin] + args,
capture_output=True,
text=True,
timeout=timeout_sec,
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
print(f"无法解析响应:{result.stdout[:200]}...")
print(f"JSON 解析错误:{e}")
return None
except subprocess.TimeoutExpired:
print('错误:命令执行超时(60 秒)')
return None
return None, f"dws 命令超时({timeout_sec} 秒)"
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装')
return None
return None, f"未找到 dws 命令:{dws_bin}"
if result.returncode != 0:
return None, (result.stderr or result.stdout).strip() or f"dws 退出码 {result.returncode}"
try:
payload = json.loads(result.stdout)
except json.JSONDecodeError as exc:
return None, f"dws 返回非 JSON:{exc}"
if not isinstance(payload, dict):
return None, "dws 返回的 JSON 不是对象"
if payload.get("status") != "success":
detail = payload.get("summary") or payload.get("error") or payload
return None, f"业务失败:{detail}"
return payload, ""
def normalize_result_item(index: int, item: Any) -> Dict[str, Any]:
if not isinstance(item, dict):
return {"index": index, "status": "failed", "error": "逐项结果不是对象"}
field_id = item.get("fieldId") or (item.get("data") or {}).get("fieldId")
succeeded = item.get("success") is True or item.get("status") == "success"
if "success" not in item and "status" not in item:
succeeded = bool(field_id)
result: Dict[str, Any] = {
"index": index,
"status": "success" if succeeded and field_id else "failed",
}
if field_id:
result["fieldId"] = str(field_id)
if result["status"] != "success":
result["error"] = item.get("reason") or item.get("error") or "字段创建未返回 fieldId"
return result
def bulk_add_fields(
base_id: str, table_id: str, fields_file: str
) -> bool:
base_id: str,
table_id: str,
fields: List[Dict[str, Any]],
dws_bin: str = "dws",
) -> Dict[str, Any]:
created, error = run_dws(
dws_bin,
[
"aitable", "field", "create",
"--base-id", base_id,
"--table-id", table_id,
"--fields", json.dumps(build_fields_payload(fields), ensure_ascii=False),
"--format", "json",
],
)
if created is None:
return {
"status": "failed",
"complete": False,
"requestedCount": len(fields),
"verifiedCount": 0,
"ledger": [{"status": "failed", "error": error}],
}
data = created.get("data") if isinstance(created.get("data"), dict) else {}
raw_results = data.get("results")
if not isinstance(raw_results, list) or len(raw_results) != len(fields):
return {
"status": "failed",
"complete": False,
"requestedCount": len(fields),
"verifiedCount": 0,
"ledger": [{"status": "failed", "error": "响应缺少与输入数量一致的 data.results[]"}],
}
ledger = [normalize_result_item(index, item) for index, item in enumerate(raw_results)]
field_ids = [item["fieldId"] for item in ledger if item["status"] == "success"]
verified_ids: List[str] = []
if field_ids:
queried, query_error = run_dws(
dws_bin,
[
"aitable", "field", "get",
"--base-id", base_id,
"--table-id", table_id,
"--field-ids", ",".join(field_ids),
"--format", "json",
],
)
if queried is None:
for item in ledger:
if item["status"] == "success":
item["status"] = "verify_failed"
item["error"] = query_error
else:
query_data = queried.get("data") if isinstance(queried.get("data"), dict) else {}
raw_fields = query_data.get("fields") or query_data.get("items") or []
found = {
str(item.get("fieldId"))
for item in raw_fields
if isinstance(item, dict) and item.get("fieldId")
}
verified_ids = [field_id for field_id in field_ids if field_id in found]
for item in ledger:
if item.get("fieldId") in field_ids and item.get("fieldId") not in found:
item["status"] = "verify_failed"
item["error"] = "回读未返回该字段"
complete = len(verified_ids) == len(fields) and all(item["status"] == "success" for item in ledger)
return {
"status": "success" if complete else "partial",
"complete": complete,
"requestedCount": len(fields),
"verifiedCount": len(verified_ids),
"fieldIds": verified_ids,
"ledger": ledger,
}
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("base_id")
parser.add_argument("table_id")
parser.add_argument("fields_file")
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径")
args = parser.parse_args()
if not validate_resource_id(args.base_id):
parser.error("无效的 baseId 格式")
if not validate_resource_id(args.table_id):
parser.error("无效的 tableId 格式")
try:
safe_path = resolve_safe_path(fields_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(fields_file, ALLOWED_FILE_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_FILE_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
fields = safe_json_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except json.JSONDecodeError as e:
print(f"错误:JSON 格式无效:{e}")
return False
if not isinstance(fields, list) or not fields:
print('错误:fields.json 必须是非空 JSON 数组')
return False
if len(fields) > 15:
print('错误:单次最多创建 15 个字段,请拆分后重试')
return False
for i, field in enumerate(fields):
valid, error = validate_field_config(field)
if not valid:
print(f"错误:字段 #{i+1} 配置无效:{error}")
return False
fields_json = build_fields_json(fields)
result = run_dws([
'aitable', 'field', 'create',
'--base-id', base_id,
'--table-id', table_id,
'--fields', fields_json,
'--format', 'json',
])
if not result:
return False
path = resolve_safe_path(args.fields_file)
if path.suffix.lower() != ".json" or not path.exists() or not path.is_file():
raise ValueError("fields_file 必须是工作区内存在的 .json 文件")
fields = safe_json_load(path)
if not isinstance(fields, list) or not fields:
raise ValueError("fields.json 必须是非空 JSON 数组")
if len(fields) > 15:
raise ValueError("单次最多创建 15 个字段,请拆分后重试")
for index, field in enumerate(fields, start=1):
valid, error = validate_field_config(field)
if not valid:
raise ValueError(f"字段 #{index} 配置无效:{error}")
result = bulk_add_fields(args.base_id, args.table_id, fields, args.dws)
except (ValueError, OSError, json.JSONDecodeError) as exc:
print(f"错误:{exc}", file=sys.stderr)
sys.exit(1)
print(json.dumps(result, ensure_ascii=False, indent=2))
return True
sys.exit(0 if result["complete"] else 2)
def main():
if len(sys.argv) != 4:
print(__doc__)
print('用法示例:')
print(' python bulk_add_fields.py basexxx tablexxx fields.json')
sys.exit(1)
base_id = sys.argv[1]
table_id = sys.argv[2]
fields_file = sys.argv[3]
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式')
sys.exit(1)
if not validate_resource_id(table_id):
print('错误:无效的 tableId 格式')
sys.exit(1)
success = bulk_add_fields(base_id, table_id, fields_file)
sys.exit(0 if success else 1)
if __name__ == '__main__':
if __name__ == "__main__":
main()
@@ -1,333 +1,264 @@
#!/usr/bin/env python3
"""
从 CSV / JSON 批量导入记录到钉钉 AI 表格(新版 schema)
"""批量追加 CSV / JSON 记录到已有钉钉 AI 表格数据表。
用法:
python import_records.py <baseId> <tableId> data.csv [batch_size]
python import_records.py <baseId> <tableId> data.json [batch_size]
python3 import_records.py <baseId> <tableId> data.csv [batch_size]
python3 import_records.py <baseId> <tableId> data.json [batch_size]
说明:
- CSV 表头默认视为 fieldId
- JSON 支持两种格式:
1. [{"cells": {"fldxxx": "value"}}, ...]
2. [{"fldxxx": "value"}, ...] # 会自动包装成 cells
⚠️ CSV 自动类型转换风险:
CSV 读入的所有 cell 都是 string,本脚本会尝试自动识别 'true'/'false'/数字
并转成对应类型(避免 text 字段塞入纯文本数字)。但当 fieldId 对应的字段是
text / telephone / idCard / barcode 这类"字符串形数字"字段时,自动转 int / float
会让 server 拒绝(字段类型不匹配)。这种情况建议改用 JSON 格式(自己显式控制类型),
或在 CSV 写入前给字段值前缀加引号 / 改为非纯数字。
CSV 表头必须是 fieldId。CSV 值保持字符串;需要布尔、数组、对象等精确类型时使用
JSON。JSON 支持 [{"cells": {...}}] 或 [{"fld...": value}] 两种格式。
脚本逐批检查业务状态、提取 newRecordIds 并回读验证;部分成功会保留 ledger,
但整体以非零状态结束。
"""
import sys
from __future__ import annotations
import argparse
import csv
import json
import subprocess
import os
import re
import subprocess
import sys
from pathlib import Path
from typing import Union, List, Dict, Any, Optional, Tuple
from typing import Any, Dict, List, Optional, Tuple, Union
JsonData = Union[List[Any], Dict[str, Any]]
RecordDict = Dict[str, str]
MAX_FILE_SIZE = 50 * 1024 * 1024
ALLOWED_CSV_EXTENSIONS = ['.csv']
ALLOWED_JSON_EXTENSIONS = ['.json']
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
RESOURCE_ID_PATTERN = re.compile(r"^[A-Za-z0-9_-]{8,128}$")
MAX_RECORDS_PER_BATCH = 100
DEFAULT_BATCH_SIZE = 50
def resolve_safe_path(
path: str, allowed_root: Optional[str] = None
) -> Path:
if allowed_root is None:
allowed_root = os.environ.get('OPENCLAW_WORKSPACE', os.getcwd())
allowed_root = Path(allowed_root).resolve()
target_path = (
Path(path).resolve()
if Path(path).is_absolute()
else (Path.cwd() / path).resolve()
)
def resolve_safe_path(path: str, allowed_root: Optional[str] = None) -> Path:
root = Path(allowed_root or os.environ.get("OPENCLAW_WORKSPACE", os.getcwd())).resolve()
candidate = Path(path)
target = candidate.resolve() if candidate.is_absolute() else (Path.cwd() / candidate).resolve()
try:
target_path.relative_to(allowed_root)
return target_path
except ValueError:
raise ValueError(
f"路径超出允许范围:{path}\n"
f"目标路径:{target_path}\n"
f"允许根目录:{allowed_root}\n"
f"提示:设置 OPENCLAW_WORKSPACE 环境变量或确保文件在工作目录内"
)
target.relative_to(root)
except ValueError as exc:
raise ValueError(f"路径超出允许范围:{path}(允许根目录:{root})") from exc
return target
def validate_resource_id(resource_id: str) -> bool:
return bool(
resource_id and RESOURCE_ID_PATTERN.match(resource_id.strip())
)
return bool(resource_id and RESOURCE_ID_PATTERN.fullmatch(resource_id.strip()))
def validate_file_extension(
filename: str, allowed_extensions: list
) -> bool:
return any(filename.lower().endswith(ext) for ext in allowed_extensions)
def safe_csv_load(file_path: Path) -> List[Dict[str, str]]:
if file_path.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"文件过大(限制 {MAX_FILE_SIZE:,} 字节)")
with file_path.open("r", encoding="utf-8-sig", newline="") as stream:
reader = csv.DictReader(stream)
if not reader.fieldnames or any(not str(name).strip() for name in reader.fieldnames):
raise ValueError("CSV 必须包含非空 fieldId 表头")
return list(reader)
def safe_csv_load(
file_path: Path, max_size: int = MAX_FILE_SIZE
) -> List[RecordDict]:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8', newline='') as f:
return list(csv.DictReader(f))
def safe_json_load(
file_path: Path, max_size: int = MAX_FILE_SIZE
) -> JsonData:
file_size = file_path.stat().st_size
if file_size > max_size:
raise ValueError(
f"文件过大:{file_size:,} 字节 (限制:{max_size:,} 字节)"
)
with open(file_path, 'r', encoding='utf-8') as f:
return json.load(f)
def sanitize_record_value(
value: Any,
) -> Optional[Union[str, int, float, bool, list, dict]]:
if value is None:
return None
if isinstance(value, (bool, int, float, list, dict)):
return value
if not isinstance(value, str):
return value
if not value.strip():
return None
value = value.strip()
if value.lower() == 'true':
return True
if value.lower() == 'false':
return False
try:
if '.' in value:
return float(value)
return int(value)
except ValueError:
return value
def safe_json_load(file_path: Path) -> JsonData:
if file_path.stat().st_size > MAX_FILE_SIZE:
raise ValueError(f"文件过大(限制 {MAX_FILE_SIZE:,} 字节)")
with file_path.open("r", encoding="utf-8") as stream:
return json.load(stream)
def normalize_record(record: Dict[str, Any]) -> Dict[str, Any]:
if 'cells' in record and isinstance(record['cells'], dict):
cells = record['cells']
else:
cells = record
normalized = {}
cells = record.get("cells") if isinstance(record.get("cells"), dict) else record
normalized: Dict[str, Any] = {}
for key, value in cells.items():
sanitized = sanitize_record_value(value)
if sanitized is not None:
normalized[key] = sanitized
return {'cells': normalized}
if not isinstance(key, str) or not key.strip():
continue
if value is None:
continue
if isinstance(value, str):
value = value.strip()
if not value:
continue
normalized[key.strip()] = value
return {"cells": normalized}
def validate_record(record: Dict[str, Any]) -> Tuple[bool, str]:
def validate_record(record: Any) -> Tuple[bool, str]:
if not isinstance(record, dict):
return False, '记录必须是对象'
normalized = normalize_record(record)
cells = normalized.get('cells', {})
if not cells or not isinstance(cells, dict):
return False, '记录必须包含非空 cells 对象'
return True, ''
return False, "记录必须是对象"
cells = normalize_record(record).get("cells")
if not isinstance(cells, dict) or not cells:
return False, "记录必须包含非空 cells 对象"
return True, ""
def run_dws(args: List[str]) -> Optional[Dict[str, Any]]:
if not args:
print('错误:空命令')
return None
cmd = ['dws'] + args
def run_dws(dws_bin: str, args: List[str], timeout_sec: int = 120) -> Tuple[Optional[Dict[str, Any]], str]:
try:
result = subprocess.run(
cmd, capture_output=True, text=True, timeout=120
[dws_bin] + args,
capture_output=True,
text=True,
timeout=timeout_sec,
)
if result.returncode != 0:
print(f"错误:{result.stderr.strip()}")
return None
try:
return json.loads(result.stdout)
except json.JSONDecodeError as e:
print(f"无法解析响应:{result.stdout[:200]}...")
print(f"JSON 解析错误:{e}")
return None
except subprocess.TimeoutExpired:
print('错误:命令执行超时(120 秒)')
return None
return None, f"dws 命令超时({timeout_sec} 秒)"
except FileNotFoundError:
print('错误:未找到 dws 命令,请确认已安装')
return None
def import_from_csv(
base_id: str, table_id: str, csv_file: str,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> bool:
return None, f"未找到 dws 命令:{dws_bin}"
if result.returncode != 0:
return None, (result.stderr or result.stdout).strip() or f"dws 退出码 {result.returncode}"
try:
safe_path = resolve_safe_path(csv_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
payload = json.loads(result.stdout)
except json.JSONDecodeError as exc:
return None, f"dws 返回非 JSON:{exc}"
if not isinstance(payload, dict):
return None, "dws 返回的 JSON 不是对象"
if payload.get("status") != "success":
detail = payload.get("summary") or payload.get("error") or payload
return None, f"业务失败:{detail}"
return payload, ""
if not validate_file_extension(csv_file, ALLOWED_CSV_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_CSV_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
rows = safe_csv_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except csv.Error as e:
print(f"错误:CSV 格式无效:{e}")
return False
if not rows:
print('错误:CSV 文件为空或没有有效数据行')
return False
records = [
normalize_record(row)
for row in rows
if normalize_record(row)['cells']
def extract_query_record_ids(payload: Dict[str, Any]) -> List[str]:
data = payload.get("data") if isinstance(payload.get("data"), dict) else {}
records = data.get("records") or data.get("items") or []
if not isinstance(records, list):
return []
return [
str(item.get("recordId"))
for item in records
if isinstance(item, dict) and item.get("recordId")
]
return import_records(base_id, table_id, records, batch_size)
def import_from_json(
base_id: str, table_id: str, json_file: str,
batch_size: int = DEFAULT_BATCH_SIZE,
) -> bool:
try:
safe_path = resolve_safe_path(json_file)
except ValueError as e:
print(f"路径验证失败:{e}")
return False
if not validate_file_extension(json_file, ALLOWED_JSON_EXTENSIONS):
print(f"错误:只允许 {', '.join(ALLOWED_JSON_EXTENSIONS)} 文件")
return False
if not safe_path.exists():
print(f"错误:文件不存在:{safe_path}")
return False
try:
records = safe_json_load(safe_path)
except ValueError as e:
print(f"错误:{e}")
return False
except json.JSONDecodeError as e:
print(f"错误:JSON 格式无效:{e}")
return False
if not isinstance(records, list) or not records:
print('错误:JSON 文件必须是非空数组')
return False
for i, record in enumerate(records):
valid, error = validate_record(record)
if not valid:
print(f"错误:记录 #{i+1} 格式无效:{error}")
return False
return import_records(
base_id, table_id,
[normalize_record(r) for r in records], batch_size,
)
def import_records(
base_id: str, table_id: str,
records: List[Dict[str, Any]], batch_size: int,
) -> bool:
if batch_size <= 0:
print('错误:batch_size 必须大于 0')
return False
if batch_size > MAX_RECORDS_PER_BATCH:
batch_size = MAX_RECORDS_PER_BATCH
base_id: str,
table_id: str,
records: List[Dict[str, Any]],
batch_size: int,
dws_bin: str = "dws",
) -> Dict[str, Any]:
if batch_size <= 0 or batch_size > MAX_RECORDS_PER_BATCH:
raise ValueError(f"batch_size 必须在 1..{MAX_RECORDS_PER_BATCH} 之间")
ledger: List[Dict[str, Any]] = []
verified_ids: List[str] = []
total_batches = (len(records) + batch_size - 1) // batch_size
success = True
for i in range(0, len(records), batch_size):
batch = records[i:i + batch_size]
batch_num = (i // batch_size) + 1
records_json = json.dumps(batch, ensure_ascii=False)
result = run_dws([
'aitable', 'record', 'create',
'--base-id', base_id,
'--table-id', table_id,
'--records', records_json,
'--format', 'json',
])
if result:
print(
f"[{batch_num}/{total_batches}] "
f"✓ 已提交 {len(batch)} 条记录"
)
else:
print(f"[{batch_num}/{total_batches}] ✗ 导入失败")
success = False
return success
def main():
if len(sys.argv) < 4 or len(sys.argv) > 5:
print(__doc__)
print('用法示例:')
print(
' python import_records.py basexxx tablexxx data.csv 50'
for start in range(0, len(records), batch_size):
batch = records[start : start + batch_size]
batch_number = start // batch_size + 1
print(f"[{batch_number}/{total_batches}] 创建 {len(batch)} 条记录", file=sys.stderr)
created, error = run_dws(
dws_bin,
[
"aitable", "record", "create",
"--base-id", base_id,
"--table-id", table_id,
"--records", json.dumps(batch, ensure_ascii=False),
"--format", "json",
],
)
sys.exit(1)
entry: Dict[str, Any] = {
"batch": batch_number,
"inputCount": len(batch),
"status": "failed",
"recordIds": [],
}
if created is None:
entry["error"] = error
ledger.append(entry)
continue
base_id = sys.argv[1]
table_id = sys.argv[2]
input_file = sys.argv[3]
batch_size = (
int(sys.argv[4]) if len(sys.argv) == 5
else DEFAULT_BATCH_SIZE
)
data = created.get("data") if isinstance(created.get("data"), dict) else {}
record_ids = data.get("newRecordIds")
if not isinstance(record_ids, list) or len(record_ids) != len(batch) or not all(record_ids):
entry["error"] = "创建响应缺少与输入数量一致的 data.newRecordIds[]"
ledger.append(entry)
continue
record_ids = [str(value) for value in record_ids]
entry["recordIds"] = record_ids
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式')
sys.exit(1)
if not validate_resource_id(table_id):
print('错误:无效的 tableId 格式')
sys.exit(1)
if input_file.lower().endswith('.csv'):
success = import_from_csv(
base_id, table_id, input_file, batch_size
)
elif input_file.lower().endswith('.json'):
success = import_from_json(
base_id, table_id, input_file, batch_size
queried, query_error = run_dws(
dws_bin,
[
"aitable", "record", "query",
"--base-id", base_id,
"--table-id", table_id,
"--record-ids", ",".join(record_ids),
"--format", "json",
],
)
if queried is None:
entry["status"] = "verify_failed"
entry["error"] = query_error
ledger.append(entry)
continue
found = set(extract_query_record_ids(queried))
missing = [record_id for record_id in record_ids if record_id not in found]
if missing:
entry["status"] = "verify_failed"
entry["missingRecordIds"] = missing
entry["error"] = "回读未返回全部新记录"
ledger.append(entry)
continue
entry["status"] = "success"
verified_ids.extend(record_ids)
ledger.append(entry)
complete = all(item["status"] == "success" for item in ledger)
return {
"status": "success" if complete else "partial",
"complete": complete,
"requestedCount": len(records),
"verifiedCount": len(verified_ids),
"recordIds": verified_ids,
"ledger": ledger,
}
def load_records(input_file: str) -> List[Dict[str, Any]]:
path = resolve_safe_path(input_file)
if not path.exists() or not path.is_file():
raise ValueError(f"文件不存在或不可读:{path}")
suffix = path.suffix.lower()
if suffix == ".csv":
raw: Any = safe_csv_load(path)
elif suffix == ".json":
raw = safe_json_load(path)
else:
print('错误:仅支持 .csv 或 .json 文件')
raise ValueError("仅支持 .csv 或 .json 文件")
if not isinstance(raw, list) or not raw:
raise ValueError("输入文件必须包含至少一条记录")
normalized: List[Dict[str, Any]] = []
for index, record in enumerate(raw, start=1):
valid, error = validate_record(record)
if not valid:
raise ValueError(f"记录 #{index} 格式无效:{error}")
normalized.append(normalize_record(record))
return normalized
def main() -> None:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("base_id")
parser.add_argument("table_id")
parser.add_argument("input_file")
parser.add_argument("batch_size", nargs="?", type=int, default=DEFAULT_BATCH_SIZE)
parser.add_argument("--dws", default="dws", help="dws 可执行文件路径")
args = parser.parse_args()
if not validate_resource_id(args.base_id):
parser.error("无效的 baseId 格式")
if not validate_resource_id(args.table_id):
parser.error("无效的 tableId 格式")
try:
records = load_records(args.input_file)
result = import_records(
args.base_id,
args.table_id,
records,
args.batch_size,
args.dws,
)
except (ValueError, OSError, csv.Error, json.JSONDecodeError) as exc:
print(f"错误:{exc}", file=sys.stderr)
sys.exit(1)
sys.exit(0 if success else 1)
print(json.dumps(result, ensure_ascii=False, indent=2))
sys.exit(0 if result["complete"] else 2)
if __name__ == '__main__':
if __name__ == "__main__":
main()
@@ -18,6 +18,7 @@
--records '[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]' --format json
"""
import argparse
import sys
import json
import subprocess
@@ -28,6 +29,7 @@ from pathlib import Path
from typing import Optional, Dict, Any
from urllib.request import Request, urlopen
from urllib.error import HTTPError, URLError
from urllib.parse import urlparse
RESOURCE_ID_PATTERN = re.compile(r'^[A-Za-z0-9_-]{8,128}$')
MAX_FILE_SIZE = 100 * 1024 * 1024 # 100MB
@@ -43,16 +45,24 @@ def detect_mime_type(file_path: Path) -> str:
return mime_type or 'application/octet-stream'
def run_dws(args: list) -> Optional[Dict[str, Any]]:
def run_dws(args: list, dws_bin: str = 'dws') -> Optional[Dict[str, Any]]:
"""调用 dws 命令并返回解析后的 JSON 结果。"""
cmd = ['dws'] + args
cmd = [dws_bin] + args
try:
result = subprocess.run(cmd, capture_output=True, text=True, timeout=60)
if result.returncode != 0:
print(f"错误:dws 命令失败: {result.stderr.strip()}", file=sys.stderr)
return None
try:
return json.loads(result.stdout)
payload = json.loads(result.stdout)
if not isinstance(payload, dict):
print('错误:dws 响应 JSON 不是对象', file=sys.stderr)
return None
if payload.get('status') != 'success':
detail = payload.get('summary') or payload.get('error') or payload
print(f"错误:dws 业务失败: {detail}", file=sys.stderr)
return None
return payload
except json.JSONDecodeError:
print(f"错误:无法解析 dws 响应: {result.stdout[:300]}", file=sys.stderr)
return None
@@ -66,6 +76,10 @@ def run_dws(args: list) -> Optional[Dict[str, Any]]:
def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool:
"""通过 HTTP PUT 上传文件到 OSS。"""
parsed = urlparse(upload_url)
if parsed.scheme != 'https' or not parsed.hostname:
print('错误:uploadUrl 必须是有效的 HTTPS URL', file=sys.stderr)
return False
file_data = file_path.read_bytes()
req = Request(upload_url, data=file_data, method='PUT')
req.add_header('Content-Type', mime_type)
@@ -84,7 +98,9 @@ def upload_to_oss(upload_url: str, file_path: Path, mime_type: str) -> bool:
return False
def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, Any]]:
def upload_attachment(
base_id: str, file_path_str: str, dws_bin: str = 'dws'
) -> Optional[Dict[str, Any]]:
"""
执行完整的附件上传流程:
1. prepare_attachment_upload → uploadUrl + fileToken
@@ -121,7 +137,7 @@ def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, An
'--mime-type', mime_type,
'--format', 'json',
]
result = run_dws(dws_args)
result = run_dws(dws_args, dws_bin)
if not result:
return None
@@ -157,27 +173,20 @@ def upload_attachment(base_id: str, file_path_str: str) -> Optional[Dict[str, An
def main():
if len(sys.argv) != 3:
print(__doc__)
print('用法:')
print(' python upload_attachment.py <baseId> <filePath>')
print()
print('示例:')
print(' python upload_attachment.py G1DKw2zgV2bEk6PMSBooNxlEVB5r9YAn ./report.pdf')
print()
print('然后在 record create 中使用返回的 fileToken:')
print(' dws aitable record create --base-id <BASE_ID> --table-id <TABLE_ID> \\')
print(' --records \'[{"cells":{"fldAttachId":[{"fileToken":"ft_xxx"}]}}]\' --format json')
sys.exit(1)
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('base_id')
parser.add_argument('file_path')
parser.add_argument('--dws', default='dws', help='dws 可执行文件路径')
args = parser.parse_args()
base_id = sys.argv[1]
file_path = sys.argv[2]
base_id = args.base_id
file_path = args.file_path
if not validate_resource_id(base_id):
print('错误:无效的 baseId 格式', file=sys.stderr)
sys.exit(1)
parser.error('无效的 baseId 格式')
result = upload_attachment(base_id, file_path)
result = upload_attachment(base_id, file_path, args.dws)
if result is None:
sys.exit(1)
+126 -64
View File
@@ -1,6 +1,6 @@
---
name: dingtalk-chat
description: 钉钉群聊与消息。Use when 用户提到 发消息/单聊/群聊/建群/搜群或消息/聊天记录/群成员管理/回复转发撤回/@消息/表情回应/图片文件与资源下载/机器人群发/Webhook通知/互动卡片/收藏与Pin/消息或会话置顶/未读与红点/已读状态/会话分类。不做紧急 DING/短信/电话(走 dingtalk-ding)、邮件(走 dingtalk-mail)、班级群(走 dingtalk-misc);找人本身走 dingtalk-contact 或 dingtalk-aisearch。命令前缀:dws chat。
description: 钉钉群聊与消息。Use when 用户提到 发消息/单聊/群聊/建群/拉人进群/改群名/搜索群/群成员管理/@消息/撤回消息/机器人群发/Webhook通知/发图片或文件到群/标记未读/清除红点/置顶消息/全部群列表。不做紧急 DING/短信/电话(走 dingtalk-ding)、邮件(走 dingtalk-mail)、班级群(走 dingtalk-misc)。命令前缀:dws chat。
metadata:
cli_version: ">=0.2.14"
category: product
@@ -15,7 +15,7 @@ metadata:
> **CRITICAL — Before any `dws` operation, MUST fully read [`dws-shared`](../dws-shared/SKILL.md).** It defines the global execution contract, safety floor, and on-demand shared-reference routing. Do not preload all of its references.
> Command reference: [chat.md](references/chat.md); emoji list: [chat-emoji-list.md](references/chat-emoji-list.md).
> Command reference: [chat.md](references/chat.md); emoji list: [chat-emoji-list.md](references/chat-emoji-list.md); workflows: [01-messaging.md](references/01-messaging.md).
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcut 发现(按需)
@@ -25,51 +25,42 @@ metadata:
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service chat --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## 加载与路由顺序
## Shortcut 执行契约
This is the only Shortcut entry point for multi/chat; `references/` documents atomic commands only. Use `exact recipe/runnable script > matching public Shortcut > atomic command`; skip an optional script if its interpreter is unavailable.
This is the only Shortcut entry point for multi/chat; `references/` documents
atomic commands only. Routing priority:
exact recipe/runnable script > matching public Shortcut > atomic command.
Skip a script if its interpreter (for example, `python3`) is unavailable.
1. 已知高频意图:直接使用“核心意图与执行骨架”,命中后照抄参数名,不先调用 `--help`。
2. 已有匹配 Shortcut:直接执行;参数、约束或安全不确定时才读 leaf Schema。
3. 仅当前 Cobra flags 不确定时读 leaf `--help`。
4. 现有路由无法定位低频能力时才查 Runtime Shortcut Catalog;select a real `cli_path` and never guess names.
5. 没有 Shortcut 时,按需读取对应 branch reference,进入原子命令。
- Select a real `cli_path` from the high-frequency routes below; never guess names.
Use `dws shortcut list --service chat --compact --format json` only when no
reviewed route matches.
- Once selected, execute directly. Read leaf Schema only when parameters,
constraints, or safety are uncertain; read leaf `--help` only when flags are
uncertain. If absent from Schema, use the same path in the Runtime Shortcut
Catalog.
- For `confirmation=user_required`, confirm before adding `--yes`. On source
conflict, use the safer interpretation and report it.
For `confirmation=user_required`, confirm before adding `--yes`. On source conflict, use the safer interpretation and report it.
### 高频直接执行骨架
## 核心对象与 ID
命中后照抄参数名,不先调用 `--help`。
| 对象 | 核心标识与边界 |
| 用户意图 | 精确 Shortcut 骨架 |
|---|---|
| 人员 | 姓名必须解析成唯一真实的 `userId` / `openDingTalkId`,不能把名称当 ID |
| 会话 | 使用真实 `openConversationId` / cid;群名只用于 Shortcut 目标解析 |
| 消息 | 使用真实 `openMessageId` / msgId,并保持与身份和会话一致 |
| 发送任务 | `openTaskId` 只用于查询发送状态,不能替代消息 ID |
| Thread | thread/topic ID 必须绑定真实会话,不跨会话复用 |
| 身份 | current-user、app-bot、Webhook 是不同操作者,不能自动互换 |
| 状态 | 收藏、消息置顶、消息 Pin、会话置顶作用于不同对象 |
| 姓名发单聊 / 群名发群消息 | `+dm --to <姓名> --text <内容>` / `+send-to-group --group <群名> --text <内容>` |
| 三种身份发消息 | `+messages-send --as user|bot|webhook`;按下方身份模板补参数 |
| 改群名 | `+chat-update --group <openConversationId> --name <新群名>` |
| 列成员 / 批查成员 | `+chat-members-list --group <群名>` 或 `--conversation-id <cid>` / `+chat-members-get --id <cid> --users <odid,...>` |
| 消息详情 / 撤回 / 发送状态 | `+messages-mget --msg-ids <mid,...>` / `+messages-recall --conversation-id <cid> --msg-id <mid>` / `+messages-query-send-status --open-task-id <tid>` |
| 查 @ 我的消息 | 全局用 `+at-me --days <N>`;限定群用 `+search-msg --at-me --group <cid>`,不要给 `+at-me` 猜群参数 |
| 群邀请链接 / 群机器人 | 先 `+chat-search --query <群名>` 取 cid,再 `+chat-invite-url --group <cid>` / `+chat-bots --group <cid>` |
| 会话置顶 / 收藏列表 | `+conversation-set-top --conversation-id <cid> [--off]` / `+flag-list --size <1-100>` |
## 核心意图与执行骨架
### 统一发送
Prefer the exact Shortcut below; otherwise use the atomic fallback. Apply shared `--format json` and take downstream IDs from actual output. Every Chat workflow must work without Python.
| 用户意图 | 精确 Shortcut 骨架 / 原子回退 | 必须保留的执行边界 |
|---|---|---|
| 姓名发单聊 / 群名发群消息 | `+dm --to <姓名> --text <内容>` / `+send-to-group --group <群名> --text <内容>` | Resolve one real person or cid; mentions/`@all` use `+messages-send` |
| user / bot / webhook 发消息 | `+messages-send --as user|bot|webhook` / 对应 `message send*` | Identity determines the operator; use the matching target and content flags |
| 建群 / 改群名 / 拉人 | `+chat-create --name <群名> --users <uid,...>` / `+chat-update --group <cid> --name <新群名>` / `group members add` | Resolve every member; extract the new cid before follow-up actions |
| 列成员 / 批查成员 | `+chat-members-list --group <群名>` 或 `--conversation-id <cid>` / `+chat-members-get --id <cid> --users <odid,...>` | Stop on ambiguous group names; keep user and bot bucket failures |
| 拉会话消息 / 查某人记录 | `+chat-messages` / `message list` | Select one group or DM target; use the requested or explicitly narrowed time boundary |
| 搜消息 / 查 @ 我 | `+search-msg --query <关键词>`;全局 `+at-me --days <N>`;群内 `+search-msg --at-me --group <cid>` | Add only real filters; never invent group flags for `+at-me` |
| 消息详情 / 撤回 / 发送状态 | `+messages-mget --msg-ids <mid,...>` / `+messages-recall --conversation-id <cid> --msg-id <mid>` / `+messages-query-send-status --open-task-id <tid>` | Recall only when explicit; use IDs from the same identity and conversation |
| 群邀请链接 / 群机器人 | `+chat-search --query <群名>` → `+chat-invite-url --group <cid>` / `+chat-bots --group <cid>` | Reuse the resolved cid; do not search again by a guessed name |
| 会话置顶 / 收藏列表 | `+conversation-set-top --conversation-id <cid> [--off]` / `+flag-list --size <1-100>` | Do not confuse conversation top with message top, Pin, or favorite |
| 群消息完整导出 | `+chat-messages`; save merged JSON if requested | Follow pagination to completion; report partial results as incomplete |
| 机器人多群广播 | Call `+messages-send-by-bot` once per resolved group | Confirm recipients once; preserve one body and return a per-group ledger |
## 统一发送
身份决定真实操作者、可见范围和可用能力;同一目标使用 user、bot 或 webhook 时结果和权限可能不同,禁止自动切换身份重试。Before sending, verify identity, target, body, title, mentions, message type, and attachment path; ask if ambiguous. Reuse the same `--idempotency-key` on retry.
Before sending, verify identity, target, body, title, mentions, message type,
and attachment path; ask if ambiguous. Reuse the same `--idempotency-key` on retry.
```bash
dws chat +messages-send --as user --chat-id <openConversationId> --text "内容" --idempotency-key <key> --format json
@@ -80,26 +71,47 @@ dws chat +messages-send --as bot --robot-code <robotCode> --chat-id <openConvers
dws chat +messages-send --as webhook --webhook-token <token> --title "告警" --text "内容" --at-all --format json
```
- `user` supports text, Markdown, existing mediaId images, and local files. Audio/video use `--file` and are sent as files.
- `bot` supports group or batch-DM text/Markdown. A webhook token selects its chat. Neither identity supports files.
- Pass only required `--at-*` / `--at-all`; never construct `@10` manually. Use real `U+000A` newlines and a blank line between Markdown paragraphs.
- `user` supports text, Markdown, existing mediaId images, and local files.
Audio/video use `--file` and are sent as files.
- `bot` supports group or batch-DM text/Markdown. A webhook token selects its
chat. Neither identity supports files.
- The Shortcut normalizes @ placeholders. Pass only required `--at-*` /
`--at-all`; never construct `@10` manually.
- Newlines in `--text` / `--markdown` must be real `U+000A`; separate Markdown
paragraphs with a blank line.
## 查询、资源与卡片
### 查询、资源与卡片
- `+search-msg` 只搜索消息内容;禁止用于按群名找群或解析群 CID。
- `+search-msg --page-all` paginates and enriches in batches; use it only when complete pagination is required.
- Preserve primary results on partial failure and return a per-item ledger; never claim an incomplete result is complete.
- If a sender name is absent, keep the real sender ID; do not guess a name or expand into an unrequested directory lookup.
- A missing optional enrichment is not a primary-query failure; record enrichment failures in the ledger.
- `+at-me`、`+chat-messages`、`+messages-mget`、`+search-msg`、`+thread-replies` support `--download-resources --output-dir ./downloads [--overwrite]`; resource download is opt-in.
- For nested resources, prefer the child `messageId` in `resourceRefs`; inherit the parent only if the chat ID is missing.
- These queries and `+messages-resource-download` are `read/not_required`; never pass `--yes`. Keep output relative, inside the working directory, without `..`; add `--overwrite` only when requested.
- Accept reviewed DingTalk/public OSS HTTPS URLs only. Validate every redirect and never forward headers across hosts.
- `+messages-send-card` 的 `--group`、`--receiver`、`--receiver-open-dingtalk-id` are mutually exclusive. With `--content`, update immediately; otherwise return `bizId`. The final `+messages-update-card` call uses `--flow-status 3`.
- `+search-msg --page-all` paginates and enriches in batches. On partial
failure, preserve results and return a per-item ledger.
- `+at-me`、`+chat-messages`、`+messages-mget`、`+search-msg`、`+thread-replies`
support `--download-resources --output-dir ./downloads [--overwrite]`.
For nested resources, prefer the child `messageId` in `resourceRefs`;
inherit the parent only if the chat ID is missing.
- These queries and `+messages-resource-download` are `read/not_required`;
never pass `--yes`. Output must be a relative path inside the working
directory without `..`; pass `--overwrite` only when explicitly requested.
- Accept reviewed DingTalk/public OSS HTTPS URLs only. Validate every redirect
and never forward headers across hosts.
- `+messages-send-card` 的 `--group`、`--receiver`、`--receiver-open-dingtalk-id`
are mutually exclusive. With `--content`, update immediately; otherwise
return `bizId`. The final `+messages-update-card` call must use `--flow-status 3`.
## 低频原子路由
### Shortcut 错误处理
Use this section only when no public Shortcut matches. If sibling commands are ambiguous, read [intent-guide.md](references/intent-guide.md), locate the leaf in [chat.md](references/chat.md#命令索引表), then load only the matching branch reference.
If a leaf is missing or parameters are invalid, check the full Catalog,
Schema, and `--help`, then correct once. Re-extract IDs from actual output.
Stop and report insufficient permission, unresolved ambiguity, no result, or
contract conflict.
## 渐进加载与一级路由
After choosing atomic fallback, select a top-level branch. If sibling commands
are ambiguous, read [intent-guide.md](references/intent-guide.md), then locate
the leaf in [chat.md](references/chat.md#命令索引表). If parameters, constraints,
or safety are uncertain, read
`dws schema --cli-path "chat <leaf>" --format json`; use leaf `--help` only
when Cobra flags are uncertain.
```text
dws chat
@@ -112,10 +124,43 @@ dws chat
├── mute* / hide / set-top
├── mark-* / clear-* # read/unread, red dots, message clearing
├── text / chmod / data-auth
├── +shortcut
└── scripts
```
Branch references: [消息](references/chat/chat-message.md), [群与成员](references/chat/chat-group.md), [机器人与 Webhook](references/chat/chat-bot.md), [会话状态与分组](references/chat/chat-conversation.md).
Load branch details on demand: [消息](references/chat/chat-message.md),
[群与成员](references/chat/chat-group.md),
[机器人与 Webhook](references/chat/chat-bot.md), and
[会话状态与分组](references/chat/chat-conversation.md).
## 核心意图与执行边界
Use this table to disambiguate identity, chat type, and operation. Prefer public
Shortcuts; otherwise use the atomic fallback. Apply the shared `--format json`
rule and take every downstream ID from actual output. Every Chat workflow
must work without Python.
| 用户说 | 首选路由 / 原子回退 | 必须保留的执行边界 |
|---|---|---|
| “发给某人” | `+dm --to <姓名> --text <内容>` / `message send --open-dingtalk-id` | Resolve one real person; never pass a name as an ID |
| “发到某群” | `+send-to-group --group <群名> --text <内容>` / `chat search` → `message send --group` | Resolve one real cid; mentions/`@all` use `+messages-send` with that cid |
| “用应用机器人发” | `+messages-send --as bot` / `message send-by-bot` | Never impersonate the current user; keep robot and target IDs from real output |
| “Webhook 推送” | `+messages-send --as webhook` / `message send-by-webhook` | Webhook identity is separate; mention text and `--at-*` flags must agree |
| “建群 / 拉人进群” | `+chat-create --name <群名> --users <uid,...>` / `group members add` | Resolve every member to a real `userId`; extract the new cid before follow-up actions |
| “拉某个会话的消息” | `+chat-messages` / `message list` | Choose one group or DM target and use the user's time range or an explicitly narrowed boundary |
| “搜消息关键词 / 组合搜索” | `+search-msg --query <关键词>`;群内 @我用 `--at-me --group <cid>` | Add only real filters; use `--page-all` only when complete pagination is needed |
| “撤回用户消息 / 机器人消息” | `+messages-recall --conversation-id <cid> --msg-id <mid>` / `recall-by-bot` | Only on explicit recall; IDs must come from the same identity and conversation |
| “群消息翻页导出” | `+chat-messages`; save merged JSON if requested | Follow pagination to completion and report partial results instead of claiming a complete export |
| “查和某人的聊天记录” | Resolve the person, then use `+chat-messages` with one user ID | Stop for ambiguous people; do not merge different users with the same name |
| “机器人多群广播” | Call `+messages-send-by-bot` once per resolved group | Confirm the recipient set once, preserve one message body, and return a per-group success/failure ledger |
Detailed native CLI loops are in [01-messaging.md](references/01-messaging.md).
## 低频操作原子回退入口
Use this table only to locate atomic fallbacks when no public Shortcut matches.
Follow leaf Schema, `--help`, and the linked reference for flags, risk, and
confirmation.
| 用户意图 | 原子回退 | 关键边界 |
|---|---|---|
@@ -133,17 +178,34 @@ Branch references: [消息](references/chat/chat-message.md), [群与成员](ref
| 行为授权 / 跨组织聊天数据授权 | `chmod` / `data-auth cross-org` | Grants action or cross-org read access; executes neither target action |
| 退群 / 解散群 | `group quit` / `group dismiss` | First exits current user; second dismisses the entire chat |
## 错误恢复与按需 Reference
## 跨命令关键边界
- If a leaf is missing or parameters are invalid, follow the established Catalog → Schema → `--help` order and correct once.
- Re-extract every downstream ID from actual output; never repair an ID by guessing.
- For complex messaging, read [01-messaging.md](references/01-messaging.md); for onboarding, read [01-onboarding.md](references/workflows/01-onboarding.md).
- On command error, read [chat-error-recovery.md](references/chat-error-recovery.md) and correct once.
- Stop and report insufficient permission, unresolved ambiguity, no result after re-search, or contract conflict.
- Never mix current-user, app-bot, and Webhook identities.
- `message list` reads one chat; use `search` / `search-advanced` for keywords
and combined filters.
- `openTaskId` is not `openMessageId`; approval, calendar, todo, and other
product IDs cannot replace a message ID.
- Message top, message Pin, and conversation top are distinct operations.
- Follow leaf Schema and current `--help` for parameters, risk, and
confirmation; never infer across safety fields.
## Workflow 与错误导航
- For complex messaging, read [01-messaging.md](references/01-messaging.md).
- For onboarding, read
[01-onboarding.md](references/workflows/01-onboarding.md).
- On command error, read
[chat-error-recovery.md](references/chat-error-recovery.md) and correct
once. Stop on insufficient permission, unresolved ambiguity, or no result
after re-search.
## 跨产品协作
- 收件人是人名 → use `dingtalk-contact` or `dingtalk-aisearch` to obtain one real `openDingTalkId` / `userId`.
- 要发本地图片/文件 → use `dws chat message send --msg-type file --file-path <本地路径>`. Images are downloadable attachments, not inline. Use `--msg-type image --media-id` only with an existing valid mediaId; DWS cannot convert local files to mediaId.
- 收件人是人名 → use `dingtalk-contact` or `dingtalk-aisearch` to get
`openDingTalkId` / `userId`.
- 要发本地图片/文件 → use
`dws chat message send --msg-type file --file-path <本地路径>`. Images are
downloadable attachments, not inline. Use `--msg-type image --media-id`
only with an existing valid mediaId; DWS cannot convert local files to mediaId.
- 紧急升级(应用内/短信/电话)→ route to `dingtalk-ding`.
- 发邮件 → route to `dingtalk-mail`.
+54 -127
View File
@@ -1,6 +1,6 @@
---
name: dingtalk-doc
description: 钉钉文档(adoc):创建、读取、编辑、块、评论、附件、导出、版本及Markdown/JSONML写入。原生 .md→dingtalk-misc;文件→dingtalk-drive;知识库→dingtalk-wiki;axls→dingtalk-misc,able→dingtalk-aitable。
description: 钉钉在线文字文档(adoc):创建、读取、追加、覆盖、块级编辑、评论、附件、导入导出、版本、模板、封面/背景,以及 Markdown/JSONML 保真写入。Use when 用户要写文档、读文档、改正文、处理富文本块或文档评论。原生 .md 文件走 dingtalk-markdown;普通文件存储与上传下载走 dingtalk-drive;知识库空间/节点管理走 dingtalk-wiki;电子表格走 dingtalk-sheet,AI 表格走 dingtalk-aitable。命令前缀:dws doc。
metadata:
cli_version: ">=0.2.14"
category: product
@@ -11,152 +11,79 @@ metadata:
# 钉钉文档 Skill
## 前置条件 — 执行操作前必读
## 执行入口
> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dws-shared`](../dws-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。
> 命令参考:[doc.md](references/doc.md);剧本:[04-document.md](references/04-document.md)。
## 参数硬约束
- 创建文档只用 `--name`,不要写 `--title`。
- 目标文件夹只用 `--folder <文档文件夹nodeId或URL>`,不要写 `--parent` / `--parent-node` / `--parent-id`。
- 目标知识库只用 `--workspace <workspaceId或URL>`,不要写 `--space-id` / `--spaceId`。
- 文档内容只用 `--content` / `--content-file`,不要写 `--markdown`。
- 复杂内容(换行、表格、代码块、长 Markdown)先写临时 `.md`,再用 `--content-file`,不要把大段 Markdown 塞进命令行。
- 每次 `create` / `update` / `block insert` / `media insert` 后必须 `dws doc read` 或 `dws doc block list` 回读关键内容。
执行前完整读取 [`dws-shared`](../dws-shared/SKILL.md)。高频意图用本文件骨架;仅特殊参数、复杂格式或边界不明时读取一个 branch reference。优先级:`骨架/recipe > Shortcut > atomic fallback`。
<!-- VISIBLE_SHORTCUTS_START -->
## Shortcuts(无专用脚本/recipe 时优先)
## Shortcut 发现(按需)
以下 shortcut 同时进入公开 catalog 与 Runtime Schema。先按本 skill 的意图表、脚本和 recipe 路由:存在精确覆盖该场景的专用脚本/recipe 时按其执行;否则用户意图命中时,shortcut 优先于手写原子命令。命令已选中时直接执行;只在参数或安全语义不确定时读取 leaf Schema(例如 `dws schema --cli-path "doc +<shortcut>" --format json`),在当前 Cobra flags 不确定时读取 `dws doc <shortcut> --help`。仅当现有路由和 reference 都无法定位低频能力时,才用 `dws shortcut list --service doc --format json` 批量发现。
`doc` 当前有 17 条公开 Shortcut,已全部进入 Runtime Schema。完整清单保留在 Runtime Shortcut Catalog,根 Skill 不重复展开;单条参数与安全契约按需查询 leaf Schema。已知意图直接使用下方的优先路由、意图表或任务 reference;命令已选中时直接执行,只在参数/安全语义不确定时读取 leaf Schema,在当前 Cobra flags 不确定时读取 leaf Help。
| Shortcut | 风险 | 适用场景 |
|---|---|---|
| `dws doc +comment-create` | write | 在文档上创建一条评论 |
| `dws doc +comment-list` | read | 查询文档评论列表 |
| `dws doc +comment-reply` | write | 回复文档中的一条评论 |
| `dws doc +copy` | write | 复制文档/文件到指定文件夹或知识库 |
| `dws doc +doc-append` | write | 在文档末尾追加一段文本(安全追加,不改动原有内容) |
| `dws doc +export-get` | read | 根据 jobId 查询文档导出任务结果 |
| `dws doc +export-submit` | read | 提交在线文档导出任务 (docx/markdown/pdf),返回 jobId |
| `dws doc +find-doc` | read | 按关键词搜索云文档并投影关键字段(只读) |
| `dws doc +list` | read | 列出文件夹或知识库下的直接子节点 |
| `dws doc +move` | write | 移动文档/文件到指定文件夹或知识库 |
| `dws doc +search` | read | 按关键词搜索有权限的文档 (不传则返回最近访问) |
| `dws doc +share-doc` | write | 按姓名把文档链接私信发给某人(自动解析 userId) |
| `dws doc +template-list` | read | 获取文档模板列表 |
| `dws doc +template-search` | read | 根据关键词搜索文档模板 |
| `dws doc +version-list` | read | 查看文档历史版本列表 |
| `dws doc +version-revert` | high-risk-write | 回滚文档到指定历史版本 |
| `dws doc +version-save` | write | 手动保存文档版本快照 |
仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service doc --compact --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。
<!-- VISIBLE_SHORTCUTS_END -->
## 意图表
命令和参数清楚时直接执行。只用真实 `cli_path`;`confirmation=user_required` 时先确认再加 `--yes`。普通创建直接调用 `dws doc create`,仅用户要求本地包装器时用 `doc_create_and_write.py`。
| 用户说 | 命令 |
|--------|------|
| "创建文档(短内容)" | `dws doc create --name "<标题>" --content "<内容>"` |
| "创建+写入(长内容自动分块)" | `python scripts/doc_create_and_write.py --name "<标题>" --content "<内容>" [--mode append\|overwrite]` |
| "搜在线文字文档 / 找在线文字文档" | `dws drive search --query "<关键词>" --format json` → `dws drive info --node <nodeId> --format json` → 仅 `extension=adoc` 使用 `dws doc read --node <nodeId> --format json` |
| "读在线文字文档(adoc)内容" | `dws doc read --node <nodeId> --format json` |
| "更新文档内容 / 分块追加" | `dws doc update --node <nodeId> --content "<分块>" --mode append` |
| "删除块" | `dws doc block delete`(需用户确认) |
| "导出 docx / markdown / pdf" | `dws doc export --node <nodeId> --export-format <docx|markdown|pdf> --output <path>` |
| "导入本地文件为在线文档" | `dws doc import --file <path> --folder <FOLDER_NODE_ID> --name "<标题>" --format json`(详见 `references/doc/doc-import.md`) |
| "查模板 / 套用模板创建文档" | `dws doc template list|search|apply`(详见 `references/doc.md` 模板管理) |
| "保存 / 查看 / 回滚在线文字文档(adoc)版本" | `dws doc version save/list/revert` |
## 核心对象、位置与格式
## 标准 SOP(必遵流程)
| 对象 | 核心标识与边界 |
|---|---|
| 文档 | 使用真实 `nodeId` / `dentryUuid` 或完整 alidocs URL;纯数字 `dentryId`、单独 `dentryKey` 不能替代 |
| 目标位置 | `--folder` 只接文档文件夹 nodeId/URL;`--workspace` 只接知识库 ID/URL;不要猜 `--parent*` |
| 块 | `blockId` / JSONML `uuid` 必须来自 `block list`,更新节点的 uuid 必须与目标块一致 |
| 评论 | `commentKey` 来自评论 list/create;划词评论还需同一块的真实 `start/end` |
| 异步任务 | 导出 `jobId` 与导入 `taskId` 只查询对应任务,不能替代 nodeId |
| 新建资源续用 | create/mkdir/import/copy 返回的新 `nodeId` / `fileId` 立即绑定后续“这篇/刚才那篇/这个文件夹”;禁止同名搜索改用旧资源 |
| 内容格式 | Markdown 适合线性正文;已有富结构优先 JSONML/块级编辑,禁止用 Markdown overwrite 误称保真 |
| 普通文件 | adoc 才用 `doc read/export`;`.md`、axls、able 和普通文件按真实 `extension` 切对应 Skill |
> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 nodeId/blockId。结构化命令必须带 `--format json`,执行后必须按"验证"步回读真实字段。文件类操作(上传/下载/复制/移动)切 `dingtalk-drive`;知识库节点管理切 `dingtalk-wiki`。
## 核心意图与执行骨架
### SOP-1 查找并读取文档(query-doc)
结构化命令加 `--format json`,ID 只取真实输出。写入后回读;未回读不能宣称内容完整。
**触发**:查文档/读文档/某文档在哪/搜文档内容。
### 短链路 Fast Path
1. **定位(必须)**:用户已提供 URL / `nodeId` 时直接使用原值;未提供目标时才执行 `dws drive search --query "<关键词>" --format json`,再取候选结果的真实 `nodeId`。
2. **探测(必须)**:对选中的候选执行 `dws drive info --node <nodeId> --format json`,从真实返回读取 `extension`;不得因为搜索结果标题像“文档”就跳过探测。
3. **按类型读取(必须)**:
- `extension=adoc`:`dws doc read --node <nodeId> --format json`;大文档只抽取用户需要的章节。
- `extension=md`:切到 `dingtalk-markdown` 用 `dws markdown fetch --node <nodeId> --format json` 读取原文;仅需文件实体下载时切 `dingtalk-drive` 用 `drive download`。
- `extension=axls`:切到 `dingtalk-misc`,读取 `references/sheet.md` 后按电子表格意图执行。
- `extension=able`:切到 `dingtalk-aitable`。
- `extension=xlsx` / `xls` / `xlsm` / `csv` 或其他普通文件:切到 `dingtalk-drive`;不得执行 `dws doc read`。
- 不超过 5 个确定性 DWS 操作时,不创建 Todo、不逐步汇报、不预读 Reference;保存真实 ID 连续执行,最终回查后答复。
- 按“先/再/然后”切分操作阶段。阶段中的 `insert/插入`、`append/追加/补一段`、`update/改成`、`list/查看块`、`delete/删除` 必须映射为对应真实命令,不能提前折叠进 create。
- create 只承载首阶段的初始正文,后续续用其 `nodeId`。当前请求将先创建资源时,禁止预先搜索同名资源解析“这篇/那篇”。
**禁止**:用户未提供目标时跳过搜索并猜 nodeId、未探测类型就执行 `doc read`、把整篇文档原样贴给用户。
| 用户意图 | 精确骨架 | 必须保留的执行边界 |
|---|---|---|
| 按名称找文档 | `+find-doc --query <关键词>`;需最近访问/扩展名/创建者等过滤用 `+search` | 候选不唯一先消歧;随后 `drive info --node <nodeId>` 判 `extension` |
| 读取 adoc | `drive info --node <nodeId>` → `doc read --node <nodeId>` | 用户已给 nodeId/URL 时不再搜索;非 adoc 不调用 `doc read` |
| 创建文档 | `doc create --name <标题> --content-file <tmp.md> [--folder <folder> | --workspace <ws>]` | 原生写入管道自动分片;取 `nodeId` 后 `doc read`,缺链接再 `doc info` |
| 显式块工作流 | 按用户原顺序执行 `create → block insert/list/update/delete` | 每个阶段是真实调用;标题、段落、列表等显式插入走 block insert |
| 末尾补短文本 | `+doc-append --doc <nodeId> --text <内容>` | 该 Shortcut 为 write/user_required;确认后执行并 `doc read` 核对 |
| 改写正文 | `doc read --content-format jsonml` → `block update` 或 `doc update --content-format jsonml --mode overwrite` | 单块优先块级编辑;整篇 overwrite 先预览/确认,Markdown overwrite 不保富结构 |
| 评论与回复 | `+comment-list --node <nodeId>` → `+comment-create` / `+comment-reply` | `commentKey` 来自真实结果;写 Shortcut 先确认;划词评论走 atomic `comment create-inline` |
| 导入 / 导出 | `doc import --file <path> ...` / `doc export --node <nodeId> --export-format <fmt> --output <path>` | Word/Excel 等本地文件要求“在线编辑/转在线文档”必须 import;drive upload 只保留普通文件。仅超时/中断后用 `import get` / `export get` |
| 版本操作 | `+version-list --node <nodeId>` → `+version-save` / `+version-revert --version <N>` | save/revert 先确认;revert 版本号必须来自 list,完成后回读 |
| 模板创建 | `+template-list` / `+template-search --query <词>` → atomic `template apply --template-id <id>` | templateId 来自真实列表;要复刻已有文档形态时用 drive copy + 副本块级更新 |
| 分享链接给某人 | `+share-doc --to <姓名> --url <docUrl> [--note <附言>]` | 会真实发消息,确认后执行;同名人员必须消歧,不改变文档权限 |
### SOP-2 创建文档并写入(create-doc)
## 写入与验证边界
**触发**:新建文档/写一篇/建文字文档。
- `--name` 是文档外壳标题,但不能覆盖用户显式要求的正文 H1。用户说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时,必须原样创建正文 H1;只有用户未要求正文 H1 时才默认从 H2 开始以避免重复。
- 用户显式列出的操作是验收步骤:create 只能承载明确要求的初始内容;后续 `list`、`insert`、`append`、`update` 必须逐项真实调用。若要求“有序列表块”,必须写入 JSONML `p.list.isOrdered=true`(或等价原生列表块),普通 Markdown/普通段落不算完成。
- 创建只用 `--name`;内容只用 `--content` / `--content-file`。长、多行、表格或特殊字符必须用临时 UTF-8 文件和 `--content-file`。
- 原生 Markdown 写入管道在内容超过 10,000 个 Unicode 字符时自动按结构分片;不要在 Skill 或脚本中预先复制分片循环。仅在 `CONTENT_TRUNCATED`、中断或回读缺失时按 [04-document.md](references/04-document.md) 恢复。
- `doc update --mode append` 不清空原文;`--mode overwrite` 会清空后重写,先 `--dry-run`,得到确认后才加 `--yes`。
- 已有 callout、分栏、样式、@人、图片或附件时,先读 JSONML;局部改动优先 `block update`,不要用 Markdown 整篇重写。
- `block insert` 默认追加;只有明确相对位置时才传真实 `--ref-block` / `--parent-block`。`block delete` 和评论删除必须确认。
- 写后按对象验证:正文用 `doc read`,块/附件用 `doc block list`,元信息/链接用 `doc info`,版本用 `version list`。
1. **执行(必须)**:`dws doc create --name "<标题>" --content-file <tmp.md> [--folder <FOLDER_NODE_ID> | --workspace <WORKSPACE_ID>] --format json`(长/多行内容用 `--content-file`,不要用 `--content` 拼长串;用户未指定位置时省略两个位置参数,创建到“我的文档”根目录)。
2. **验证(必须)**:从返回取 `nodeId`,立即 `dws doc info --node <nodeId> --format json` 回读确认。
## 低频 Reference
**禁止**:创建后不回读就答复"已创建"、把 `--folder` 当成空间 ID 传入。
[doc.md](references/doc.md) 只是 atomic 分支索引。每次只读一个对应 branch reference;JSONML workflow/cookbook/schema 仅在构造复杂 JSONML 后加载,不递归预读。
### SOP-3 覆盖/追加内容(write-content)
## 错误恢复
**触发**:覆盖写/追加内容/改文档正文。
1. **执行(必须)**:覆盖先执行 `dws doc update --node <nodeId> --mode overwrite --content-file <tmp.md> --dry-run --format json` 预览,用户确认后改用 `--yes` 实际覆盖;追加执行 `dws doc update --node <nodeId> --mode append --content-file <tmp.md> --format json`。
2. **验证(必须)**:写后 `dws doc read --node <nodeId> --format json` 抽取受影响段落核对。
**禁止**:不加 `--yes` 反复重试覆盖、跳过 `--dry-run` 直接覆盖未确认的长文档。
### SOP-4 导出 / 下载(export-doc)
**触发**:导出文档/下载文档/转 PDF·Markdown。
1. **判类型(必须)**:先 `dws drive info --node <nodeId> --format json`;`extension=adoc` → `dws doc export --node <nodeId> --export-format <pdf|markdown|docx> --output <path> --format json`;普通文件 → 切 `dingtalk-drive` 用 `dws drive download --node <nodeId> --output <path> --format json`。
**禁止**:不分类型一律走 `doc export`(普通文件会失败)、跳过 `drive info` 判断。
### SOP-5 块级编辑(block-edit)
**触发**:插引用块/代码块/表格/分栏/图片/附件,或删除某块。
1. **先列块(必须)**:`dws doc block list --node <nodeId> --format json`,当前响应的可操作块 ID 位于 `blocks[].element.id`(部分版本可能回显为 `blockId`);必须从目标内容对应项读取,不得编造。空文档的占位空段落可能不能作为 `--ref-block`。
2. **按动作执行(必须)**:
- 插入:默认追加用 `dws doc block insert --node <nodeId> --text "<内容>" --format json`;只有明确要求相对位置时才加 `--ref-block <非空参照块ID> --where before|after`,容器内插入使用 `--parent-block <父块ID> --index <位置>`。插入命令**不接受** `--block-id`。
- 更新:`dws doc block update --node <nodeId> --block-id <目标blockId> --text "<新内容>" --format json`。
- 删除:用户确认后执行 `dws doc block delete --node <nodeId> --block-id <目标blockId> --yes --format json`。
3. **验证(必须)**:再次执行 `dws doc block list --node <nodeId> --format json` 核对插入、更新或删除结果。
4. **复杂块(必须)**:插入引用/代码/表格/分栏/附件/图片前,**必须**先读 [doc.md](references/doc.md) 对应小节,**禁止**只停在"准备查看 help"——说"我将插入..."后必须立即执行命令。
**禁止**:编造 blockId、未确认就删除、把完整 `--help` 输出当成最终结果答复用户。
### SOP-6 导入本地文件为在线文档(import-file)
**触发**:导入 Word / Excel / Markdown / 本地文件为在线文档。
1. **判类型(必须)**:确认用户意图是“导入为在线文档”,不是“上传到钉盘”。仅上传存储时切 `dingtalk-drive`。
2. **执行(必须)**:`dws doc import --file <path> --folder <FOLDER_NODE_ID> --name "<标题>" --format json`;复杂参数和限制见 [doc-import.md](references/doc/doc-import.md)。
3. **验证(必须)**:拿到返回 `nodeId` 后执行 `dws doc info --node <nodeId> --format json`,必要时 `dws doc read --node <nodeId> --format json` 抽样核对内容。
**禁止**:把上传文件到钉盘误当成 doc import;不知道目标文件夹 nodeId 时先切 `dingtalk-drive`/`dingtalk-wiki` 查询。
## 多步文档短路径
- 在目标文件夹创建文字文档:`dws doc create --name "<标题>" --folder <FOLDER_NODE_ID> --content-file <tmp.md> --format json`。拿到 `nodeId` 后立即回读。
- 块级编辑固定顺序:`doc block list --node <nodeId>` → 插入用 `--ref-block`/`--parent-block`,更新或删除用 `--block-id` → `doc block list` 验证。删除块必须已有用户明确删除意图或二次确认。
- 插入引用块、代码块、表格、分栏、附件、图片时,优先读 [doc.md](references/doc.md) 对应小节,不要只停在"准备查看 help"。说出"我将插入..."后必须立即执行对应 terminal 调用。
- 用户要求多个子文档/附件/块操作时,按 checklist 串行完成;最后一条 assistant 消息不能停在"接下来我要...",必须有实际工具调用或明确失败原因。
- 用户说“读取并下载/导出”时,先 `drive info --node ... --format json` 按
`extension` 判断类型:`adoc` 用 `doc export`,普通文件切到
`dingtalk-drive` 用 `drive download`。
- 所有结构化 dws 命令带 `--format json`。仅参数不确定时查 `--help`,不要把完整 help 当成最终结果。
## 危险操作
`block delete` 不可逆,必须确认再加 `--yes`。
- 路径或参数错误:按既定顺序查 leaf Schema、再查 leaf Help,校正一次;不要连续尝试近似参数。
- 始终从实际输出提取并续用 `nodeId`、`blockId`、`commentKey`、`jobId` 或 `taskId`,不得搜索同名项覆盖当前请求的新 ID。
- 部分写入或回读缺失:保留已创建的 nodeId,报告已完成范围与缺失位置;先读回,再只补缺失内容,禁止无条件重新创建副本。
- 权限不足、候选未消歧、目标类型不符、没有可推进的任务 ID 或 Schema/Help 冲突时停止并报告。
## 跨产品协作
- 文件存储 / 上传下载 → 切到 `dingtalk-drive`
- 知识库空间管理 → 切到 `dingtalk-wiki`
- 数据表 → 切到 `dingtalk-aitable`
- 原生 `.md` 文件读取、创建、全量覆盖或局部替换 → 切到 `dingtalk-markdown`
- 长篇报告生成(多源采集 + 写文档)→ 此 skill 提供 `doc_create_and_write.py` 脚本
## 局部意图与短流程
- [局部意图消歧](references/intent-guide.md);[短流程](references/lite-recipes.md)。
`.md` 走 `dingtalk-markdown`;普通文件走 `dingtalk-drive`;知识库走 `dingtalk-wiki`;表格走 `dingtalk-sheet` / `dingtalk-aitable`;评论 userId 走人员 Skill。边界读 [intent-guide.md](references/intent-guide.md),固定流程读 [lite-recipes.md](references/lite-recipes.md)。
@@ -5,10 +5,10 @@
| Recipe | 行动指南(固定路线) |
|--------|-------------------|
| import-file | 1. **直接执行** `dws doc import --file <本地文件路径> --format json`(一条命令完成上传+转换+创建)<br>2. 从返回中提取 `documentUrl` 并告知用户<br>3. **禁止先 Read 文件内容再 `doc create` + `doc update`**——`doc import` 是服务端格式转换,客户端无需解析文件内容<br>4. 可选参数:`--folder <文件夹ID>` 指定目标文件夹、`--workspace <知识库ID>` 指定目标知识库、`--name "文档名"` 自定义名称<br>5. 格式映射:docx/doc→文档, xlsx/xls→表格, xmind/mark→脑图, md/txt→文档<br>6. 超时或中断时 CLI 返回 `taskId`,用 `dws doc import get --task-id <taskId>` 手动查询<br>详见 [doc-import.md](./doc/doc-import.md) |
| write-doc | 0. 阅读 [doc-create-workflow.md](./doc/style/doc-create-workflow.md) 的 §前置必读 + §关键词速查表,锁定文档类型和起稿路径:**决策型/含对比的知识沉淀型/用户要求美观 → JSONML 起稿**(`.json`);执行型/说明型 → Markdown 起稿(`.md`)<br>1. 按选定路径执行 doc-create-workflow.md(JSONML 路径有骨架范例可直接复制修改)<br>2. `doc create --content-file /tmp/<name>.json --content-format jsonml`(或 `.md` + `--content-format markdown`)<br>3. 大内容默认依赖 DWS 自动分片;只有 `CONTENT_TRUNCATED`、部分写入失败或回读发现缺失时,才按 workflow 的恢复流程手工补片<br>4. **回读校验(必须)**:所有写入完成后,执行 `doc read --node <nodeId>`,校验关键标题/段落/表格是否完整写入 |
| write-doc | 1. 普通线性正文直接写入 UTF-8 `.md`,执行 `doc create --name <标题> --content-file <tmp.md> --content-format markdown --format json`<br>2. 仅当用户要求复杂版式且确实选择 JSONML 时,读取 [doc-create-workflow.md](./doc/style/doc-create-workflow.md) 对应章节,不预读整套 style/reference<br>3. 大内容依赖 DWS 自动分片;仅在 `CONTENT_TRUNCATED`、中断或回读缺失时恢复<br>4. 取 create 返回的 `nodeId` 执行 `doc read --node <nodeId> --format json`,核对明确要求的标题、段落和结构 |
| search-docs-and-share | 1. `dws drive search --query "<关键词>" --format json` → 取候选 `nodeId` + 标题建索引(不读全文)<br>2. 对追问选中的候选执行 `dws drive info --node <nodeId> --format json`<br>3. 仅 `extension=adoc` 使用 `dws doc read --node <nodeId> --format json`(最多 2 篇);`md` / `axls` / `able` / 普通文件分别切到 markdown / sheet / aitable / drive,禁止固定执行 `doc read` |
| create-knowledge-base | 1. 创建知识库空间取 `WS_ID`<br>2. `wiki node create --workspace <WS_ID> --name "<文档名>"` → 取 `nodeId`<br>3. `wiki node list --workspace <WS_ID>` 确认 |
| migrate-doc | 1. `doc read --node <源nodeId>` → 取正文并写入临时文件 `<tmp>.md`<br>2. `doc create --name "<文档名>" --folder <DOC_FOLDER_NODE_ID> --content-file <tmp>.md` → 取新 `nodeId`(`--folder` 只传文档文件夹 nodeId / alidocs 文件夹 URL,不传数字 dentryId;正文 <200KB 单步到位)<br>2a. 若正文 >200KB:**必须先向用户提示截断风险**(详见下方「分块 append 截断风险提示」),用户确认后再执行:`doc create --name "<文档名>" --folder <DOC_FOLDER_NODE_ID>` → `nodeId` → 按段落切片 → 每片 `doc update --node <nodeId> --content-file <part> --mode append`<br>3. **回读校验**:`doc read --node <nodeId>` 校验内容完整性 |
| migrate-doc | 1. `doc read --node <源nodeId>` → 取正文并写入临时文件 `<tmp>.md`<br>2. `doc create --name "<文档名>" --folder <DOC_FOLDER_NODE_ID> --content-file <tmp>.md` → 取新 `nodeId`;所有长度都先走这一条原生命令,由 CLI 自动分片(`--folder` 只传文档文件夹 nodeId / alidocs 文件夹 URL,不传数字 dentryId)<br>3. **回读校验**:`doc read --node <nodeId>` 校验内容完整性;仅在 `CONTENT_TRUNCATED`、中断或回读缺失时,从真实断点补写缺失部分 |
| update-doc-section | 1. `dws drive search --query "<关键词>" --format json` → 取 `nodeId`<br>2. `dws drive info --node <nodeId> --format json`,仅 `extension=adoc` 继续;其他类型切到对应 skill/reference<br>3. **形态选择(按 [doc-update-workflow.md §1.3](./doc/style/doc-update-workflow.md) 优先级)**:目标段落含 callout / 分栏 / 颜色 / @人 / 附件 / 嵌套结构 → 走 `jsonml-node-edit`;纯文本替换且确认无富结构 → 继续本 recipe<br>4. `dws doc read --node <nodeId> --format json` 定位目标章节<br>5. `dws doc update --node <nodeId> --content "<替换内容>" --mode overwrite --yes --format json`<br>6. **回读校验**:`dws doc read --node <nodeId> --format json` 确认 overwrite 未被降级为 append、内容完整无截断<br>**overwrite 须用户确认**;完整改写流程见 [doc-update-workflow.md](./doc/style/doc-update-workflow.md) |
| rewrite-doc | 1. 阅读并执行 [doc-update-workflow.md](./doc/style/doc-update-workflow.md):先看 §1.3 编辑形态优先级(**JSONML 首选**),再按 §3 速查表选路径,跳 §4 对应小节执行<br>2. 单块改写 / 含富结构 → §4.4 路径 B;多处保真改写或改 root → §4.4 路径 A;纯文本骨架重写 → §4.5 markdown<br>3. 整篇 overwrite 前必须按 workflow §4.5 向用户提示风险并等待确认<br>4. **回读校验(必须)**:按 workflow §6 的校验要点逐项核查;@人、附件、图片等保真要素必须原样保留<br>**适用场景**:用户提供已有 nodeId/链接,需要改写、润色、章节补充、段落形态转换、整篇重写 |
| doc-to-message | 1. `doc read --node <nodeId>` → 取正文(大文档只摘要+链接)<br>2. `aisearch person --keyword "<姓名>" --dimension name` → 取 `openDingTalkId`(推荐);或 `chat search --query "<群名>"` → 取 `openConversationId`<br>3. `chat message send --open-dingtalk-id <openDingTalkId> --text "<内容>"`(推荐)或 `--group <openConversationId> --text "<内容>"` 发送。仅当无法获取 openDingTalkId 时才用 `--user <userId>`(备选) |
@@ -29,28 +29,17 @@
---
## 分块 append 截断风险提示
## 自动分片失败恢复
### 触发条件
`doc create` / `doc update` 的 Markdown 写入管道会从 10,000 个 Unicode 字符开始自动分片,超时后降到 5,000 字符重试。不要在调用前手工复制分片逻辑。
当内容总大小 **超过 200KB**,需要拆分为多片通过 `doc update --mode append` 分块写入时,**必须在执行前向用户发出截断风险提示**,等待用户确认后再继续。
只有返回 `CONTENT_TRUNCATED`、写入被中断或回读发现缺失时,才执行恢复:
### 提示话术(参考模板)
> 注意: 内容较长(约 {size}),需要分 {n} 片写入。分块 append 存在以下风险:
> - 部分片段可能写入失败但返回 success,导致文档**内容截断或缺失**
> - 片段之间的表格、代码块等跨块元素可能**被截断破坏**
> - 写入顺序异常可能导致**段落错乱**
>
> 建议:写入完成后我会回读校验文档完整性。是否继续?
### 执行规范
1. **提示时机**:在执行第一片 append **之前**提示,而非写入过程中
2. **分片原则**:按段落/标题边界切分,**禁止**在表格、代码块、列表内部截断
3. **逐片校验**(推荐):每写入一片后记录已写入的最后一个标题/段落标记,供最终回读时比对
4. **最终回读**(必须):所有片段写入完成后,执行 `doc read --node <nodeId>` 回读全文,逐片比对关键标记是否完整(详见下方「doc update 回读校验规范」)
5. **失败处理**:若回读发现缺失片段,向用户报告具体缺失位置,建议针对缺失部分单独重试 append
1. 保存命令返回的真实 `nodeId` 和 `chunksWritten`,不要重新创建文档。
2. `doc read --node <nodeId>` 定位最后一个完整标题/段落。
3. 从原始输入中只提取缺失后缀,写入一个新的 UTF-8 文件。
4. 向用户报告已完成范围与待补范围;得到继续指示后,用一次 `doc update --mode append --content-file <missing.md>` 补写。
5. 再次回读并核对关键标题、表格、列表和代码块;仍缺失则停止,不循环追加。
---
@@ -68,3 +57,8 @@ dws doc read --node <nodeId> # 校验关键标题、段落首句、表格、@
```
**禁止**在未回读的情况下向用户报告「已完成」。
## 显式工作流
- 用户点名的 `create → list → insert/append/update` 是可观察命令链,必须保持顺序逐项执行;create 只承载明确的初始正文。有序列表块必须验证回读结构中的 `list.isOrdered=true`。
- 新建资源返回 ID 后,同一请求的指代默认绑定该新资源;禁止搜索同名旧资源替换绑定。
+15 -24
View File
@@ -11,44 +11,35 @@
> **操作后请返回文档 URI**:每次执行 create / read / update 等操作后,从返回数据中提取 `docUrl` 直接返回;缺失时用 `doc info --node <ID>` 补查。
## 前置条件 — 执行操作前必读
## 按需加载边界
**CRITICAL — 执行对应操作前,MUST 先用 Read 工具读取以下子文件:**
本文件只做低频 atomic 路由,不是任何命令的前置必读。根 Skill 已覆盖的普通 create/read/block/import/export 直接执行;仅在命令已选中但特殊参数或边界仍不明确时,读取下方对应的一个 branch reference。只有实际执行复杂 JSONML 保真写入时才加载 workflow/cookbook/schema,禁止递归预读整组文件。
1. **解析 URL / 定位文档**(几乎所有命令都需要先拿 nodeId)
→ 必读 [`doc/doc-info.md`](doc/doc-info.md)(URL/dentryKey 提取规则、ID 边界、extension 路由、**获取 nodeId 三种方式 A/B/C**)
## Atomic 命令加载契约
2. **创建或编辑文档内容**(`doc create` / `doc update` / `doc block insert|update`)
→ 必读 [`doc/style/doc-update-workflow.md`](./doc/style/doc-update-workflow.md)(**形态优先级硬规则:JSONML > element JSON > markdown**;markdown overwrite 会丢富结构)
- 从零创建时加读 [`doc/style/doc-create-workflow.md`](./doc/style/doc-create-workflow.md)
- **任何 `doc create` 都必须先读 [`doc/style/doc-style-guideline.md`](./doc/style/doc-style-guideline.md) §2.0 类型决策表 + §1 硬规则**(决定骨架 + 全局约束,不读就不知道用哪种骨架)
- 涉及 callout / 分栏 / 富 block 精修时再加读 style-guideline §4-§7 + [`doc/format/doc-jsonml-cookbook.md`](./doc/format/doc-jsonml-cookbook.md)
本文件只负责 atomic branch 路由;公开 Shortcut 的选择与发现只在根
[`SKILL.md`](../SKILL.md) 维护,不在 references 重复列出。
**未读以上文件就改写已有文档会导致富结构丢失、参数错误或样式不达标。其他命令(阅读 / 评论 / 权限 / 附件 / 下载导出 / 文件操作)按需查下方 §命令索引表跳转对应子文件加载,不必提前加载。**
## 查询命令帮助
当你不确定某个命令的具体参数、格式或可选项时,**优先执行 `--help` 查询**,不要猜测参数名或凭记忆编造。
命令已经选中且参数清楚时直接执行,不做重复发现。参数、约束或安全语义不确定时,
先查询精确 leaf Schema;只有当前 Cobra flag 不确定时才查询精确 leaf `--help`。
不要加载产品级全量 Schema,也不要猜测参数名。
```bash
# 查看 doc 下所有子命令
dws doc --help
# 参数、约束或安全语义不确定
dws schema --cli-path "doc update" --format json
# 查看具体命令的完整参数说明
# 仅 Cobra flag 不确定
dws doc read --help
dws doc create --help
dws doc block insert --help
# 查看子命令组下的所有命令
dws doc block --help
dws doc media --help
```
规则:
- 参数名不确定时 → 先 `--help`,再调用
- 报错 "unknown flag" 时 → `--help` 确认正确的 flag 名称
- 不确定某个功能是否存在时 → `dws doc --help` 查看命令列表
- 参数、约束、安全不确定 → leaf Schema
- `unknown flag` 或当前 Cobra flag 不确定 → leaf `--help`,修正一次
- 能力或分支未知 → 先用下方场景/命令索引;仍无法定位才回根 Skill 的 Runtime Shortcut Catalog
- Schema 与 Help 冲突 → 采用更安全解释并报告契约漂移
## 命令索引表
@@ -1,15 +1,11 @@
# doc block(块级精细编辑:list / insert / update / delete)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、JSONML validator 行为)
> 3. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — JSONML 范例(含 callout / 分栏 / 表格 / 标题等节点的完整命令)
> 4. [`./format/doc-jsonml-schema.md`](./format/doc-jsonml-schema.md) — JSONML 节点结构字段定义
>
> **同任务常配合**:[`doc-update.md`](./doc-update.md)(整篇 overwrite / 末尾追加纯文本)/ [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md)(JSONML 复制范例)
> 本文件自包含简单 list/insert/update/delete 契约,不要递归预读路由或 style reference。只有实际构造复杂 JSONML 节点时,才读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);字段仍不确定时再查 [`doc-jsonml-schema.md`](./format/doc-jsonml-schema.md)。整篇 overwrite 或纯文本 append 才转读 [`doc-update.md`](./doc-update.md)。
> **改写已有文档优先 JSONML**:保真度最高、callout / 分栏 / 表格 / @人 / 附件 / 颜色 / 嵌套都能 1:1 round-trip;写入端有 validator 兜底。详见 [`./style/doc-update-workflow.md` §1.3 编辑形态优先级](./style/doc-update-workflow.md)。
> **显式块操作不可折叠**:用户说“先 create,再 list/insert/update/append”时按原顺序真实调用;不能因为最终正文相似,就把后续块操作合并进 create 或一次 Markdown 写入。
---
## doc block list(查询块元素)
@@ -173,7 +169,8 @@ dws doc block delete --node DOC_ID --block-id UUID
- **块类型**:paragraph、heading、blockquote、callout、columns、orderedList、unorderedList、table、sheet、attachment、slot。
- **快捷 vs --element**:`block insert` 优先使用 `--text` 或 `--heading` 快捷方式;复杂块类型(table、callout、columns 等)使用 `--element` JSON 或 `--content-format jsonml`。
- **简单内容追加**:建议用 [`./doc-update.md`](./doc-update.md) `--mode append`,不必走 block insert。
- **有序列表块**:用户明确要求 ordered list / 有序列表块时,必须用 JSONML `p` 节点的 `list.isOrdered=true`(同一 `listId`;仅首项设 `start:1`)或等价原生 orderedList element;带 `1.` 前缀的普通段落、普通 Markdown 或一次 create 不满足要求。
- **简单内容追加**:用户只说追加纯文本且不强调块操作时可用 [`./doc-update.md`](./doc-update.md) `--mode append`;用户明确说 block insert / 插入段落 / 插入标题 / 插入列表块时必须走 block insert。
- **JSONML validator**(写入端默认行为):
- 裸字符串、缺 uuid 等结构错误会被 validator 抦下并返回带 path 的错误(如 `$[2][2]: paragraph child must be span wrapper, got raw string.`)。
- `--fix-jsonml` 开启 JSON 语法修复,推荐 agent 调用。
@@ -241,6 +238,12 @@ dws doc block list --node <DOC_ID> --content-format jsonml --block-id <UUID>
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["p",{},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"新段落"]]]'
# 插入有序列表块(3 项共用 listId,仅首项有 start)
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol1","list":{"listId":"actions","level":0,"isOrdered":true,"start":1}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第一项"]]]'
dws doc block insert --node <DOC_ID> --content-format jsonml \
--element '["p",{"uuid":"ol2","list":{"listId":"actions","level":0,"isOrdered":true}},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"第二项"]]]'
# 插入 callout(colorBlocks)
dws doc block insert --node <DOC_ID> --content-format jsonml --ref-block <UUID> --where after \
--element '["container",{"uuid":"co1","subType":"colorBlocks","metadata":{"bgcolor":"#FDE2E0","border":"#F5C2C7"}},["p",{"uuid":"co1p1"},["span",{"data-type":"text"},["span",{"data-type":"leaf"},"高风险操作,先备份"]]]]'
@@ -1,9 +1,6 @@
# doc comment(文档评论:list / create / reply / update / delete / create-inline)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
>
> **同任务常配合**:`dws aisearch person`(查 `--mention` 用 userId)/ `dws chat search`(查群用 openConversationId)/ [`doc-block.md`](./doc-block.md)(划词评论必须先取 blockId 与 paragraph 文本)
> 本文件自包含评论命令契约。仅在需要 mention 时查询真实 userId/openConversationId;仅在划词评论尚无 blockId 与 paragraph 文本时读取 [`doc-block.md`](./doc-block.md) 并执行 block list。
---
@@ -1,11 +1,6 @@
# doc create(创建文档)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-create-workflow.md`](./style/doc-create-workflow.md) — 创建工作流(标题、位置、骨架、回读校验)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范(草稿元素清单、骨架样板)
> 4. [`./doc-update.md` §内容写入管道](./doc-update.md) — 长内容自动分片、`--content-file` vs `--content` 选择
> 5. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
> 本文件自包含普通 Markdown 创建契约,不要递归预读 `doc.md`、style 或 update reference。仅当用户要求复杂版式并实际选择 JSONML 时,读取 [`doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md);需要文档骨架建议时才读取对应 style 章节。
## 创建路由前置判断(必看)
@@ -40,11 +35,11 @@ Flags:
## 关键说明
- **`--name` 是 H1**:正文从 `##` 开始;正文内不要再写 `#` 一级标题(除非确需且已说明动机)。
- **标题优先级**:`--name` 是文档外壳标题,默认可视作 H1;但它不能替代用户显式要求的正文一级标题。用户说“正文写 `# ...`”“正文先起个一级标题”时,必须在初始内容中保留该 `#` H1;用户未要求正文 H1 时,正文默认从 `##` 开始以避免重复。
- 不传 `--folder` 和 `--workspace` 时,默认创建在「我的文档」根目录。
- `--folder` 仅接受文档文件夹 `nodeId` / `dentryUuid` / alidocs 文件夹 URL;**禁止**传入 drive `dentryId`、`parentId`、`spaceId` 这类纯数字 ID。
- 输入方式选择见 [`./doc-update.md` §内容写入管道](./doc-update.md)(与 update 共用)。短文本字面量可 `--content`,多行/表格/特殊字符必须 `--content-file` 或 `--content -`。
- 长内容(>30000 字符)CLI 自动分片:先创建空文档拿 `nodeId`,再按 markdown 标题边界切分后逐片 append;调用方无需手动编排。
- Markdown 超过 10,000 个 Unicode 字符时,CLI 自动按结构分片:第一片随 `create` 写入并取得 `nodeId`,后续片自动 append;调用方不要手动预分片。
## 上下文传递
@@ -54,6 +49,12 @@ Flags:
| `docUrl` | 最终交付给用户的链接;缺失时用 [`./doc-info.md`](./doc-info.md) 补查 |
| `chunksWritten` | 判断是否触发自动分片;> 1 时重点检查章节顺序 |
同一请求后续出现“这篇/刚才那篇/上次那篇”时,直接续用本次 create 返回的 `nodeId`;禁止先搜索同名文档再把后续操作指向旧节点。
## 显式操作序列
用户点名 `block list`、插入、追加、更新等后续动作时,必须按原顺序逐项执行。`doc create` 只写用户指定的初始内容,不能为了减少调用把后续标题、列表或段落提前塞进 create。例:`创建 → 查看块结构 → 末尾插入段落` 必须真实执行 create、block list、block insert 三步。
## 回读验收(必读)
CLI **不会**自动回读校验。**每次创建后**都必须执行 `doc read --node <nodeId>` 校验关键标题、段落首句、表格表头是否完整。详见 [`./style/doc-create-workflow.md` «回读验收»](./style/doc-create-workflow.md)。
@@ -70,7 +71,7 @@ dws doc create --name "<文档名>" --content-file /tmp/<name>.md --folder <DOC_
# 创建到知识库
dws doc create --name "<文档名>" --content-file /tmp/<name>.md --workspace <WS_ID> --content-format markdown
# 创建空文档(仅取 nodeId 后再分步写入,适合 >200KB 兜底)
# 创建空文档(用户明确需要空文档时)
dws doc create --name "<文档名>" [--folder <ID> | --workspace <ID>] --content-format markdown
# 短纯文本字面量(< 2KB 且无换行/表格才允许)
@@ -1,7 +1,6 @@
# doc export(在线文档导出为 docx/markdown/pdf)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 export 契约。已有当前请求返回的 adoc nodeId 时直接导出;目标类型未知时只执行一次 `drive info`,不要递归读取 `doc.md`。
> **路由前置判断**:用户说「下载/导出」时**必须**先用 `dws drive info --node <ID> --format json` 查 `extension`:
> - `extension` 为 `adoc`(在线文档)→ **必须用 `export`**,禁止用 `download`
@@ -51,6 +50,7 @@ Flags:
## 关键说明
- 同一请求中刚执行 create/copy/import 并紧接着说“这篇/刚才那篇/上次那篇”时,`--node` 必须使用该写操作真实返回的新 `nodeId`;不得预先搜索同名文档,也不得用搜索结果中的旧节点替换它。
- `export` 是一体化命令,一条命令自动完成提交→轮询→下载,**无需手动编排轮询**。CLI 内部使用渐进式退避轮询(最多约 5 分钟)。
- `export` 超时或中断后,CLI 会输出 `jobId`,可用 `dws doc export get --job-id <jobId>` 手动查询任务状态。
- `export` 支持钉钉在线文档(alidocs,`contentType=ALIDOC`)导出为 `docx`、`markdown` 或 `pdf`,**在线表格导出请使用其他命令**。
@@ -1,11 +1,12 @@
# doc import(本地文件导入为在线文档)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 import 契约。文件、目标 folder/workspace 与参数已知时直接执行;只有参数或安全语义不确定时查询精确 leaf Schema,不要递归读取 `doc.md`。
> **支持的文件格式**:docx, doc, xlsx, xls, md, txt, xmind, mark
> **文件大小限制**:20MB
> **在线编辑硬路由**:用户说“上传后在线编辑/大家直接在线改/转成钉钉文档”时必须使用 `doc import`。`drive upload` 只保留原始 `.docx/.xlsx/...` 普通文件,不能据此宣称已可在线编辑。若用户明确要同时保留原文件和在线版,才先 `drive upload`,再单独 `doc import`,并分别验证两个返回节点。
---
## doc import(一体化命令)
@@ -63,6 +64,7 @@ Flags:
## 关键说明
- `import` 是一体化命令,一条命令自动完成创建会话→上传→确认→轮询,**无需手动编排**。CLI 内部使用渐进式退避轮询(最多约 5 分钟)。
- 导入完成后必须用返回的 `documentUrl`/`nodeId` 执行 `drive info` 或 `doc info`,确认 `extension=adoc`(Word/文本)或对应在线类型,并确认目标 `folderId`;只有验证后才能说“可直接在线编辑”。
- `import` 超时或中断后,CLI 会输出 `taskId`,可用 `dws doc import get --task-id <taskId>` 手动查询任务状态。
- 支持的文件格式:docx, doc, xlsx, xls, md, txt, xmind, mark(共 8 种)。
- 文件大小限制:20MB。超过限制时 CLI 会直接报错,不会发起网络请求。
@@ -1,8 +1,6 @@
# doc info(获取文档元信息 + URL 解析)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`url-patterns.md`](../../../dws-shared/references/url-patterns.md) — 仅当用户原始 `alidocs` URL 需要 probe 时
> 本文件自包含已知 nodeId/URL 的 info 契约。只有原始 alidocs URL 类型仍不明确时,才读取 [`url-patterns.md`](../../../dws-shared/references/url-patterns.md);不要递归读取 `doc.md`。
>
> **探测入口变更**:alidocs URL 的类型探测现在统一走 `dws drive info`(详见 [链接规范](../../../dws-shared/references/url-patterns.md#alidocs-url-类型探测流程))。`drive info` 检测到 `extension=adoc/axls/able` 时会自动调用 `doc info` 返回更详细的文档信息。**仅在 `drive info` 已确认是 ALIDOC 类型后**,才需要直接使用 `doc info`。
>
@@ -1,7 +1,6 @@
# doc media(附件 / 图片:download / insert)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 本文件自包含 media insert/download 契约。nodeId、文件路径或 resourceId 已知时直接执行;只在需要相对块定位且 blockId 未知时读取 [`doc-block.md`](./doc-block.md)。
> ⚠️ **图片插入硬规则**:
> - 图片来源如果是钉盘/文档空间中的文件,**必须先下载到本地**(`dws drive download --node <图片nodeId> --output /tmp/xxx.png`),再执行 `media insert`
@@ -1,10 +1,6 @@
# doc read(读取文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-info.md`](doc-info.md)(先解析 URL,确认 extension=adoc)/ [`doc-update.md`](doc-update.md)(读后改写)/ [`doc-block.md`](doc-block.md)(块级精修前先读结构)
> 本文件自包含普通 read 契约。用户已给当前 adoc nodeId/URL 时直接读取;类型未知时才先执行 `drive info`。选择 JSONML 只为获取结构,不要求预读 cookbook;实际构造 JSONML 写入时再按需加载。
## 命令格式
@@ -1,12 +1,6 @@
# doc update(更新文档内容)
> **前置条件(MUST READ):** 执行本命令前,必须先用 Read 工具读取以下文件:
> 1. [`../doc.md`](../doc.md) — 命令路由 + 场景索引 + 意图判断 + 工作流
> 2. [`./style/doc-update-workflow.md`](./style/doc-update-workflow.md) — 改写流程(编辑形态优先级、分片 append、回读验收)
> 3. [`./style/doc-style-guideline.md`](./style/doc-style-guideline.md) — 排版规范
> 4. [`./format/doc-jsonml-cookbook.md`](./format/doc-jsonml-cookbook.md) — 仅当使用 `--content-format jsonml` 时必读
>
> **同任务常配合**:[`doc-read.md`](./doc-read.md)(改写前必读,jsonml 模式拿当前结构;担心被并发覆盖时再取 revision)/ [`doc-block.md`](./doc-block.md)(单 block 改写优先;本命令更适合追加 / 整篇 overwrite)
> 本文件自包含普通 append/overwrite 契约,不要递归预读路由或 style reference。纯文本 append 可直接执行;overwrite 先 read/dry-run/确认。只有保真改写或复杂 JSONML 才读取 [`doc-update-workflow.md`](./style/doc-update-workflow.md) 与 cookbook;单块修改改用 [`doc-block.md`](./doc-block.md)。
## 命令格式
@@ -75,7 +69,7 @@ Flags:
## 内容写入管道(create / update 共用)
> **关键原则**:CLI 内置自动分片。超长内容(>30000 字符)自动按 markdown 结构切分后逐片写入,对调用方透明。写入完成后由调用方自行决定是否回读确认。
> **关键原则**:CLI 内置自动分片。Markdown 超过 10,000 个 Unicode 字符时自动按结构切分后逐片写入,对调用方透明。调用方必须在写入后回读确认。
### 输入方式选择
@@ -88,9 +82,9 @@ Flags:
### 自动分片行为
当内容超过 30000 字符时,CLI 自动执行:
当 Markdown 内容超过 10,000 个 Unicode 字符时,CLI 自动执行:
1. **create**: 先创建空文档拿 `nodeId`,再按 markdown 标题边界切分后逐片 append
1. **create**: 第一片随 create 写入并取得 `nodeId`,后续片 append
2. **update (overwrite)**: 第一片用 overwrite,后续片用 append
3. **update (append)**: 所有片段用 append
@@ -124,13 +118,13 @@ CLI **不会**自动执行回读验证。**你必须在文档写入完成后主
### 进度输出示例
```
[INFO] 内容较长 (45000 字符),自动分片写入...
[INFO] 已创建空文档 (nodeId=abc123),开始分片写入...
[INFO] 写入分片 (1/3),15000 字符...
[INFO] 写入分片 (2/3),15000 字符...
[INFO] 写入分片 (3/3),15000 字符...
[INFO] 内容较长 (25000 字符),自动分片写入...
[INFO] 写入分片 (1/3),10000 字符 (create)...
[INFO] 文档已创建 (nodeId=abc123)
[INFO] 写入分片 (2/3),10000 字符...
[INFO] 写入分片 (3/3),5000 字符...
[INFO] 全部 3 个分片写入完成
{"success": true, "nodeId": "abc123", "chunksWritten": 3}
{"success":true,"nodeId":"abc123","chunksWritten":3}
```
### CONTENT_TRUNCATED 错误
@@ -153,24 +147,16 @@ CLI **不会**自动执行回读验证。**你必须在文档写入完成后主
| `--content -` | 从 stdin 读取(可配合 heredoc/pipe) |
| `--content-file path` | 从文件读取(UTF-8),推荐 |
### 短/中等长度(< 200KB)— 单步写入
### 单一原生命令(所有长度默认路径)
```bash
# 1. 把内容写入 UTF-8 文本文件:
# Linux/Mac: /tmp/<name>.md;Windows: %TEMP%\<name>.md
# 2. 一步写入:
# 2. 一次调用;CLI 按需自动分片:
dws doc update --node <DOC_ID> --content-file <tmp> --mode overwrite --content-format markdown
```
### 超长(> 200KB 兜底)— 分片追加
```bash
# 1. 按 markdown 标题或段落边界切成 ≤200KB 的片段(不要切断表格)
# 2. 逐个追加:
dws doc update --node <nodeId> --content-file <part> --mode append --content-format markdown
```
> **注意**:分块 append 存在静默失败风险(部分片段返回 success 但实际未写入),执行前**必须**向用户发出截断风险提示并等待确认。完整规范见 [`../../best_practices/04-document.md` «分块 append 截断风险提示»](../04-document.md)。
只有命令返回 `CONTENT_TRUNCATED`、被中断或回读确认缺失时,才先读取已写入内容定位断点,再用一次 `doc update --mode append --content-file <missing.md>` 补缺失部分。不要在调用原生命令前复制一套手工分片循环。
### stdin 变体
@@ -4,11 +4,9 @@
> 改写已有文档见 [doc-update-workflow.md](./doc-update-workflow.md)。排版规范见 [doc-style-guideline.md](./doc-style-guideline.md)。
## 前置必读
## 按需使用
> **同时读取 [doc-style-guideline.md](./doc-style-guideline.md):**
> - **§2.0 类型判断决策表** → 锁定文档类型(决策型 / 执行型 / 说明型 / 知识沉淀型)和骨架
> - **§1 硬规则** → 全程生效(`--name` 已是 H1、不编造 URL、Markdown 草稿不写 callout 等)
普通 Markdown 创建不需要先读本文件或 style guideline。只有用户要求设计文档骨架或复杂版式时,才查看下方对应章节;实际选择 JSONML 后再读取 cookbook。需要按文档类型选骨架时,按需读取 [doc-style-guideline.md](./doc-style-guideline.md) 的对应一节,不要通读。
### 关键词速查(用户意图 → 起稿路径)
@@ -45,7 +43,7 @@
| 项目 | 要求 |
|------|------|
| 标题 | 用 `--name` 传入;正文不要再重复同名一级标题 |
| 标题 | 用 `--name` 传入;用户显式要求正文 H1 时原样保留,未要求时正文默认从 H2 开始 |
| 位置 | 默认创建到我的文档;指定目录时只接受文档文件夹 `nodeId` 或 alidocs 文件夹 URL |
| 正文 | 多行、表格、代码块、特殊字符或长度 >= 2KB 时必须写入 UTF-8 临时 `.md` 文件 |
| 格式 | 按 §JSONML 起稿判定 决定起稿路径:命中 JSONML 起稿条件时**直接用 JSONML 构造**(跳过 markdown);未命中时用 Markdown 起稿,创建后按 [doc-update-workflow.md](./doc-update-workflow.md) 精修 |
@@ -226,7 +224,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
> **脚手架策略警示**:Markdown 无法表达分栏/callout/色彩表头,拉回的 JSONML 只有纯文本骨架。精修阶段不是“在现有结构上加色”,而是“参照 RFC/Spec 重组结构”。
> **MUST READ**:动手写 JSONML 前,必须先用 Read 工具读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md) — 其中 §决策型文档骨架范例 有可直接复制修改的完整模板。
> **JSONML 条件加载**:仅在确定使用 JSONML 后读取 [doc-jsonml-cookbook.md](../format/doc-jsonml-cookbook.md);其中“决策型文档骨架范例”可直接改写。
> 节点类型和属性的权威定义见 [doc-jsonml-schema.md](../format/doc-jsonml-schema.md)。
### ⚠️ JSONML 降级约束
@@ -274,7 +272,7 @@ callout 可通过 `"showstk": true, "sticker": "图标名"` 配置顶部贴纸
```
- 根节点固定 `"root"`(不是 `"body"`)
- `--name` 已是 H1,JSONML 从 `h2` 开始
- 用户未要求正文 H1 时,JSONML 从 `h2` 开始;用户明确要求“正文一级标题/插入一级标题”时必须构造 `h1`
- 表格结构是 `table → tr → tc`(无 `th`/`td`)
- 分栏是 `table` + `"sr": true`,`tc` 建议设 `fill` 背景色
- 有序列表:仅第一项设 `"start": 1`,后续项不设 `start`(系统自动递增)
@@ -315,7 +313,7 @@ dws doc read --node <nodeId> --content-format jsonml --output /tmp/<name>-readba
- 只使用用户已提供或对话中已确认的正文素材。
- 如果正文素材不足,先补齐文档目标、受众、章节和缺口;不要在本文中临时扩展跨产品采集流程。
- **先按 [doc-style-guideline.md §2.0 类型判断决策表](./doc-style-guideline.md) 确定文档类型,再用对应类型的骨架样板(§2.1 决策型 / §2.2 执行型 / §2.3 说明型 / §2.4 知识沉淀型)**。不要套通用三段式。
- **`--name` 已是 H1,正文从 `##` 开始**;正文内不要再写 `#` 一级标题(除非确实需要正文内再造一级 H1 并说明动机)。
- **`--name` 是外壳标题,不覆盖显式正文 H1**:用户未要求正文一级标题时从 `##` 开始;用户明确给出 `# ...` 或要求“先起一级标题”时,正文必须保留该 H1。
- 摘要、bullet、引用块、callout 等元素的使用边界以 style-guideline §3-§7 为准。
- 同类信息保持一致:风险、状态、行动项各用一种元素 + 一种视觉语义(style-guideline §1.2 / §5)。
- 临时文件必须保留真实换行,不能把换行写成字面量 `\n`。
@@ -25,7 +25,7 @@
## 一、硬规则
1. **`--name` 是 H1**:正文从 `##` 开始;正文内不写 `#`(除非确需正文内再造一级 H1 并说明动机)
1. **用户显式正文 H1 优先**:`--name` 是文档外壳标题;用户未要求正文一级标题时从 `##` 开始。用户明确说“正文写 `# ...` / 先起一级标题 / 插入一级标题”时必须保留或插入真实 H1,不得用 `--name` 代替
2. **同类信息同表达**:风险、状态、行动项、证据,每类只用一种元素 + 一种视觉语义(见 §5)
3. **Markdown 草稿阶段只用稳定元素**:标题、段落、列表、checklist、表格、代码块;callout / 分栏 / 附件 / 复杂嵌套留到创建后用 `doc block insert` / `doc media insert` 精修
4. **引用块只用于原文**:用户原话、会议摘录、外部材料原文;不许包装作者自己的结论
@@ -209,7 +209,7 @@
### 4.1 标题与段落
- 正文从 `##` 开始(H1 已被 `--name` 占用)
- 用户未要求正文 H1 时从 `##` 开始;用户明确要求正文 H1 时按原文使用 `#` 或 heading level 1
- 标题层级 ≤ 4 层(§7)
- 单段过长先拆段,再考虑换元素
@@ -217,6 +217,7 @@
- 普通列表:并列要点
- 有序列表:顺序步骤
- 用户明确要求“有序列表块”时必须使用真实列表结构;JSONML 为带 `list.isOrdered=true` 的多个 `p` 节点,不能只写带数字前缀的普通段落或以整篇 Markdown 代替显式 block insert
- checklist:待办状态(含 `- [ ]` / `- [x]`)
列表项里开始出现"负责人 / 截止时间 / 状态"这类字段时,改用表格。
@@ -99,7 +99,7 @@ JSONML 模式下这些元素的节点结构见 [doc-jsonml-schema.md](../format/
| 整篇按新骨架重写 | overwrite 全文(优先 JSONML;纯文本可用 markdown) | §4.5 |
| 段落转表格 / 表格转段落 | block update --content-format jsonml;或 markdown overwrite 单段 | §4.4 / §4.1 |
| 插入附件 / 图片 | doc media insert | §4.3 |
| 一次追加 >200KB 内容 | 分块 append + 用户风险确认 + 逐片记录 | §4.6 |
| 长 Markdown 追加 | 单一 `doc update --content-file`,由 CLI 自动分片;失败时再按真实断点恢复 | §4.6 |
| 兜底:纯文本快速替换某段 | doc update --content overwrite(markdown) | §4.1 |
---
@@ -254,17 +254,11 @@ dws doc update --node <nodeId> --content-file /tmp/<name>-full.md --mode overwri
**写入后必须回读**(§6)。如果发现旧内容残留,按 §6 的修复路径处理。
### 4.6 超长内容追加(分块 append)
### 4.6 长 Markdown 追加与失败恢复
当一次性追加内容 **超过 200KB** 时,必须拆分为多片 `--mode append`,并在执行第一片**之前**向用户发出截断风险提示等待确认。
默认只执行一条 `doc update --node <nodeId> --mode append --content-file <long.md>`。CLI 在超过 10,000 个 Unicode 字符时自动按结构分片,并在超时后缩小分片重试;不要预先手工拆片。
完整规范(提示话术模板、触发条件、失败处理)见 [04-document.md «分块 append 截断风险提示»](../../04-document.md)。
update 场景下的额外约束:
1. 按段落/标题边界切分,**禁止**在表格、代码块、列表内部截断
2. 每写一片记录已写入的最后一个标题/段落标记,供 §6 回读比对
3. 与既有内容衔接位置不能产生悬空标题或断列表
只有返回 `CONTENT_TRUNCATED`、中断或 §6 回读发现缺失时,才按 [04-document.md «自动分片失败恢复»](../../04-document.md) 从最后一个真实完整段落补写缺失后缀。补写前报告已完成范围,补写后再次回读;同一缺失后缀最多自动校正一次。
---
@@ -10,10 +10,11 @@
| "参照这个生成同样的 / 按模板生成 / 复刻 X / 同样的模板 X 月份的" + 已有 alidocs URL | 模板保形生成同形态变体 | `drive copy + drive rename + doc block update` → 见 [best_practices/04-document.md `template-based-generation`](../../dingtalk-doc/references/04-document.md#template-based-generation) | `doc read + doc create`(重写链) | adoc → markdown 是有损投影,read+create 会丢行高/单元格背景色/字号;copy 在 adoc 层保形复制后只在副本上局部修改 |
| "这个 alidocs 表格链接帮我看下"(粘贴原始 URL) | 先 probe 节点类型 | `dws drive info --node` → 按 `extension` 路由 | 直接调 `sheet` | `alidocs/i/nodes/{id}` 可能是文档/axls/able/xlsx 等,禁止凭 URL 猜类型 |
| "帮我记一下明天要做的事" | 创建个人待办 | `todo` | `doc` | 个人待办提醒,非文档内容 |
| "在知识库里创建一个文档" | 创建空文件实体 | `wiki node create --type adoc` | `doc create` | 空间内创建节点归 wiki;doc create 是向已有文档写入内容,不是创建文件节点 |
| "在这个知识库里写一篇/创建带内容的文档" | 在已知知识库根创建 adoc 并写入正文 | `doc create --workspace <workspaceId> --name ... --content-file ...` | 先建空 wiki 节点再重复写入 | `doc create` 的真实 Cobra/Schema 支持 `--workspace`,适合一步创建带内容 adoc |
| "只在知识库里建一个空文档节点/整理节点层级" | 创建或管理知识库节点实体 | `wiki node create --type adoc` | `doc create` | 空节点与目录层级归 wiki;正文创作归 doc |
| "帮我看看收到的日报" | 收到的日志 | `report` | `doc` | 钉钉日志系统(日报/周报),不是文档 |
| "整理一下XX项目的所有讨论" | 跨源主题归档 | #5 generate-topic-report | #4 write-doc | #4 侧重单篇文档创作;按主题跨听记/群消息汇总属于工作汇报 |
| "搜一下智能化方案/最近 OKR 相关邮件/最近发版相关消息" | 搜企业知识内容 | `aisearch enterprise` | `doc search` / `mail search` / `chat message search` | 跨文档、消息、日程、听记、邮件等企业内容语义检索走 enterprise;具体 `queries/types/time-range` 抽槽见 `aisearch.md` |
| "我发给某人的消息/邮件/文档/今天我干了什么" | 搜行为记录 | `aisearch behavior` | `chat` / `mail` / `doc` / `report` | 关注“谁对什么做过什么”,走 behavior;具体 `behavior-type/direction/chat-scope` 抽槽见 `aisearch.md` |
| "把这段文字翻译成英文/translate this" | 通用文本翻译 | `chat text translate` | `doc` / `aisearch` | 纯文本翻译,不是文档编辑或语义搜索 |
| "帮我把这个文档翻译成日文" | 文档内容翻译 | 先 `doc get` 再 `chat text translate` | `chat text translate` 直接传文件 | translate 仅支持纯文本,需先提取文档内容 |
| "帮我把这个文档翻译成日文" | 文档内容翻译 | 先 `doc read --node <nodeId>` 再 `chat text translate` | `chat text translate` 直接传文件 | translate 仅支持纯文本,需先读取 adoc 正文 |

Some files were not shown because too many files have changed in this diff Show More