diff --git a/docs/multi-im-optimization-plan.md b/docs/multi-im-optimization-plan.md new file mode 100644 index 00000000..db5b314a --- /dev/null +++ b/docs/multi-im-optimization-plan.md @@ -0,0 +1,1546 @@ +# Multi IM 优化完整执行方案 + +> 状态:方案已实施并完成端到端验证 +> +> 日期:2026-08-03 +> +> 适用范围:DWS Multi Skill 的 IM 发送、读取、搜索、群创建、回复、资源下载与个人 IM 事件监听 +> +> 实现分支:`codex/multi-im-optimization`(基于最新 `origin/main`) +> +> Token 口径:`tiktoken 0.12.0`、`o200k_base` + +## 0. 执行摘要 + +本方案的核心不是继续增加 Shortcut,也不是盲目追求 Skill 更短,而是提高每个控制 Token 对正确选路、正确执行、安全和结果判断的贡献,并把仍需 Agent 手工完成的名称解析、分页、富化、资源处理和事件映射下沉到 CLI。 + +目标路径统一为: + +```text +激活 dingtalk-chat +→ 从有限 Golden Route 中选择最小充分的意图入口 +→ CLI 内部完成自然目标解析、参数校验、分页、富化和执行 +→ 返回稳定、可证明完整性的任务级 DTO +``` + +优化顺序冻结为: + +```text +PR 1:Skill 路由收敛与 shared 冷启动解耦 +→ PR 2:统一 Resolver 与发送执行内核 +→ PR 3:读取、搜索、消息 DTO 与资源闭环 +→ PR 4:扩展建群与回复闭环 +→ PR 5:IM 事件高阶入口 +→ 后续:依赖下层的新能力 +``` + +其中 PR 1 收益最大、风险最低:不需要新增后端能力,就能消除 Skill 仍在推荐的旧多步链路。PR 2 是功能层 Token 价值密度和模型轮次收益最大的改造。 + +### 0.1 实施结果 + +截至 2026-08-03,本方案的 PR 1~PR 5 已在同一实现分支完成: + +| 阶段 | 已实现结果 | +|---|---| +| PR 1 | Chat 根 Skill 改为 Golden Route;最小运行契约由单一 authored source 注入 Chat/Shared;正常单产品任务不再要求完整读取 Shared;卡片、Bot、复杂转发与运维细节按需加载 | +| PR 2 | 新增共享 typed target resolver、结构化 candidates 错误和批量全量预检;`+dm`、`+send-to-group` 与 `+messages-send` 复用发送执行内核;高级 user 发送支持 `--user-query/--chat-query` | +| PR 3 | `+chat-messages` 支持 `--chat-query/--user-query`,`+search-msg` 支持批量 `--chat-query/--sender-query`;list/search/mget/thread 共享 `MessageViewV1` 投影和 `im.message-list.v1` 完整性契约;资源失败并入任务级 ledger | +| PR 4 | `+chat-create --member-query` 在读取当前用户和创建群前完成全部成员解析;`+messages-reply` 返回 `im.message-reply.v1` 后续上下文并保证 dry-run 不触达写 transport | +| PR 5 | 新增 `event +listen-im`,把 kind/events/自然目标确定性编译为 EventKey 集合,并复用既有订阅、bus、ready、NDJSON、回滚和清理生命周期;Event 根 Skill 收敛为 Golden Route | + +Schema 暴露同步完成:`+chat-create`、`+messages-reply` 和 `event +listen-im` 已进入 reviewed CommandRegistry、selection 与 metadata;生成 Catalog 由源输入重新生成,没有手改输出快照。 + +### 0.2 验证结果 + +截至 2026-08-03,最终工作树通过以下验证: + +| 验证层 | 命令 / 结果 | +|---|---| +| 构建 | `go build -o /tmp/dws-multi-im-codex-build ./cmd` 通过 | +| 全仓测试 | `DWS_PACKAGE_VERSION=0.0.0-test go test ./...` 通过;90 个包、13,545 个测试/子测试通过事件,命令退出码为 0 | +| 真实 CLI 子进程 E2E | `go test ./test/mock_mcp -run '^TestMultiIME2E_NaturalTargetsCompletenessAndWriteBoundaries$' -count=1 -v` 通过 9 个场景 | +| E2E 覆盖 | 自然用户解析后恰好一次写;文件真实 PUT→commit→发送;用户歧义零写;资源通过受信任 HTTPS 实际原子落盘;第二页失败保留结果并 `complete=false`;Event 自然群 dry-run 零订阅;自然群完整读取;建群 dry-run 零创建;回复 continuation context | +| Event lifecycle E2E | facade 直通真实 lifecycle,覆盖多订阅 ready、正常退出全部清理、第二项创建失败回滚第一项且不启动 consumer | +| 真实钉钉组织 smoke | 当前 OAuth 身份创建仅含自己的临时测试群,真实发送后 delivery=`SUCCESS`,再由 `+chat-messages` 读回同一消息且 `complete=true`;`event +listen-im` 创建两个真实订阅并输出 ready,bounded 退出后两类订阅均为空;最后成功解散临时群 | +| Schema 生成漂移 | `go generate ./internal/cli`、`check-generated-drift.sh` 通过;两次独立生成一致 | +| Schema 合同 | `check-schema-catalog.sh` 通过:26 个产品、848 个工具;Runtime confirmation truth 为 137 个 gated 工具 | +| Agent 示例 | `make test-schema-agent-examples` 通过:1,045 个示例,其中 24 个走真实 dry-run | +| Skill 冷启动门禁 | `check-skill-context-budget.sh` 通过:Chat 根 Skill 8,346 bytes,最小 Runtime 契约 1,531 bytes;`check-skill-commands.sh` 校验 1,003 条可执行路径 | +| 精确 Token | 使用冻结的 `tiktoken 0.12.0 / o200k_base`:Chat 根 Skill 2,217;最小 Runtime 契约 380;Event 根 Skill 2,041;完整 Shared 1,493(正常单产品任务不冷加载) | +| 变更卫生 | `git diff --check` 通过 | + +这里的 E2E 是隔离认证目录下启动真实 `app.Execute()` 子进程,经过真实 Cobra 解析和 HTTP JSON-RPC transport 访问本地 mock MCP;它验证 CLI 进程边界、调用顺序、输出 DTO 和写副作用,但不会向真实钉钉组织发送消息或创建群。 + +真实组织 smoke 与本地 E2E 分开执行:只创建带时间戳且仅含当前用户的临时群,不邀请其他用户;消息、读取、事件 ready/清理验证完成后立即解散。文档不记录真实 conversation/message/subscription ID 或凭证。 + +### 0.3 固定场景执行证据 + +| 场景 | 业务 CLI | 内部关键调用 | 结果护栏 | +|---|---:|---|---| +| 已知/自然用户发送 | 1 | resolve → send×1 | 成功只写一次 | +| 多人同名发送 | 1 | resolve | 写副作用 0,返回 candidates | +| 群名文件发送 | 1 | resolve → init → HTTP PUT → commit → send | 上传字节一致,最终只发送一次 | +| 跨会话分页搜索 | 1 | search page 1 → page 2 failure | 保留首屏,`complete=false`、failure ledger | +| 群名读取并下载 | 1 | resolve → list → resource URL → HTTPS download | 受信任域、相对路径、原子落盘、`complete=true` | +| 姓名建群 dry-run | 1 | resolve all → current profile | create 副作用 0 | +| 群事件监听 dry-run | 1 | resolve → compile EventKeys | subscription 副作用 0 | +| 群事件真实 lifecycle | 1 | create×2 → ready → bounded exit → delete×2 | 正常清理;第二项失败时回滚第一项 | +| 引用回复 | 1 | send×1 | 返回 continuation context | + +上述场景均不需要泛化 `schema`/`help` 查询,证明 Golden Route 已把用户可见业务命令收敛为 1。模型轮次属于外部 Agent host 指标;仓库内以 reviewed selection fixture 和命令级 E2E 作为可复现门禁,Ark live-model 评测仍按仓库约定保持显式 opt-in,不作为普通 CI 或本轮合并前置条件。 + +本方案同时冻结以下约束: + +1. 不通过删减安全事实、确认门禁或结果完整性换取 Token。 +2. 不为已有能力重复新增同义 Shortcut。 +3. 不做未版本化的公开输出 DTO 破坏性变更。 +4. 不机械修改 `schema_command_registry.json`;只有稳定 identity、主路径或 alias 真正变化时才评审修改。 +5. 不手改生成的 Schema Catalog 或 Agent metadata。 +6. Token 预算是防膨胀护栏,不是要求删除所有上下文;能显著降低误路由、误写或 false success 的内容应保留。 +7. Golden Route 负责公开意图分流;“统一”只发生在内部 Resolver、执行内核、DTO 和错误契约层,不强行收敛成一个万能公开命令。 + +--- + +## 1. 背景与当前事实 + +> 本章 1.1~1.4 冻结的是实施前基线;其中“当前”均指从 `origin/main` 切出实现分支时的状态,不代表完成后的工作树。完成态和验证证据以 0.1、0.2 为准。 + +### 1.1 当前冷启动成本 + +三套 IM Skill 根加载体量如下: + +| 体系 | 根加载文件 | 行数 | Tokens | +|---|---|---:|---:| +| Mono | `mono/SKILL.md` | 319 | 8,914 | +| Multi | `dingtalk-chat` + `dws-shared` | 229 | 4,922 | +| lark-cli | `lark-im` + `lark-shared` | 460 | 8,449 | + +Multi 的总量已经低于 Mono 和 lark-cli,但这不代表当前加载结构合理。Multi 的 4,922 Tokens 中仍有重复路由、旧 SOP 和无条件 shared 串行读取;lark-cli 的 8,449 Tokens 也不应成为 Multi 的目标预算。应吸收的是 lark-im 已验证的任务闭环、结果契约和渐进披露方式,而不是复制其根文件篇幅。 + +当前 Multi Chat 根加载由两个文件组成: + +| 文件 | 行数 | Tokens | 说明 | +|---|---:|---:|---| +| `skills/multi/dingtalk-chat/SKILL.md` | 139 | 3,402 | IM 路由、意图表和旧 SOP | +| `skills/multi/dws-shared/SKILL.md` | 90 | 1,520 | 全局执行、安全、路由和错误契约 | +| 合计 | 229 | 4,922 | 当前正常 IM 强制冷启动 | + +其中 `dingtalk-chat` 的“标准 SOP”约 1,551 Tokens,是当前产品根 Skill 最大单块;它与前面的 Shortcut 优先路由、意图表存在重复甚至冲突。 + +### 1.2 当前 Runtime 已具备的高频能力 + +| 能力 | 当前入口 | 已有行为 | +|---|---|---| +| 按姓名发单聊 | `chat +dm` | 姓名搜索、唯一性判断、使用 `openDingTalkId` 发送 | +| 按群名发消息 | `chat +send-to-group` | 群名搜索、精确匹配优先、歧义拒绝 | +| 统一发送 | `chat +messages-send` | user/bot/webhook、文本/Markdown、user 文件上传、幂等、@约束 | +| 指定会话读消息 | `chat +chat-messages` | 群/单聊统一浏览语义、默认最近时间、稳定消息投影、可选资源下载 | +| 跨会话搜索 | `chat +search-msg` | 多维过滤、默认近 7 天、翻页、mget 富化、失败 ledger | +| 批量取消息 | `chat +messages-mget` | 批量详情、reaction、资源引用和可选下载 | +| 单资源下载 | `chat +messages-resource-download` | 安全相对路径、默认不覆盖、原子落盘 | +| 引用回复 | `chat +messages-reply` | 自动读取原发送者、可选幂等键 | +| 置顶会话 | `chat +conversation-list-top` | group/direct 规范化、保留 unresolved 项 | +| 创建群聊 | `chat +chat-create` | 当前用户入群并设为群主、基础群类型和话题模式 | +| 事件监听 | `event consume` | 多 EventKey、NDJSON、ready marker、状态与清理 | + +因此,当前首要问题不是 CLI 缺少高频入口,而是 Skill 仍指导 Agent 绕开这些入口。 + +### 1.3 当前 Skill 与 Runtime 的主要冲突 + +| 用户意图 | Skill 旧路线 | 已有最短路线 | 直接代价 | +|---|---|---|---| +| 给姓名发消息 | `aisearch person` → 原子 `message send` | `chat +dm` | 多一次业务命令和模型继续轮次 | +| 给群名发消息 | `chat search` → `+messages-send` | `chat +send-to-group` | 多一次业务命令和 ID 搬运 | +| 已知目标发送文件 | 独立上传 → 提取 mediaId → 发送 | `chat +messages-send --file ...` | 多次调用、旧链路漂移 | +| 读最近聊天记录 | 旧 SOP 强制 `--time` | `chat +chat-messages` 可自动取当前时间边界 | Agent 需要额外构造参数 | +| Webhook 发送 | 原子 `send-by-webhook` | `chat +messages-send --as webhook` | 身份入口分叉 | +| 群消息下载 | 读消息 → 手工识别资源 → 单独下载 | `+chat-messages --download-resources` | Agent 手工 join,失败语义不统一 | + +### 1.4 当前实现仍有的真实缺口 + +1. `+dm` 与 `+send-to-group` 已完成名称解析,但仍各自直接调用发送接口,没有完全复用 `+messages-send` 的统一内容、幂等、AI 标记和输出逻辑。 +2. 当前 person/group resolver 能拒绝零命中和多命中,但错误主要是人类可读字符串,尚未形成稳定结构化 candidates 契约。 +3. `+chat-messages`、`+search-msg`、`+chat-create` 和事件监听仍要求调用者先提供 ID。 +4. `+chat-create` 与 `+messages-reply` 已存在;后续任务是扩展闭环,而不是再次创建同义命令。 +5. 不同消息读取入口的核心字段已较接近,但尚未冻结兼容的公共 Message DTO 和完整性字段。 +6. `dws-shared` 既是共享入口又被产品 Skill 当作每次必须完整读取的文本依赖,导致清晰单产品任务仍多一个串行读取阶段。 + +### 1.5 从 lark-im 吸收什么 + +吸收原则是“迁移设计能力,不迁移平台假设”。 + +| lark-im 中值得吸收的设计 | DWS 当前基础 | Multi 的落点 | 阶段 | +|---|---|---|---| +| 根 Skill 以用户意图选择任务级入口 | 已有多个高阶 Shortcut,但旧 SOP 仍优先 | Golden Route 成为唯一默认路由 | PR 1 | +| 已知 leaf 直接执行,参数和安全由 leaf/Runtime 提供 | Schema/Runtime 已能交付参数和 confirmation | 根 Skill 删除整张参数、身份和 Scope 表 | PR 1 | +| 认证、权限和错误恢复按失败类型加载 | shared 当前无条件读取 | 最小公共契约内嵌,完整 shared 按需 | PR 1 | +| 自然姓名/群名解析进入 CLI | `+dm`、`+send-to-group` 已有局部实现 | 统一 typed Resolver,扩展到 send/read/search/create/event | PR 2~5 | +| 歧义返回 candidates,禁止默认第一个 | 当前能拒绝歧义,但主要是错误字符串 | 稳定 resolution envelope 和退出语义 | PR 2 | +| 发送入口统一承载身份、文件和幂等 | `+messages-send` 已具备主要能力 | 让 `+dm`、`+send-to-group` 复用同一执行器 | PR 2 | +| 读取命令直接保留 reaction、引用和资源语义 | Multi 已有投影、mget 和下载基础 | 统一 MessageView、完整性和 failure ledger | PR 3 | +| 搜索、富化、分页是一个任务闭环 | `+search-msg` 已具备大部分链路 | 增加自然 sender/chat 目标和稳定 DTO | PR 3 | +| 建群先完成全部成员解析再写入 | `+chat-create` 目前只接受 IDs | `--member-query` 全量预检 | PR 4 | +| 普通引用回复与 Thread 回复语义分离 | `+messages-reply` 已存在 | 先补引用回复闭环,Thread 等真实 writer | PR 4/后续 | +| 事件入口按用户语义映射底层 EventKey | 已有 `event consume` 控制面 | `+listen-im` 作为确定性 facade | PR 5 | + +以下内容不得直接从 lark-im 照搬: + +1. Lark 的 `open_id`、`chat_id`、资源 key、消息字段和事件名;DWS 必须保留自己的稳定 identity 和下层接口事实。 +2. 群 description、初始 Bot、visibility、owner/manager、Thread writer 等尚未确认的能力。 +3. Bot/Webhook 与 user 完全等价的富媒体能力假设。 +4. 把嵌套 sender/resources 或 `items` 顶层结构无版本替换到当前公开 DTO。 +5. lark-im 的完整根文件体量、全量命令表或平台特有 SOP。 +6. 依赖 Agent 手工调用 contact/chat search 再搬运 ID 的多步流程;应吸收结果语义,但把确定性步骤下沉到 CLI。 + +最终对齐目标不是命令名相同,而是同一类高频意图在两套 CLI 中都具备:短路由、唯一解析、完整结果、可恢复错误和可证明的安全边界。 + +--- + +## 2. 目标、指标与非目标 + +### 2.1 北极星目标 + +高频 IM 请求应满足: + +```text +一个明确用户意图 +→ 一个最小充分的 Golden Shortcut +→ 最多一个用户可见业务 CLI 命令 +→ 无 Agent 手工搬运 ID、分页或拼接资源结果 +``` + +优化优先级固定为: + +```text +任务正确完成 +→ 安全且无错误副作用 +→ 结果完整、可继续使用 +→ 首次选路正确、少返工 +→ 在满足以上条件后减少无效控制 Token 和模型轮次 +``` + +不能因为根 Skill 变短,却导致更多 Schema/Help 查询、更多纠错轮次或更高 false success;这种变化属于 Token 搬家,不属于优化。 + +### 2.2 价值与效率目标 + +主指标优先于体量指标: + +| 类型 | 指标 | 目标 | +|---|---|---| +| 任务价值 | 首次路由正确率 | 不低于基线,并逐 PR 提升 | +| 任务价值 | 任务成功率、完整结果率 | 不低于基线 | +| 安全价值 | 多候选误写、false success | 0 | +| Token 价值 | 已加载根 Skill 内容均能映射到路由、安全、结果判断或错误恢复场景 | 100% 可解释 | +| Token 价值 | 因 Skill 缺失而追加的 Schema/Help/纠错 Token | 不高于基线 | +| 执行效率 | 用户可见业务 CLI 和模型继续轮次 | 在质量护栏内下降 | + +体量和轮次作为次级量化目标: + +按每轮固定 20K 基础输入计算: + +| 指标 | 当前 | 最终目标 | +|---|---:|---:| +| 核心场景平均模型轮次 | 4.67 | ≤3.7 | +| 平均累计输入 | 111.7K | ≤90K | +| 平均常驻上下文 | 25.7K | 23~24.5K | +| 已知目标发送 | 4 轮 | 3 轮 | +| 唯一姓名发送 | 6 轮 | 3~4 轮 | +| 群名读取并下载 | 5 轮 | 3~4 轮 | +| IM 事件监听 | 5 轮 | 3~4 轮 | + +业务消息正文、文件内容和最终用户回答不计入控制 Token。 + +### 2.3 Skill 预算 + +| 内容 | 目标预算 | +|---|---:| +| `dingtalk-chat/SKILL.md` | ≤2.5K Tokens | +| 正常单产品 IM 强制 shared | 0 | +| 内嵌最小全局契约 | 300~500 Tokens | +| 单个任务 reference | ≤1.5K Tokens | +| 高频任务 reference | 默认不读取 | + +Token 使用 `o200k_base` 计量;CI 同时保留稳定字节预算,防止 tokenizer 环境差异让硬门禁失效。这些预算是评审触发线:超过预算必须解释新增内容防止了什么失败;低于预算也不能作为内容充分的证明。 + +根 Skill 中每个保留区块至少要承担一种明确作用: + +- 选择正确任务入口; +- 阻止高代价误操作; +- 解释 Runtime 输出中无法从 Schema 推导的关键语义; +- 把已知失败精确路由到按需 reference。 + +不承担上述作用的内容迁出;承担作用但表达重复的内容合并;能够由 leaf Schema/Runtime 稳定交付的内容不再复制。 + +### 2.4 质量与安全护栏 + +效率优化必须同时满足: + +- 高频场景任务成功率不低于优化前基线; +- 多候选写操作副作用为 0; +- 所有写操作继续遵循最终 Runtime/Schema 的 confirmation truth; +- 读取、搜索和下载不得把部分结果标记为完整成功; +- `--dry-run` 与真实执行使用同一目标解析链; +- 不泄露 token、appSecret、webhook token 或内部原始错误载荷; +- 不发生跨 profile、跨组织 ID 串用; +- 不通过隐藏命令、未公开 Shortcut 或猜测接口绕过能力边界。 + +### 2.5 非目标 + +本轮不做: + +1. 删除旧原子命令或已发布 Shortcut。 +2. 为了命名统一机械重写 CommandRegistry identity 或主路径。 +3. 把完整 Shortcut Catalog、原子 API 清单或 Scope 表重新放入根 Skill。 +4. 在没有下层能力的情况下承诺 Bot 富媒体、Thread 写入、群 description、初始机器人或断点续传。 +5. 无版本地把现有 `sender` 字段从字符串改成对象,或把 `messages` 顶层数组改名为 `items`。 +6. 同时重构所有 DWS Multi Skill;本方案先验证 IM 路径。 + +--- + +## 3. 目标架构 + +### 3.1 四层职责 + +| 层 | 负责 | 不负责 | +|---|---|---| +| 产品根 Skill | 触发边界、Golden Route、最小不可推导语义 | 全量命令、完整参数表、旧 SOP | +| 最小全局契约 | CLI 使用、安全底线、ID 不猜测、写后验证 | 产品路由大全、认证手册、错误码大全 | +| Task reference | 低频复杂流程、卡片、复杂转发、Bot 管理、事件运维 | 高频命令选择 | +| Runtime + leaf Schema/Help | 参数、约束、身份矩阵、确认、接口与执行 | 自然语言产品路由教程 | + +### 3.2 目标加载路径 + +清晰单产品任务: + +```text +dingtalk-chat 根 Skill(含最小全局契约) +→ 一个 Golden Shortcut +→ 完成 +``` + +低频复杂任务: + +```text +dingtalk-chat 根 Skill +→ 精确 task reference +→ 已知 leaf +→ 仅在参数/安全不确定时查询 leaf Schema +→ 仅在 Cobra flag 不确定时查询 leaf Help +``` + +异常路径: + +```text +认证/权限/profile/未知错误 +→ 按错误类型读取 dws-shared 对应 reference +→ 修复或停止 +``` + +### 3.3 Golden Route + +根 Skill 的核心只保留以下高频路线: + +| 意图 | 唯一推荐入口 | 说明 | +|---|---|---| +| 按姓名发文本/Markdown | `chat +dm` | PR 2 后内部委托统一发送器 | +| 按群名发文本/Markdown | `chat +send-to-group` | PR 2 后内部委托统一发送器 | +| 已知目标、文件、Bot、Webhook | `chat +messages-send` | 身份和内容矩阵由 Runtime 校验 | +| 指定会话读取 | `chat +chat-messages` | 最近消息无需强制 `--time` | +| 跨会话搜索 | `chat +search-msg` | 支持多维过滤、翻页、富化 | +| 读取并下载附件 | 查询命令加 `--download-resources` | 不另起手工下载循环 | +| 查看置顶会话 | `chat +conversation-list-top` | group/direct/unresolved 契约 | +| 监听 IM 事件 | 切换 `dingtalk-event` | PR 5 后优先 `event +listen-im` | + +以下能力作为次级直接入口保留,但不扩大成完整命令表: + +| 意图 | 入口 | +|---|---| +| 已知消息 ID 批量读取 | `chat +messages-mget` | +| 已知资源引用单独下载 | `chat +messages-resource-download` | +| 引用回复 | `chat +messages-reply` | +| 创建基础群 | `chat +chat-create` | + +### 3.4 降级顺序 + +```text +Golden Shortcut +→ 精确 task reference 中的专用 Shortcut +→ 已知原子 leaf +→ leaf Schema / leaf Help +→ 最后才使用低频发现接口 +``` + +已知意图禁止调用: + +- 产品级 Schema; +- `schema --all`; +- 完整 `shortcut list --service chat`; +- 父级 Help 代替 leaf Help; +- 旧原子 SOP 作为默认路线。 + +### 3.5 Golden Route 与内部统一的边界 + +Golden Route 和内部统一解决的是不同层次的问题: + +```text +用户意图层:按任务分流,入口少而明确 + +dm / +send-to-group / +messages-send + +chat-messages / +search-msg / +messages-mget + ↓ +Runtime 内部层:复用同一 Resolver、执行内核、投影和错误契约 +``` + +公开层不建设一个同时承担发送、读取、搜索、下载和事件的万能命令。内部层也不允许每个 Shortcut 各自复制名称解析、分页、消息投影或错误处理。 + +选择公开入口时采用“最小充分能力”原则,而不是“argv 最短”或“功能最多”原则: + +| 意图 | 公开入口 | 选择理由 | +|---|---|---| +| 给姓名发简单文本/Markdown | `+dm` | 语义窄,参数空间小,误选身份概率低 | +| 给群名发简单文本/Markdown | `+send-to-group` | 群目标明确,不暴露无关身份矩阵 | +| 文件、Bot、Webhook、复杂 @、已知 ID 或高级发送控制 | `+messages-send` | 需要统一发送的完整能力矩阵 | +| 浏览一个指定会话的消息 | `+chat-messages` | 以会话和时间窗口为中心 | +| 跨会话或按多维条件检索 | `+search-msg` | 以查询条件、分页和富化为中心 | +| 已知一组消息 ID 获取详情 | `+messages-mget` | 精确读取,不应伪装成搜索 | + +因此,`+messages-send --user-query/--chat-query` 可以支持“自然目标 + 高级发送能力”,但不会取代简单文本场景的 `+dm` 和 `+send-to-group`。三个入口共享内部发送内核,不共享同一份公开参数复杂度。 + +--- + +## 4. Skill 与 shared 设计 + +### 4.1 `dingtalk-chat` 目标结构 + +建议根文件固定为: + +```text +Frontmatter / 触发边界 +→ 最小 DWS 执行契约 +→ Golden Route +→ 三条关键结果语义 +→ 低频 reference 导航 +→ 最短错误分流 +``` + +只保留三类无法完全由 leaf Schema 表达的结果语义: + +1. `openTaskId` 是发送任务 ID,不是回复/撤回使用的消息 ID。 +2. 消息读取默认保留 reaction、`updateTime`、引用、转发和 `resourceRefs`;`--no-reactions` 可关闭 reaction 输出。 +3. Favorite、消息 Pin、消息 Top、会话 Top 是不同对象层级,不能互换。 + +不在根 Skill 重复完整身份矩阵;只说明如何选择 `+messages-send`。具体 target/content/credential 互斥关系以 leaf Schema 与 Runtime 为唯一事实源。 + +### 4.2 必须删除或迁移的内容 + +- 七条旧原子 SOP 的命令细节; +- 人名先查 ID 的手工流程; +- 群名先查会话 ID 的手工流程; +- 文件先上传再提取 mediaId 的旧默认流程; +- 聊天记录强制 `--time`; +- Webhook 默认原子发送路线; +- 完整 Shortcut 表; +- 完整 flags、互斥和 required 说明; +- 卡片 payload、复杂转发、Bot 管理和事件运维细节。 + +### 4.3 shared 冷启动解耦 + +正常 IM 请求不再强制完整读取 `dws-shared/SKILL.md`。但不能简单删除 `MUST read` 而丢失全局安全契约。 + +冻结的交付方式为: + +1. 新建一份单一来源的最小公共契约,例如: + `skills/multi/dws-shared/references/runtime-contract.md`。 +2. 由生成器把该区块同步到 `dingtalk-chat/SKILL.md` 的受控 marker 中。 +3. `dws-shared/SKILL.md` 继续作为泛称 DWS、跨产品、认证/profile/错误路由入口,但不再是清晰 IM 任务的强制文本依赖。 +4. 策略门禁校验源文件与内嵌区块一致,禁止人工漂移。 + +最小契约只包括: + +- 只通过 `dws` 操作; +- 结构化读取使用 JSON; +- 不猜命令、flag、ID 或字段; +- 后续 ID 来自真实返回; +- 不输出凭据; +- 高影响操作遵循 Runtime confirmation; +- 写后按任务契约验证; +- 未知命令只查精确 leaf Help。 + +### 4.4 shared 按需入口 + +仅在以下条件加载 shared 或其 reference: + +| 条件 | 内容 | +|---|---| +| 登录态或权限失败 | `global-reference.md` / 对应错误章节 | +| profile 或组织不明确 | `dingtalk-profile` 与 shared profile 规则 | +| confirmation 错误 | Runtime confirmation 处理协议 | +| 未知 CLI/业务错误 | `error-codes.md` 对应章节 | +| 跨产品流程 | `workflow-routing.md` | +| 泛称 DWS、产品不明 | `routing.md` | + +### 4.5 Task reference 策略 + +根 Skill 不加载下列低频内容: + +- 流式卡片生命周期; +- 合并转发、话题转发和复杂引用; +- Bot 搜索、入群、管理员和撤回; +- Favorite/Pin/Top 的完整原子命令; +- 事件 ready、cooldown、terminal hold、status/stop 运维; +- 兼容媒体链路和异常恢复。 + +只有现有 reference 超过单任务预算且无法精确定位时才继续拆文件,避免为了“细粒度”制造大量无路由价值的小文档。 + +--- + +## 5. 统一 Resolver 契约 + +### 5.1 目标 + +所有自然目标解析必须复用同一套内部组件: + +```text +ResolveUser(profile, query, requiredIdentity) +ResolveChat(profile, query) +ResolveBot(profile, query) +ResolveUsers(profile, queries, requiredIdentity) +``` + +Resolver 只做确定性解析,不执行最终写操作。 + +### 5.2 类型化结果 + +建议内部结构: + +```json +{ + "status": "resolved", + "entityType": "user", + "query": "张三", + "matchType": "exact", + "selected": { + "id": "userId-or-openDingTalkId", + "userId": "...", + "openDingTalkId": "...", + "name": "张三", + "organization": "..." + }, + "candidates": [], + "profile": "corpId:userId" +} +``` + +状态固定为: + +- `resolved` +- `ambiguous` +- `not_found` +- `forbidden` +- `upstream_failure` + +### 5.3 匹配规则 + +1. 去除首尾空白,按产品允许的大小写规则比较。 +2. 按稳定 ID 去重,禁止同一对象的重复行制造假歧义。 +3. 用户解析按稳定 ID 去重后必须只有一个可用候选;即使其中一个显示名 exact,也不能隐藏同名或外部联系人候选。 +4. 群聊解析仅有一个 exact match 时优先 exact;多个 exact match 仍为 `ambiguous`。 +5. 群聊没有 exact 时,只有一个可用候选才允许 resolved。 +6. 多候选禁止默认第一项、最近使用项或第一组织项。 +7. 下游需要 `openDingTalkId` 时,不得选择只有 userId 且无法转换的候选;反之亦然。 +8. 已提供稳定 ID 时直接使用,不触发名称搜索。 + +### 5.4 结构化错误 + +零命中和多命中应返回非零退出码和机器可读细节: + +```json +{ + "ok": false, + "error": { + "type": "resolution", + "subtype": "ambiguous", + "entityType": "chat", + "query": "项目群", + "candidates": [ + { + "name": "项目群", + "openConversationId": "..." + } + ] + } +} +``` + +禁止只把候选拼进不可解析的错误字符串。面向人的 `message` 可以保留,但不是唯一数据面。 + +### 5.5 Profile 与组织一致性 + +- 解析和最终执行必须使用同一个 `--profile`。 +- Resolver 输出记录 profile identity,执行器在写入前校验一致。 +- 不缓存或跨调用复用其它 profile 下的 userId/openDingTalkId/openConversationId。 +- 多组织名称搜索遵循现有 profile 选择规则;没有明确默认账号时先停止并要求用户选择。 + +### 5.6 批量解析 + +`ResolveUsers` 采用全量预检: + +```text +解析所有查询 +→ 收集 resolved / ambiguous / not_found +→ 任意一项未 resolved,则整体停止 +→ 全部 resolved 后才进入写操作 +``` + +创建群等不可接受半完成的操作禁止“成功一个先写一个”。 + +### 5.7 Dry-run 一致性 + +`--dry-run` 必须: + +1. 运行与真实执行相同的只读 Resolver; +2. 运行相同的 target/content/identity 校验; +3. 输出最终 resolved target 和将调用的下层动作; +4. 在第一个写调用前停止; +5. 不因 dry-run 使用另一套模糊匹配逻辑。 + +现有 `CallMCPData` 的 dry-run 只读通道继续作为基础;新增 resolver 所使用的下层工具必须进入明确的只读分类,不能用名称猜测绕过写保护。 + +--- + +## 6. 分层发送路由与统一执行内核 + +### 6.1 自然目标参数 + +扩展 `chat +messages-send`: + +```bash +--user-query +--chat-query +``` + +示例: + +```bash +dws chat +messages-send \ + --as user \ + --user-query "张三" \ + --text "请查收" + +dws chat +messages-send \ + --as user \ + --chat-query "项目群" \ + --file ./report.pdf \ + --idempotency-key +``` + +目标严格且只能选择一个: + +```text +--chat-id / --group +--chat-query +--user +--user-query +--open-dingtalk-id +``` + +Bot 和 Webhook 不自动继承 user 的自然目标或文件能力;是否支持由现有身份矩阵和真实下层能力决定。 + +这些自然目标参数服务于“自然目标 + 文件/复杂 @/幂等等高级发送”场景,不把 `+messages-send` 提升为所有发送意图的唯一公开入口。简单姓名文本继续走 `+dm`,简单群名文本继续走 `+send-to-group`。 + +### 6.2 统一内部发送器 + +将当前发送路径收敛为一个内部执行器: + +```text +resolve target(如需要) +→ validate identity/content/target +→ normalize @ placeholders +→ resolve/upload file(如需要) +→ attach idempotency / AI tag +→ dispatch lower transport +→ normalize delivery result +``` + +`+dm` 和 `+send-to-group` 保留稳定 CLI path,但不再各自维护发送实现: + +```text ++dm +→ ResolveUser +→ unified send executor + ++send-to-group +→ ResolveChat +→ unified send executor +``` + +这里的“统一”指 transport 前的校验、内容构造、幂等、AI tag、执行和结果归一化,不要求三个公开命令接受完全相同的 flags。 + +### 6.3 内容与身份边界 + +- user:文本、Markdown、已有 mediaId 图片、安全相对路径文件/音频/视频; +- bot:仅发布真实下层支持的文本/Markdown; +- webhook:目标由 token 所属群决定,只发布真实支持的文本/Markdown; +- 不把 user 文件能力伪装为 bot/webhook 等价能力; +- 不在 Skill 复制完整矩阵,由 leaf Schema/Runtime 交付。 + +### 6.4 幂等与重试 + +- 已提供 idempotency key 时原样透传; +- Agent 或 facade 重试必须复用原 key; +- 未提供时不得宣称具备跨进程幂等; +- 解析成功但发送结果未知时,返回明确 delivery/unknown 状态,不自动换目标重发; +- 不因错误 envelope 误判成功而重复发送。 + +### 6.5 Dry-run 输出 + +```json +{ + "identity": "user", + "resolvedTarget": { + "type": "chat", + "name": "项目群", + "openConversationId": "..." + }, + "contentType": "file", + "file": "./report.pdf", + "idempotencyKey": "...", + "willSend": false +} +``` + +### 6.6 兼容策略 + +- 保留 `+dm`、`+send-to-group`、旧 ID flags 和原子命令; +- 不为统一发送新建第二个 canonical Shortcut; +- 新增 flags 属于现有 Cobra leaf 的兼容扩展,不修改稳定 command identity; +- 只有主路径或 alias 真正调整时才评审 CommandRegistry; +- Skill 始终按最小充分意图选路;PR 2 完成后也不把简单文本意图统一改为 `+messages-send`。 + +--- + +## 7. 读取、搜索与资源闭环 + +读取侧不新增一个万能 `+messages-read`。`+chat-messages`、`+search-msg` 和 `+messages-mget` 的任务语义不同,继续作为三个公开入口;它们只在内部共享自然目标 Resolver、分页基元、`MessageViewV1` 投影、资源处理和完整性契约。 + +### 7.1 扩展 `+chat-messages` + +新增: + +```bash +--chat-query +--user-query +``` + +示例: + +```bash +dws chat +chat-messages \ + --chat-query "项目群" \ + --download-resources +``` + +内部路径: + +```text +解析 chat/user +→ 获取消息 +→ 投影 sender/text/time/identity +→ 保留 reaction/引用/转发/resourceRefs +→ 计算分页完整性 +→ 可选安全下载 +→ 输出稳定 DTO +``` + +省略 `--time` 继续默认以当前时间为边界向前读取;Skill 不再强制 Agent 构造时间。 + +### 7.2 扩展 `+search-msg` + +新增: + +```bash +--sender-query +--chat-query +``` + +其中 `--sender-query` 应支持重复或列表输入,先解析全部发送者,再执行一次搜索;任何名称歧义都在搜索前停止。 + +继续保留: + +- 时间范围和默认近 7 天; +- 多会话、多发送人; +- @我/@对象; +- 消息类型和机器人来源; +- `--page-all` 与页数上限; +- mget 批量富化; +- `--no-enrich`、`--no-reactions`; +- partial failure ledger; +- `--download-resources`。 + +### 7.3 Message DTO 兼容策略 + +目标是让 list/search/mget/thread reply 共享一个内部 `MessageViewV1` 构造器,但不得无版本破坏现有公开输出。 + +第一阶段保留已有字段类型和顶层名称: + +```json +{ + "contractVersion": "im.message-list.v1", + "messages": [ + { + "messageId": "...", + "conversationId": "...", + "threadId": "...", + "sender": "张三", + "senderId": "...", + "senderType": "user", + "messageType": "text", + "text": "...", + "createTime": "...", + "updateTime": "...", + "reactions": {}, + "quotedMessage": {}, + "forwarded": [], + "resourceRefs": [] + } + ] +} +``` + +兼容规则: + +1. `sender` 暂时保持现有标量类型,不直接替换为对象。 +2. 新的 `senderId`、`senderType` 采用增量字段。 +3. `resourceRefs` 保留现有名称;如需新 `resources` 结构,必须增量发布或引入显式 DTO 版本。 +4. 顶层 `messages` 不无版本改名为 `items`。 +5. 原字段删除或类型变化必须走独立兼容性评审。 + +如果未来确需嵌套 sender/resources 对象,使用显式 `v2` projection 或新主版本,而不是静默切换。 + +### 7.4 完整性字段 + +所有分页读取和搜索逐步统一为: + +```json +{ + "messages": [], + "complete": true, + "hasMore": false, + "nextCursor": "", + "pagesFetched": 1, + "enrichedCount": 0, + "failedCount": 0, + "failures": [] +} +``` + +规则: + +- `hasMore=true` 且未继续翻页时,`complete=false`; +- 下层缺失可靠分页字段时,不能证明完整,`complete=false`; +- 达到 page limit 且仍有后续页,`complete=false`; +- mget 富化部分失败,保留已有消息并记录 failures; +- 单资源下载失败不丢弃消息; +- 不捕获错误后动态降级为虚假的完整成功。 + +### 7.5 资源下载契约 + +- 路径必须是工作目录内安全相对路径; +- 默认不覆盖; +- 显式 `--overwrite` 才允许覆盖; +- 临时文件完成后原子 rename; +- 按稳定资源 key 去重; +- 单资源失败隔离; +- 返回每项 `messageId`、resource key/type、local path、size/error; +- 下载保持 `read/not_required`,不得制造无意义的 `--yes`; +- 资源引用中的子消息 ID 优先使用子消息自己的 `messageId`。 + +--- + +## 8. 建群与回复闭环 + +### 8.1 扩展现有 `+chat-create` + +`+chat-create` 已存在,本阶段增加自然成员解析,而不是创建同义命令。 + +目标示例: + +```bash +dws chat +chat-create \ + --name "项目群" \ + --member-query "张三,李四" \ + --type INTERNAL +``` + +保留现有: + +- `--users`; +- `--type INTERNAL|EXTERNAL|NORMAL`; +- `--thread`; +- 当前用户自动加入并设为群主; +- `openCid` 规范化为 `openConversationId`。 + +新增: + +- `--member-query`; +- 批量解析预检; +- dry-run 成员清单与最终下层参数; +- 重复 ID 去重; +- 结构化 ambiguity/not-found 错误。 + +以下能力只有下层真实支持并完成 Schema/Runtime 评审后才能加入: + +- 群 description; +- 初始机器人; +- 指定其他 owner; +- public/private 或其它 Lark 专属语义; +- 服务端幂等键。 + +不得通过群名预搜索把“禁止同名群”伪装成幂等,因为业务上可能合法存在同名群。 + +### 8.2 扩展现有 `+messages-reply` + +当前入口已能根据原消息补全发送者;本阶段目标是补齐后续可执行上下文: + +```bash +dws chat +messages-reply \ + --message-id \ + --conversation-id \ + --text "收到" +``` + +期望结果包含: + +- 新消息 ID(下层可提供时); +- conversation ID; +- thread ID(适用时); +- delivery 状态; +- idempotency key; +- 原消息解析结果或来源。 + +`--reply-in-thread` 只有在确认真实 Thread writer 下层能力后才发布。普通引用回复和 Thread 回复必须是两个明确模式,不能用同一个 flag 猜测下层行为。 + +### 8.3 写操作安全 + +- 目标解析、原消息读取和参数校验全部先于写调用; +- 多候选或缺失原消息时不产生副作用; +- confirmation 由最终 Runtime gate 与 Schema 决定,不根据 `risk` 文案机械推导; +- dry-run 必须在写调用前停止; +- 部分成功时返回已创建对象和恢复上下文,禁止统一报成“完全失败”。 + +--- + +## 9. IM 事件高阶入口 + +### 9.1 定位 + +新增一个高层 facade: + +```bash +dws event +listen-im --kind at-me +dws event +listen-im --kind sender --user-query "张三" +dws event +listen-im --kind group --chat-query "项目群" +dws event +listen-im \ + --events message,reaction,recall \ + --chat-query "项目群" +``` + +它不是新事件系统,而是现有 `event consume` 的确定性编译层。 + +### 9.2 建议参数 + +```text +--kind at-me|sender|group|all-direct|all-group +--events message,reaction,read,recall +--user / --open-dingtalk-id / --user-query +--chat-id / --chat-query +--max-events +--timeout +--profile +``` + +`kind`、target 和 events 的组合由 Runtime 校验;不兼容组合在创建订阅前失败。 + +### 9.3 确定性映射 + +示例: + +| 用户语义 | EventKey | +|---|---| +| @我消息 | `user_im_message_receive_at` | +| 指定人的单聊消息 | `user_im_message_receive_o2o` | +| 指定人发给我的所有消息 | `user_im_message_receive_user` | +| 指定群消息 | `user_im_message_receive_group` | +| 指定群 reaction | `user_im_message_reaction_group` | +| 指定群撤回 | `user_im_message_recall_group` | +| 指定单聊已读 | `user_im_message_read_o2o` | + +映射表由代码和测试维护,根 Skill 只保留高频用户语义,不展开全部 EventKey。 + +### 9.4 执行与生命周期 + +内部必须复用现有 event 控制面: + +- 多个兼容事件合并为一次命令、一个消费进程; +- 自动启用 `--flatten -f ndjson`; +- stdout 只输出事件 NDJSON; +- stderr 输出 subscription/ready/status/error; +- 等待真实 ready marker,不用 `sleep` 猜测; +- 部分订阅创建失败时回滚已创建订阅; +- stdin、SIGINT、SIGTERM 和 timeout 触发有序清理; +- 尊重 `in_flight`、`cooldown`、`terminal_hold`; +- `--profile` 在目标解析、订阅、status 和 stop 全程一致; +- bounded 结束不泄漏订阅。 + +由于这是持续输出命令,不应简单套用只在结束时 `rt.Output` 一次的普通 Shortcut 模板。优先设计为复用 `event consume` 内部 runner 的 composite facade,避免通过 shell 启动第二个 `dws`。 + +### 9.5 `dingtalk-event` Skill 精简 + +根 Skill只保留: + +- 监听对象选择; +- `+listen-im` 高频示例; +- 有界/无界监听; +- 何时进入运维 reference; +- chat 写操作与 event 监听的边界。 + +全部 EventKey 表、ready 细节、重试状态和 status/stop 运维迁入按需 reference。 + +--- + +## 10. 分阶段实施计划 + +### PR 1:Skill 路由收敛与 shared 解耦 + +#### 范围 + +只修改 Skill、reference、生成器和策略门禁;不改变 CLI 行为、公开参数或业务接口。 + +#### 工作项 + +1. 将 `dingtalk-chat` 重写为 Golden Route。 +2. 删除旧原子 SOP 和旧文件上传路线。 +3. 修正 `--time`、Webhook、资源下载说明。 +4. 移除正常 IM 对完整 `dws-shared` 的强制读取。 +5. 建立最小公共契约单一来源和受控注入区块。 +6. 将卡片、复杂转发、Bot 管理、事件运维迁入精确 reference。 +7. 增加禁止旧路线回流的静态策略。 + +#### 验收 + +- 已知 ID 发送:读产品 Skill → 执行一个业务命令; +- 姓名发送直接选择 `+dm`; +- 群名发送直接选择 `+send-to-group`; +- 最近聊天记录不强制 `--time`; +- Webhook 默认选择 `+messages-send --as webhook`; +- 文件不再推荐独立 mediaId 上传链; +- 同一意图只有一个主入口; +- 根 Skill ≤2.5K Tokens; +- 正常 IM 强制 shared 为 0; +- 所有示例通过真实 Cobra 路径和 flag 校验。 + +#### 回滚边界 + +PR 仅改变 Agent 路由文本,可独立回滚,不影响 CLI 兼容性。 + +### PR 2:统一 Resolver 与发送执行内核 + +#### 范围 + +- 内部 typed resolver; +- 结构化 resolution error; +- `+messages-send --user-query/--chat-query`; +- `+dm`、`+send-to-group` 委托统一发送内核; +- dry-run parity; +- Schema/Help/selection metadata 同步。 + +#### 验收 + +- 唯一姓名发送只执行一个用户可见业务命令; +- 群名发送只执行一个用户可见业务命令; +- 群名发送文件可由 `+messages-send --chat-query --file` 一步完成; +- 多候选副作用为 0,并返回结构化 candidates; +- 已知 ID 不触发 resolver; +- dry-run 与真实执行选择同一目标; +- `+dm`、`+send-to-group` 经统一发送内核产生等价的下层发送结果; +- AI tag、幂等和内容验证不因入口不同而漂移。 +- 简单姓名/群名文本仍分别选择 `+dm`、`+send-to-group`,不会因为内部统一而暴露完整身份参数矩阵。 + +#### 兼容性 + +只增加可选 flags 和内部复用;保留已有命令与参数。 + +### PR 3:读取、搜索、DTO 与资源闭环 + +#### 范围 + +- `+chat-messages --chat-query/--user-query`; +- `+search-msg --sender-query/--chat-query`; +- 公共 `MessageViewV1` 投影; +- 完整性字段; +- 资源下载 ledger 统一; +- 兼容性 fixture。 + +#### 验收 + +- 群名读取并下载资源只需要一个业务命令; +- 跨群搜索某人消息只需要一个业务命令; +- 单资源失败不影响其它消息; +- 翻页和富化结束后能证明 `complete=true`; +- list/search/mget/thread 的核心字段一致; +- Agent 不再手工 join sender/chat/resource; +- 现有公开 DTO 字段类型和顶层名称保持兼容。 + +### PR 4:扩展建群与回复闭环 + +#### 范围 + +- 扩展现有 `+chat-create --member-query`; +- 批量解析全量预检; +- dry-run 成员计划; +- 扩展 `+messages-reply` 输出上下文; +- Thread writer 若下层仍缺失则只保留需求,不虚假发布。 + +#### 验收 + +- 使用姓名创建群不需 Agent 手工查 ID; +- 任一成员歧义时不创建群; +- 当前用户仍在成员列表并作为群主; +- 回复结果可继续用于查询、撤回和资源操作; +- 普通引用回复与 Thread 回复不混淆; +- 不承诺下层不存在的 description/bot/visibility 能力。 + +### PR 5:IM 事件高阶入口 + +#### 范围 + +- `event +listen-im`; +- kind/events 到 EventKey 的映射; +- user/chat natural resolution; +- 多事件合并; +- ready、bounded、取消、回滚和清理; +- `dingtalk-event` 根 Skill 精简。 + +#### 验收 + +- 已知监听意图不调用 `event list`; +- 不需要字段推断时不调用 `event schema`; +- 多兼容事件使用一个命令和一个进程; +- 等待 ready marker,不使用 sleep; +- 正常、超时、取消和部分失败均不泄漏订阅; +- 控制模型轮次 ≤4; +- stdout/stderr 契约保持稳定。 + +--- + +## 11. 后续依赖下层的能力(明确非本轮遗留) + +以下项目依赖尚未存在或未确认的下层能力,不阻塞前五个 PR,也不属于本轮完成定义: + +| 优先级 | 能力 | 当前处理 | +|---|---|---| +| P1 | Thread 回复 writer | 未确认前不发布 `--reply-in-thread` | +| P1 | 异步 delivery status / `--wait` | 先保留 `openTaskId` 和明确未知状态 | +| P1 | 图片、文件、音视频统一资源元数据 | PR 3 先规范当前可识别资源 | +| P2 | Bot 富媒体发送 | 不把 user 能力映射给 bot | +| P2 | 大文件分片、Range、断点续传 | 保持当前安全下载边界 | +| P2 | 卡片按钮回调与完整生命周期 | 独立 card/event reference | +| P3 | Moderation、owner/manager/visibility | 仅发布真实 DWS 下层子集 | +| P3 | 事件触发发送幂等关联 | 待 delivery/event correlation 下层支持 | + +--- + +## 12. 评测设计 + +### 12.1 固定回归场景 + +1. 已知 ID 发文本; +2. 唯一姓名发文本; +3. 多人同名发消息; +4. 群名发文件; +5. 跨群搜索某人最近消息; +6. 群名读取历史并下载资源; +7. 使用姓名创建群; +8. 监听某群消息和 reaction。 + +### 12.2 每场景记录 + +```text +task_success +routing_correct +first_pass_success +model_calls +skill_files_loaded +reference_files_loaded +schema_calls +help_calls +business_cli_calls +internal_read_calls +internal_write_calls +resident_control_tokens +cumulative_input_tokens +ambiguous_target_behavior +side_effect_count +complete +false_success +correction_rounds +``` + +### 12.3 指标定义 + +- `model_calls`:初始决策及每次工具结果后的模型继续调用; +- `business_cli_calls`:Agent 显式执行的用户可见 `dws` 业务命令;Shortcut 内部 MCP 调用不另算模型轮次; +- `resident_control_tokens`:本任务进入上下文的 Skill、shared、reference、Schema 和 Help 去重总量; +- `cumulative_input_tokens`:每轮模型输入中固定基础上下文与已驻留控制文本的累计暴露; +- `first_pass_success`:首次选定入口和参数后,无需改走其它命令或补查泛化 Schema/Help 即完成任务; +- `correction_rounds`:因路由、参数、身份或结果解释不足产生的额外模型继续轮次; +- `false_success`:结果不完整、写入未知或部分失败却被报告为完整成功; +- `side_effect_count`:测试期望之外的创建、发送、更新、订阅或删除次数。 + +Token 价值通过场景证据评审,而不是仅计算“越少越好”:根 Skill 每个区块必须标注它支持的评测场景或阻止的失败;删除区块后若 Schema/Help fallback、纠错轮次或失败率上升,即使冷启动 Token 下降也不能合入。 + +### 12.4 分阶段预期路径 + +| 场景 | PR 1 后 | 最终目标 | +|---|---|---| +| 已知 ID 发文本 | `+messages-send` | 1 个业务命令,3 轮 | +| 唯一姓名发文本 | `+dm` | 1 个业务命令,3~4 轮 | +| 多人同名发送 | `+dm` 无写入失败 | 结构化 candidates,副作用 0 | +| 群名发文件 | 仍可能需 ID | `+messages-send --chat-query --file` 一步 | +| 跨群搜某人 | 仍可能需先解析 | `+search-msg --sender-query` 一步 | +| 群名读并下载 | 仍可能需先解析 | `+chat-messages --chat-query --download-resources` 一步 | +| 姓名建群 | 仍需 IDs | `+chat-create --member-query` 一步 | +| 群事件监听 | 切 `dingtalk-event` | `event +listen-im --chat-query` 一步 | + +### 12.5 硬门禁 + +- 任务成功率、首次路由正确率和完整结果率不得低于基线; +- 冷启动 Token 下降但 Schema/Help fallback 或纠错轮次上升,不算通过; +- 高频已知意图 `schema_calls=0`; +- 高频已知意图 `help_calls=0`; +- 唯一姓名/群名最终只执行一个业务 CLI 命令; +- 多候选写操作 `side_effect_count=0`; +- 不允许完整 Shortcut Catalog 进入上下文; +- 不允许根 Skill 恢复旧原子 SOP; +- 不允许正常 Chat 强制读取完整 shared; +- 不允许 partial result 标记 `complete=true`; +- 不允许生成文件被手工编辑。 + +--- + +## 13. 测试与 CI 门禁 + +### 13.1 Skill 静态门禁 + +扩展 `check-skill-context-budget.sh` 或增加专用检查: + +1. `dingtalk-chat` 字节与 Token 预算; +2. Golden Route 精确命令集合; +3. 同一意图不得出现两个“优先/必须”入口; +4. 禁止旧模式文本回流,例如: + - 发消息必须先 `aisearch person`; + - 文件必须先 `dt_media_upload`; + - `+chat-messages` 必须传 `--time`; + - Webhook 默认走原子命令; +5. 禁止 Chat 完整 Shortcut 表重新展开; +6. 最小公共契约生成区块必须与源一致; +7. 所有示例通过真实 Cobra path/flag/constraint 校验。 + +### 13.2 Resolver 单元测试 + +至少覆盖: + +- 零命中; +- 单一模糊候选; +- 单一 exact; +- 多个 exact; +- 重复 ID 去重; +- 外部联系人只有 openDingTalkId; +- 下游要求 userId/openDingTalkId 的差异; +- 群 exact 优先和同名群歧义; +- profile 不一致; +- upstream error; +- dry-run 与真实解析相同; +- 已知 ID 不调用搜索。 + +### 13.3 Shortcut 行为测试 + +- `+dm`、`+send-to-group`、`+messages-send` 使用同一发送器; +- user/bot/webhook 目标矩阵; +- 文件路径与内容类型; +- idempotency 和 AI tag; +- 解析失败前无写调用; +- send/read/search/create 的 dry-run 计划; +- Message DTO fixture 和向后兼容; +- 分页、富化、资源 partial failure; +- 群创建全量预检; +- 回复结果的后续上下文。 + +### 13.4 Event 测试 + +- kind/events/target 映射; +- 多事件去重和顺序; +- ready marker; +- 部分订阅失败回滚; +- timeout/SIGINT/SIGTERM 清理; +- dry-run 不创建订阅; +- profile 一致; +- stdout 只包含 NDJSON; +- bounded 结束无泄漏。 + +### 13.5 Schema 与生成门禁 + +每个改变 Cobra flags、Runtime Schema 或 Agent selection 的 PR 必须: + +```bash +go generate ./internal/cli +./scripts/policy/check-generated-drift.sh +./scripts/policy/check-schema-catalog.sh +./scripts/policy/check-runtime-confirmation-truth.sh +make skill-command-integrity skill-context-budget +DWS_PACKAGE_VERSION=0.0.0-test go test ./... +``` + +并重点运行: + +```bash +go test ./internal/shortcut/... -count=1 +go test ./internal/app -run 'Test(Chat|Event|Schema|ManualAgentExample)' -count=1 +make test-schema-agent-examples +``` + +示例中不得存储 `--yes`;所有公开示例必须通过真实 `BoundCommand` 的路径、flag、required 和跨参数约束。 + +### 13.6 Live 验证 + +在具备隔离测试账号和明确授权时运行: + +- 唯一/歧义姓名; +- 同名群; +- user 文本与文件; +- bot/webhook 文本; +- 消息分页和下载; +- 事件多订阅和清理。 + +Live 报告必须脱敏,不保存 token、webhook token、原始私人消息或下载内容。 + +--- + +## 14. 生成与单一来源 + +### 14.1 Command identity + +- 已有 `+dm`、`+send-to-group`、`+messages-send`、`+chat-messages`、`+search-msg`、`+chat-create`、`+messages-reply` 保持 identity 和主路径; +- 新增可选 flag 不机械改写 CommandRegistry; +- `event +listen-im` 是新公开 leaf,需要评审 registry identity、主路径、selection 和 metadata; +- alias 只有存在真实兼容需求时才加入。 + +### 14.2 参数与安全来源 + +- Cobra tree:命令存在性与接受的 flags; +- Runtime constraints:互斥、至少一个、条件组合; +- metadata:接口、安全、runtime gate 和必要参数 overlay; +- selection:Agent 选择语义; +- Skill:Golden Route,不重复参数真相。 + +### 14.3 输出 DTO 来源 + +消息投影应有单一内部 builder 和共享 fixture。list/search/mget/thread 不各自维护字段别名、reaction、quoted、forwarded 和 resource 解析逻辑。 + +### 14.4 生成文件 + +只能修改源输入和生成器,再运行生成: + +- 不手改 `schema_catalog/`; +- 不手改 `schema_agent_metadata/`; +- 不以旧 Catalog 为生成输入; +- 生成前后必须通过 byte guard 和 drift check。 + +--- + +## 15. 风险与缓解 + +| 风险 | 表现 | 缓解 | +|---|---|---| +| Skill 过度精简 | 低频能力找不到 | 精确 reference + 最后回退发现,不恢复全量 Catalog | +| 移除 shared 后安全丢失 | 产品 Skill 缺全局底线 | 单一来源最小契约生成注入 + hash/marker 门禁 | +| Resolver 误选人/群 | 发错对象 | exact/unique 规则、结构化 candidates、写前停止 | +| 批量解析半完成 | 建出成员不完整的群 | 全量预检,全成功后单次写入 | +| dry-run 与真实执行漂移 | 预览目标不同 | 同一 resolver、同一校验、只在写调用前分叉 | +| DTO 统一导致兼容破坏 | 脚本解析失败 | 共享内部模型、增量字段、显式版本、禁止改字段类型 | +| facade 继续膨胀 | 又出现大量同义入口 | 只扩展五个主入口,保留兼容 wrapper,不再横向新增 | +| 虚假对齐 Lark | 广告下层不支持能力 | `availability/interface_reason` 如实发布,能力进入后续清单 | +| 事件订阅泄漏 | 进程结束后仍订阅 | ready/rollback/cancel/timeout/cleanup 全路径测试 | +| 跨 profile ID 串用 | 错组织操作 | Resolver 结果携带 profile,执行前一致性校验 | +| 输出富化过度 | 延迟和业务数据 Token 增长 | 保留 `--no-enrich`、`--no-reactions`、下载 opt-in | +| 安全从 Skill 下沉后漂移 | confirmation 不一致 | Runtime gate truth + final embedded Schema 语义测试 | + +--- + +## 16. 冻结决策与待确认项 + +### 16.1 已冻结 + +1. PR 顺序为 Skill → Resolver/Send → Read/Search → Create/Reply → Event。 +2. 正常 IM 不再完整读取 shared。 +3. 最小公共契约采用单一来源生成注入,不人工复制。 +4. `+dm`、`+send-to-group` 保留兼容,但内部复用统一发送器。 +5. 自然目标 canonical flag 使用 `--user-query`、`--chat-query`、`--sender-query`、`--member-query`。 +6. 多候选永不默认第一项。 +7. dry-run 运行真实只读解析。 +8. `+chat-create`、`+messages-reply` 是扩展项,不重复新增。 +9. 消息 DTO 只做兼容性增量;嵌套对象需要显式新版本。 +10. `event +listen-im` 复用现有事件控制面,不另建订阅系统。 +11. Token 预算是防膨胀护栏,北极星是任务价值、正确性、安全和完整性。 +12. Golden Route 保留多个最小充分意图入口;发送和读取只在内部执行内核与数据契约层统一。 +13. 简单姓名文本走 `+dm`,简单群名文本走 `+send-to-group`;高级发送才选择 `+messages-send`。 + +### 16.2 实施前仍需确认 + +| 项目 | 需要确认的事实 | +|---|---| +| Thread writer | 下层是否能稳定返回新消息 ID、thread ID 和 delivery 状态 | +| 群创建幂等 | 下层是否支持服务端幂等键;否则不宣称幂等 | +| 群 description/初始 Bot | DWS 下层是否真实支持,不照搬 Lark 参数 | +| sender DTO v2 | 是否有消费者需要嵌套对象,是否值得显式版本 | +| event bounded 模式 | `--max-events`、`--timeout` 的退出码和 cleanup 语义 | +| Resolution envelope | 是否扩展通用 `apperrors`,供其它产品复用 | +| Token CI | 使用固定 tokenizer 依赖还是字节硬门禁 + Token 报告双轨 | + +未确认项不得阻塞 PR 1,也不得在 Skill 中提前广告。 + +--- + +## 17. PR 评审清单 + +### 路由 + +- [ ] 高频意图只有一个主入口; +- [ ] 已知意图不需要产品 Schema、Catalog 或 Help; +- [ ] 原子命令只作为明确降级; +- [ ] Skill 没有广告当前下层不可用能力。 + +### Resolver + +- [ ] 已知 ID 跳过解析; +- [ ] exact、unique、ambiguous、not_found 行为有测试; +- [ ] candidates 结构化且不泄露敏感信息; +- [ ] profile 全链路一致; +- [ ] dry-run 与真实执行同解析结果。 + +### 安全 + +- [ ] 多候选写操作副作用为 0; +- [ ] confirmation 与最终 Runtime gate/Schema 一致; +- [ ] 文件路径、覆盖和原子落盘规则未放宽; +- [ ] partial success 不被报告为完整成功; +- [ ] 事件异常退出完成清理。 + +### 兼容性 + +- [ ] 未删除旧 CLI path/flag; +- [ ] 未机械改写 CommandRegistry; +- [ ] 未改变现有 DTO 字段类型; +- [ ] 新字段为增量或显式版本; +- [ ] alias 有真实兼容理由。 + +### 生成与验证 + +- [ ] 修改了正确的 authored source; +- [ ] 未手改 Catalog/Agent metadata; +- [ ] generation、drift、Schema policy 通过; +- [ ] Skill 命令完整性和 context budget 通过; +- [ ] 单元、dry-run、fixture、事件清理测试通过; +- [ ] 评测表记录了模型轮次和控制 Token 变化。 + +--- + +## 18. 最终验收标准 + +全部五个 PR 完成后,必须同时达到: + +1. 八个固定高频场景的任务成功率、首次路由正确率和完整结果率不低于基线。 +2. 多候选写操作副作用和 false success 为 0。 +3. 每个根 Skill 区块都能映射到路由、安全、结果判断或错误恢复证据。 +4. 八个固定高频场景平均模型轮次 ≤3.7。 +5. 平均累计输入 ≤90K。 +6. 平均常驻上下文 23~24.5K。 +7. 高频已知意图 Schema/Help 调用为 0。 +8. 自然姓名/群名场景只执行一个用户可见业务命令。 +9. 群名读取并下载可一命令完成。 +10. 姓名建群可一命令完成,任一歧义时不创建。 +11. 多事件监听使用一个 facade 和一个消费进程。 +12. 读取/搜索 partial failure 不产生 false success。 +13. 消息 DTO 保持兼容并共享同一内部投影实现。 +14. Safety、confirmation、Schema、Help 和真实 Runtime 无漂移。 + +如果效率指标达到但任务成功率、安全或完整性退化,则该阶段不算通过。 + +--- + +## 附录 A:目标根 Skill 骨架 + +```markdown +# 钉钉群聊 / 消息 Skill + +## 最小执行契约 + + + +## Golden Route + +| 用户意图 | 唯一入口 | +|---|---| +| 按姓名发文本 | `dws chat +dm ...` | +| 按群名发文本 | `dws chat +send-to-group ...` | +| 已知目标/文件/Bot/Webhook | `dws chat +messages-send ...` | +| 读指定会话 | `dws chat +chat-messages ...` | +| 跨会话搜索 | `dws chat +search-msg ...` | +| 读并下载资源 | 查询命令加 `--download-resources` | +| 查看置顶会话 | `dws chat +conversation-list-top` | +| 监听事件 | 切 `dingtalk-event` | + +## 关键结果语义 + +- openTaskId 不是消息 ID。 +- reaction/updateTime/resourceRefs 默认随稳定投影返回。 +- Favorite/Pin/Message Top/Conversation Top 不可混用。 + +## 低频能力 + +- 卡片、复杂转发、Bot 管理、事件运维按需读取精确 reference。 +- 已知 leaf 直接执行;仅在参数/安全不确定时读取 leaf Schema。 + +## 错误分流 + +- resolution candidates:交给用户消歧,不猜。 +- auth/profile/confirmation:读取对应 shared reference。 +- unknown command/flag:精确 leaf Help,最多修正一次。 +``` + +## 附录 B:评测记录模板 + +```markdown +| 场景 | 成功 | 主入口 | 模型轮次 | Skill | Ref | Schema | Help | 业务 CLI | 常驻 Tokens | 累计输入 | 副作用 | complete | false success | +|---|---|---|---:|---:|---:|---:|---:|---:|---:|---:|---:|---|---| +| 已知 ID 发文本 | | | | | | | | | | | | | | +| 唯一姓名发文本 | | | | | | | | | | | | | | +| 多人同名发送 | | | | | | | | | | | | | | +| 群名发文件 | | | | | | | | | | | | | | +| 跨群搜某人 | | | | | | | | | | | | | | +| 群名读并下载 | | | | | | | | | | | | | | +| 姓名创建群 | | | | | | | | | | | | | | +| 群消息+reaction 监听 | | | | | | | | | | | | | | +``` + +## 附录 C:实施总路线 + +```text +先修 Skill,使 Agent 使用现有最小充分路由 +→ 再统一 Resolver,消除名称到 ID 的 Agent 手工步骤 +→ 再统一发送内核,以及读取/搜索共享的投影和完整性契约 +→ 扩展现有建群与回复入口 +→ 最后封装长连接事件入口 +→ 下层能力成熟后再补 Thread、Bot 富媒体和大文件能力 +``` + +最终判断标准不是 Shortcut 数量,而是:用户的高频 IM 意图是否能由一个稳定入口完成,Agent 是否不再承担本可由 CLI 确定性完成的工作。 diff --git a/internal/app/event_command.go b/internal/app/event_command.go index a6ac41f6..6672a6c8 100644 --- a/internal/app/event_command.go +++ b/internal/app/event_command.go @@ -83,6 +83,7 @@ func newEventCommand() *cobra.Command { RunE: func(c *cobra.Command, _ []string) error { return c.Help() }, } cmd.AddCommand( + newEventListenIMCommand(), newEventConsumeCommand(), newEventListCommand(), newEventSchemaCommand(), diff --git a/internal/app/event_listen_im.go b/internal/app/event_listen_im.go new file mode 100644 index 00000000..4ae6afe6 --- /dev/null +++ b/internal/app/event_listen_im.go @@ -0,0 +1,247 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package app + +import ( + "encoding/json" + "fmt" + "os" + "strings" + "time" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/personal" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver" + "github.com/spf13/cobra" +) + +type listenIMOptions struct { + Kind string + Events []string + UserID string + OpenDingTalkID string + UserQuery string + ChatID string + ChatQuery string + QueryCSV string + MaxEvents int + Duration time.Duration + DryRun bool + ControlBaseURL string + StreamTicketMode string + StreamTicketURL string + StreamSourceID string +} + +type listenIMPlan struct { + EventKeys []string + UserID string + OpenDingTalkID string + GroupID string + ResolvedTargets []any +} + +type eventTargetReader struct{} + +func (eventTargetReader) CallMCPData(product, tool string, params map[string]any) (map[string]any, error) { + text, err := helpers.CallMCPReadToolTextOnServer(product, tool, params) + if err != nil { + return nil, err + } + if strings.TrimSpace(text) == "" { + return map[string]any{}, nil + } + var data map[string]any + if err := json.Unmarshal([]byte(text), &data); err != nil { + return nil, apperrors.NewInternal(fmt.Sprintf("解析 %s 返回失败: %v", tool, err)) + } + return data, nil +} + +var eventListenIMReader = func() targetresolver.Reader { return eventTargetReader{} } + +func newEventListenIMCommand() *cobra.Command { + var opts listenIMOptions + cmd := &cobra.Command{ + Use: "+listen-im", + Short: "按 IM 意图解析目标并监听一个或多个个人消息事件", + Long: "把 @我、指定发送人、指定群、全部单聊或全部群聊等用户意图确定性编译为个人 EventKey," + + "自然姓名/群名会先唯一解析,再复用 event consume 的订阅、ready marker、NDJSON、取消、回滚和清理生命周期。", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(c *cobra.Command, _ []string) error { + plan, err := compileListenIMPlan(eventListenIMReader(), opts) + if err != nil { + return fmt.Errorf("event +listen-im: %w", err) + } + consumeOpts := personalConsumeOptions{ + EventKey: firstArg(plan.EventKeys), + EventKeys: plan.EventKeys, + Flatten: true, + UserID: plan.UserID, + OpenDingTalkID: plan.OpenDingTalkID, + GroupID: plan.GroupID, + QueryCSV: opts.QueryCSV, + ControlBaseURL: opts.ControlBaseURL, + StreamTicketMode: opts.StreamTicketMode, + StreamTicketURL: opts.StreamTicketURL, + StreamSourceID: opts.StreamSourceID, + Common: commonConsumeOptions{ + FormatRaw: "ndjson", + MaxEvents: opts.MaxEvents, + Duration: opts.Duration, + DryRun: opts.DryRun, + }, + } + return eventRunPersonalConsume(c, consumeOpts) + }, + } + f := cmd.Flags() + f.StringVar(&opts.Kind, "kind", "at-me", "监听意图: at-me|sender|group|all-direct|all-group") + f.StringSliceVar(&opts.Events, "events", []string{"message"}, "事件种类: message,reaction,read,recall") + f.StringVar(&opts.UserID, "user", "", "指定发送人/单聊对端 userId") + f.StringVar(&opts.OpenDingTalkID, "open-dingtalk-id", "", "指定发送人/单聊对端 openDingTalkId") + f.StringVar(&opts.UserQuery, "user-query", "", "按姓名/花名唯一解析指定发送人") + f.StringVar(&opts.ChatID, "chat-id", "", "指定群 openConversationId") + f.StringVar(&opts.ChatQuery, "chat-query", "", "按群名唯一解析指定群") + f.StringVar(&opts.QueryCSV, "query", "", "消息文本关键词过滤,逗号分隔;仅 message 事件") + f.IntVar(&opts.MaxEvents, "max-events", 0, "收到 N 条后退出 (0 = 不限)") + f.DurationVar(&opts.Duration, "duration", 0, "运行时长上限 (Go duration,如 30s/5m;0 = 不限)") + f.BoolVar(&opts.DryRun, "dry-run", false, "解析目标并打印订阅计划,不创建订阅或连接 bus") + f.StringVar(&opts.ControlBaseURL, "personal-event-base-url", "", "个人事件控制面 base URL;默认由 MCP base 派生 /dws") + f.StringVar(&opts.StreamTicketMode, "stream-ticket-mode", strings.TrimSpace(os.Getenv("DWS_STREAM_TICKET_MODE")), "个人 Stream 建联模式;默认 normal") + f.StringVar(&opts.StreamSourceID, "stream-source-id", strings.TrimSpace(os.Getenv("DWS_STREAM_SOURCE_ID")), "个人 Stream sourceId;开源版默认 open") + f.StringVar(&opts.StreamTicketURL, "stream-ticket-url", strings.TrimSpace(os.Getenv("DWS_STREAM_TICKET_URL")), "个人 Stream 取票 URL") + hideEventInternalFlags(cmd, "personal-event-base-url", "stream-ticket-mode", "stream-source-id", "stream-ticket-url") + cli.AnnotateRuntimeFlagEnum(cmd, "kind", "at-me", "sender", "group", "all-direct", "all-group") + cli.AnnotateRuntimeFlagEnum(cmd, "events", "message", "reaction", "read", "recall") + cli.AnnotateRuntimeConstraints(cmd, cli.RuntimeSchemaConstraints{ + MutuallyExclusive: [][]string{{"user", "open-dingtalk-id", "user-query", "chat-id", "chat-query"}}, + }) + return cmd +} + +func compileListenIMPlan(reader targetresolver.Reader, opts listenIMOptions) (listenIMPlan, error) { + kind := strings.ToLower(strings.TrimSpace(opts.Kind)) + if kind == "" { + kind = "at-me" + } + events := uniqueListenIMValues(opts.Events) + if len(events) == 0 { + return listenIMPlan{}, apperrors.NewValidation("--events 至少包含一个事件种类") + } + if strings.TrimSpace(opts.QueryCSV) != "" { + for _, eventName := range events { + if eventName != "message" { + return listenIMPlan{}, apperrors.NewValidation("--query 只支持 message 事件") + } + } + } + + plan := listenIMPlan{} + var err error + switch kind { + case "at-me", "all-direct", "all-group": + if listenIMTargetCount(opts) != 0 { + return listenIMPlan{}, apperrors.NewValidation(fmt.Sprintf("--kind %s 不接受用户或群目标", kind)) + } + case "sender": + if listenIMUserTargetCount(opts) != 1 || listenIMChatTargetCount(opts) != 0 { + return listenIMPlan{}, apperrors.NewValidation("--kind sender 必须且只能指定 --user、--open-dingtalk-id 或 --user-query 之一") + } + plan.UserID = strings.TrimSpace(opts.UserID) + plan.OpenDingTalkID = strings.TrimSpace(opts.OpenDingTalkID) + if query := strings.TrimSpace(opts.UserQuery); query != "" { + resolved, resolveErr := targetresolver.ResolveUser(reader, query, targetresolver.IdentityAny) + if resolveErr != nil { + return listenIMPlan{}, resolveErr + } + plan.ResolvedTargets = append(plan.ResolvedTargets, resolved) + plan.UserID = resolved.Selected.UserID + if plan.UserID == "" { + plan.OpenDingTalkID = resolved.Selected.OpenDingTalkID + } + } + case "group": + if listenIMChatTargetCount(opts) != 1 || listenIMUserTargetCount(opts) != 0 { + return listenIMPlan{}, apperrors.NewValidation("--kind group 必须且只能指定 --chat-id 或 --chat-query 之一") + } + plan.GroupID = strings.TrimSpace(opts.ChatID) + if query := strings.TrimSpace(opts.ChatQuery); query != "" { + resolved, resolveErr := targetresolver.ResolveChat(reader, query) + if resolveErr != nil { + return listenIMPlan{}, resolveErr + } + plan.ResolvedTargets = append(plan.ResolvedTargets, resolved) + plan.GroupID = resolved.Selected.OpenConversationID + } + default: + return listenIMPlan{}, apperrors.NewValidation("--kind 必须是 at-me、sender、group、all-direct 或 all-group") + } + + plan.EventKeys, err = listenIMEventKeys(kind, events) + if err != nil { + return listenIMPlan{}, err + } + return plan, nil +} + +func listenIMEventKeys(kind string, events []string) ([]string, error) { + mapping := map[string]map[string]string{ + "at-me": {"message": personal.EventMention}, + "sender": {"message": personal.EventFromUser, "reaction": personal.EventReactionO2O, "read": personal.EventReadO2O, "recall": personal.EventRecallO2O}, + "group": {"message": personal.EventInChat, "reaction": personal.EventReactionGroup, "read": personal.EventReadGroup, "recall": personal.EventRecallGroup}, + "all-direct": {"message": personal.EventAllSingleChat}, + "all-group": {"message": personal.EventAllGroupChat}, + } + byEvent := mapping[kind] + keys := make([]string, 0, len(events)) + for _, eventName := range events { + key := byEvent[eventName] + if key == "" { + return nil, apperrors.NewValidation(fmt.Sprintf("--kind %s 不支持 event %s", kind, eventName)) + } + keys = append(keys, key) + } + return keys, nil +} + +func listenIMUserTargetCount(opts listenIMOptions) int { + return nonEmptyListenIMCount(opts.UserID, opts.OpenDingTalkID, opts.UserQuery) +} + +func listenIMChatTargetCount(opts listenIMOptions) int { + return nonEmptyListenIMCount(opts.ChatID, opts.ChatQuery) +} + +func listenIMTargetCount(opts listenIMOptions) int { + return listenIMUserTargetCount(opts) + listenIMChatTargetCount(opts) +} + +func nonEmptyListenIMCount(values ...string) int { + count := 0 + for _, value := range values { + if strings.TrimSpace(value) != "" { + count++ + } + } + return count +} + +func uniqueListenIMValues(values []string) []string { + out := make([]string, 0, len(values)) + seen := map[string]bool{} + for _, value := range values { + value = strings.ToLower(strings.TrimSpace(value)) + if value == "" || seen[value] { + continue + } + seen[value] = true + out = append(out, value) + } + return out +} diff --git a/internal/app/event_listen_im_test.go b/internal/app/event_listen_im_test.go new file mode 100644 index 00000000..73d24eaa --- /dev/null +++ b/internal/app/event_listen_im_test.go @@ -0,0 +1,270 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); + +package app + +import ( + "bytes" + "context" + "errors" + "fmt" + "reflect" + "strings" + "testing" + + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/consume" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/event/personal" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver" + "github.com/spf13/cobra" +) + +type listenIMFakeReader struct { + responses map[string]map[string]any + calls []string +} + +func (f *listenIMFakeReader) CallMCPData(product, tool string, _ map[string]any) (map[string]any, error) { + key := product + "/" + tool + f.calls = append(f.calls, key) + if response, ok := f.responses[key]; ok { + return response, nil + } + return map[string]any{}, nil +} + +func TestCompileListenIMPlanResolvesGroupAndMapsMultipleEvents(t *testing.T) { + reader := &listenIMFakeReader{responses: map[string]map[string]any{ + "im/search_groups": { + "result": []any{map[string]any{"title": "项目群", "openConversationId": "cid-1"}}, + }, + }} + plan, err := compileListenIMPlan(reader, listenIMOptions{ + Kind: "group", + Events: []string{"message", "reaction", "recall"}, + ChatQuery: "项目群", + }) + if err != nil { + t.Fatal(err) + } + wantKeys := []string{personal.EventInChat, personal.EventReactionGroup, personal.EventRecallGroup} + if !reflect.DeepEqual(plan.EventKeys, wantKeys) || plan.GroupID != "cid-1" { + t.Fatalf("plan = %#v, want keys=%v group=cid-1", plan, wantKeys) + } + if !reflect.DeepEqual(reader.calls, []string{"im/search_groups"}) { + t.Fatalf("resolver calls = %#v", reader.calls) + } +} + +func TestCompileListenIMPlanReturnsStructuredAmbiguityBeforeSubscription(t *testing.T) { + reader := &listenIMFakeReader{responses: map[string]map[string]any{ + "contact/search_contact_by_key_word": { + "result": []any{ + map[string]any{"name": "张三", "userId": "u1"}, + map[string]any{"name": "张三", "userId": "u2"}, + }, + }, + }} + _, err := compileListenIMPlan(reader, listenIMOptions{ + Kind: "sender", + Events: []string{"message"}, + UserQuery: "张三", + }) + if err == nil { + t.Fatal("ambiguous sender unexpectedly compiled") + } + var typed *apperrors.Error + if !errors.As(err, &typed) || typed.Reason != "resolution_ambiguous" { + t.Fatalf("ambiguity error = %#v", err) + } +} + +func TestEventListenIMCommandDelegatesOneCompiledConsumeLifecycle(t *testing.T) { + reader := &listenIMFakeReader{responses: map[string]map[string]any{ + "im/search_groups": { + "result": []any{map[string]any{"title": "项目群", "openConversationId": "cid-1"}}, + }, + }} + oldReader := eventListenIMReader + oldRun := eventRunPersonalConsume + t.Cleanup(func() { + eventListenIMReader = oldReader + eventRunPersonalConsume = oldRun + }) + eventListenIMReader = func() targetresolver.Reader { return reader } + var captured personalConsumeOptions + var calls int + eventRunPersonalConsume = func(_ *cobra.Command, opts personalConsumeOptions) error { + calls++ + captured = opts + return nil + } + + cmd := newEventListenIMCommand() + cmd.SetArgs([]string{ + "--kind", "group", + "--events", "message,reaction", + "--chat-query", "项目群", + "--max-events", "2", + "--duration", "30s", + "--dry-run", + }) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + if calls != 1 { + t.Fatalf("consume lifecycle calls = %d, want 1", calls) + } + if !reflect.DeepEqual(captured.EventKeys, []string{personal.EventInChat, personal.EventReactionGroup}) || + captured.GroupID != "cid-1" || !captured.Flatten || !captured.Common.DryRun || + captured.Common.MaxEvents != 2 || captured.Common.Duration.String() != "30s" { + t.Fatalf("captured options = %#v", captured) + } +} + +func TestCompileListenIMPlanRejectsIncompatibleKindAndTargets(t *testing.T) { + reader := &listenIMFakeReader{} + cases := []listenIMOptions{ + {Kind: "at-me", Events: []string{"reaction"}}, + {Kind: "all-group", Events: []string{"message"}, ChatID: "cid"}, + {Kind: "sender", Events: []string{"message"}}, + {Kind: "group", Events: []string{"message"}, ChatID: "cid", ChatQuery: "群"}, + {Kind: "group", Events: []string{"message", "reaction"}, ChatID: "cid", QueryCSV: "关键词"}, + } + for _, opts := range cases { + if _, err := compileListenIMPlan(reader, opts); err == nil { + t.Errorf("options unexpectedly accepted: %#v", opts) + } + } +} + +func TestEventListenIME2ELifecycleCleansAndRollsBack(t *testing.T) { + newReader := func() *listenIMFakeReader { + return &listenIMFakeReader{responses: map[string]map[string]any{ + "im/search_groups": { + "result": []any{map[string]any{"title": "项目群", "openConversationId": "cid-1"}}, + }, + }} + } + installFacade := func(t *testing.T, reader *listenIMFakeReader) { + t.Helper() + oldReader := eventListenIMReader + oldRun := eventRunPersonalConsume + t.Cleanup(func() { + eventListenIMReader = oldReader + eventRunPersonalConsume = oldRun + }) + eventListenIMReader = func() targetresolver.Reader { return reader } + eventRunPersonalConsume = runPersonalEventConsume + } + installLifecycle := func(t *testing.T) { + t.Helper() + restore := installPersonalManySeams(t) + t.Cleanup(restore) + t.Setenv("DWS_CONFIG_DIR", t.TempDir()) + personalResolveEventIdentity = func(context.Context, string, string) (personal.Identity, error) { + return personal.Identity{ + AccessToken: "token", CorpID: "corp", UserID: "user", + ClientID: "client", SourceID: "open", + }, nil + } + personalUpsertRunState = func(string, personal.RunState) error { return nil } + personalValidateConsumeConfig = func(consume.Config) error { return nil } + personalValidateNoOutputConflict = func(consume.Config, string) error { return nil } + } + + t.Run("ready then clean every created subscription", func(t *testing.T) { + reader := newReader() + installFacade(t, reader) + installLifecycle(t) + var created, deleted, removed []string + personalEnsureSubscription = func(_ context.Context, _ *personal.Client, _ personal.Identity, opts personalConsumeOptions) (*personal.Subscription, string, string, error) { + created = append(created, opts.EventKey) + return &personal.Subscription{SubscribeID: "sub-" + opts.EventKey}, opts.EventKey, "group", nil + } + personalDeleteSubscription = func(_ *personal.Client, _ context.Context, id string) error { + deleted = append(deleted, id) + return nil + } + personalRemoveRunStates = func(_ string, ids []string) error { + removed = append(removed, ids...) + return nil + } + personalConsumeRunMany = func(_ context.Context, cfg consume.Config, specs []consume.ConsumerSpec) error { + if len(specs) != 2 || !cfg.Flatten { + t.Fatalf("consume specs/config = %#v / %#v", specs, cfg) + } + fmt.Fprintf(cfg.Stderr, "[event] ready event_count=%d bus_pid=123\n", len(specs)) + return nil + } + + cmd := newEventListenIMCommand() + var stderr bytes.Buffer + cmd.SetErr(&stderr) + cmd.SetArgs([]string{ + "--kind", "group", "--events", "message,reaction", + "--chat-query", "项目群", "--max-events", "1", + }) + if err := cmd.Execute(); err != nil { + t.Fatal(err) + } + wantEvents := []string{personal.EventInChat, personal.EventReactionGroup} + if !reflect.DeepEqual(created, wantEvents) { + t.Fatalf("created = %#v, want %#v", created, wantEvents) + } + wantDeleted := []string{"sub-" + personal.EventReactionGroup, "sub-" + personal.EventInChat} + if !reflect.DeepEqual(deleted, wantDeleted) || !reflect.DeepEqual(removed, wantDeleted) { + t.Fatalf("deleted=%#v removed=%#v want=%#v", deleted, removed, wantDeleted) + } + if !strings.Contains(stderr.String(), "[event] ready event_count=2") { + t.Fatalf("missing ready marker: %s", stderr.String()) + } + if !reflect.DeepEqual(reader.calls, []string{"im/search_groups"}) { + t.Fatalf("resolver calls = %#v", reader.calls) + } + }) + + t.Run("second create failure rolls back first without starting consumer", func(t *testing.T) { + reader := newReader() + installFacade(t, reader) + installLifecycle(t) + wantErr := errors.New("second subscription failed") + calls := 0 + personalEnsureSubscription = func(_ context.Context, _ *personal.Client, _ personal.Identity, opts personalConsumeOptions) (*personal.Subscription, string, string, error) { + calls++ + if calls == 2 { + return nil, "", "", wantErr + } + return &personal.Subscription{SubscribeID: "sub-first"}, opts.EventKey, "group", nil + } + var deleted, removed []string + personalDeleteSubscription = func(_ *personal.Client, _ context.Context, id string) error { + deleted = append(deleted, id) + return nil + } + personalRemoveRunStates = func(_ string, ids []string) error { + removed = append(removed, ids...) + return nil + } + personalConsumeRunMany = func(context.Context, consume.Config, []consume.ConsumerSpec) error { + t.Fatal("consumer started after partial subscription failure") + return nil + } + + cmd := newEventListenIMCommand() + var stderr bytes.Buffer + cmd.SetErr(&stderr) + cmd.SilenceUsage = true + cmd.SetArgs([]string{ + "--kind", "group", "--events", "message,reaction", + "--chat-query", "项目群", + }) + err := cmd.Execute() + if err == nil || !strings.Contains(err.Error(), wantErr.Error()) { + t.Fatalf("error = %v, want %v", err, wantErr) + } + if !reflect.DeepEqual(deleted, []string{"sub-first"}) || !reflect.DeepEqual(removed, []string{"sub-first"}) { + t.Fatalf("rollback deleted=%#v removed=%#v", deleted, removed) + } + }) +} diff --git a/internal/app/event_registration_test.go b/internal/app/event_registration_test.go index f70c9d47..e7893746 100644 --- a/internal/app/event_registration_test.go +++ b/internal/app/event_registration_test.go @@ -37,7 +37,7 @@ func TestEventCommandRemainsVisibleAsBuiltInPublicGroup(t *testing.T) { leaves = append(leaves, command.Name()) } sort.Strings(leaves) - want := []string{"consume", "list", "schema", "status", "stop"} + want := []string{"+listen-im", "consume", "list", "schema", "status", "stop"} if len(leaves) != len(want) { t.Fatalf("public event leaves = %v, want %v", leaves, want) } diff --git a/internal/app/schema_shortcut_contract_test.go b/internal/app/schema_shortcut_contract_test.go index 007fc545..db7488f4 100644 --- a/internal/app/schema_shortcut_contract_test.go +++ b/internal/app/schema_shortcut_contract_test.go @@ -17,7 +17,7 @@ import ( const ( publicShortcutCount = 265 - schemaPublishedShortcutCount = 215 + schemaPublishedShortcutCount = 217 ) func TestEmbeddedSchemaCoversOrExactlyExcludesEveryPublicShortcutContract(t *testing.T) { @@ -106,7 +106,7 @@ func TestEmbeddedShortcutProgressiveQueriesReturnCompleteContracts(t *testing.T) product := executeShortcutSchemaQuery(t, "chat") productPayload, _ := product["product"].(map[string]any) - if got, want := int(product["count"].(float64)), 129; got != want { + if got, want := int(product["count"].(float64)), 131; got != want { t.Fatalf("schema chat count = %d, want %d", got, want) } summaries := schemaContractObjectSlice(productPayload["tools"]) @@ -116,8 +116,8 @@ func TestEmbeddedShortcutProgressiveQueriesReturnCompleteContracts(t *testing.T) shortcutCount++ } } - if shortcutCount != 47 { - t.Fatalf("schema chat shortcut summaries = %d, want 47", shortcutCount) + if shortcutCount != 49 { + t.Fatalf("schema chat shortcut summaries = %d, want 49", shortcutCount) } } diff --git a/internal/cli/schema_agent_metadata/chat.json b/internal/cli/schema_agent_metadata/chat.json index 702ddd52..fa492f05 100644 --- a/internal/cli/schema_agent_metadata/chat.json +++ b/internal/cli/schema_agent_metadata/chat.json @@ -2327,6 +2327,267 @@ "当你想查看某个群里已添加了哪些机器人时使用;需传群 openConversationId,只读返回群内机器人列表(含 openBotId,供后续移除)。" ] }, + "chat +chat-create": { + "agent_summary": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + "agent_summary_source": "dws-agent-selection/chat", + "availability": "available", + "avoid_when": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ], + "confirmation": "user_required", + "effect": "write", + "effect_source": "agent-hint", + "examples": [ + "dws chat +chat-create --name \"项目冲刺群\" --users \u003cuserId1\u003e,\u003cuserId2\u003e", + "dws chat +chat-create --name \"合作群\" --member-query \"张三,李四\" --type EXTERNAL" + ], + "field_provenance": { + "agent_summary": { + "value": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "candidates": [ + { + "value": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields." + } + ] + }, + "availability": { + "value": "available", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": "available", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + } + ] + }, + "avoid_when": { + "value": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "candidates": [ + { + "value": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields." + } + ] + }, + "confirmation": { + "value": "user_required", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": "user_required", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + } + ] + }, + "effect": { + "value": "write", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": "write", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + } + ] + }, + "examples": { + "value": [ + "dws chat +chat-create --name \"项目冲刺群\" --users \u003cuserId1\u003e,\u003cuserId2\u003e", + "dws chat +chat-create --name \"合作群\" --member-query \"张三,李四\" --type EXTERNAL" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "candidates": [ + { + "value": [ + "dws chat +chat-create --name \"项目冲刺群\" --users \u003cuserId1\u003e,\u003cuserId2\u003e", + "dws chat +chat-create --name \"合作群\" --member-query \"张三,李四\" --type EXTERNAL" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields." + } + ] + }, + "idempotency": { + "value": "unknown", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": "unknown", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + } + ] + }, + "interface_mode": { + "value": "composite", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": "composite", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + } + ] + }, + "interface_reason": { + "value": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + } + ] + }, + "interface_ref": { + "value": null, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "interface_disposition_matrix", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "candidates": [ + { + "value": null, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "final interface mode composite forbids a direct MCP interface_ref" + } + ] + }, + "reviewed": { + "value": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + }, + { + "value": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": false, + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields." + } + ] + }, + "risk": { + "value": "medium", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "candidates": [ + { + "value": "medium", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics." + } + ] + }, + "use_when": { + "value": [ + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "candidates": [ + { + "value": [ + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields." + } + ] + } + }, + "idempotency": "unknown", + "interface_mode": "composite", + "interface_reason": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity.", + "reviewed": true, + "risk": "medium", + "source_refs": [ + "ShortcutRegistry:chat +chat-create", + "cobra-help:dws chat +chat-create", + "internal/cli/schema_command_registry/products/chat.json#chat.shortcut_chat_create", + "internal/cli/schema_hints/metadata/chat.json", + "internal/cli/schema_hints/selection/chat.json" + ], + "use_when": [ + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ] + }, "chat +chat-dismiss": { "agent_summary": "解散群聊(不可逆,需群主权限)", "agent_summary_source": "dws-agent-selection/chat", @@ -3618,7 +3879,7 @@ ] }, "chat +chat-messages": { - "agent_summary": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "agent_summary": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -3633,18 +3894,18 @@ ], "field_provenance": { "agent_summary": { - "value": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "value": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "candidates": [ { - "value": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "value": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget." + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget." } ] }, @@ -3653,14 +3914,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { "value": "available", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." } ] }, @@ -3671,7 +3932,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "candidates": [ { "value": [ @@ -3680,7 +3941,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget." + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget." } ] }, @@ -3689,14 +3950,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { "value": "not_required", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." } ] }, @@ -3705,14 +3966,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { "value": "read", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." } ] }, @@ -3724,7 +3985,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "candidates": [ { "value": [ @@ -3734,7 +3995,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget." + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget." } ] }, @@ -3743,14 +4004,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { "value": "idempotent", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." } ] }, @@ -3759,30 +4020,30 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { "value": "composite", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." } ] }, "interface_reason": { - "value": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.", + "value": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { - "value": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.", + "value": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." } ] }, @@ -3807,21 +4068,21 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { "value": true, "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." }, { "value": true, "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": false, - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget." + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget." } ] }, @@ -3830,41 +4091,41 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "candidates": [ { "value": "low", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication." } ] }, "use_when": { "value": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "candidates": [ { "value": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget." + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget." } ] } }, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.", + "interface_reason": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness.", "reviewed": true, "risk": "low", "source_refs": [ @@ -3876,7 +4137,7 @@ "skills/mono/references/intent-guide.md" ], "use_when": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ] }, "chat +chat-mute": { @@ -7753,7 +8014,7 @@ ] }, "chat +dm": { - "agent_summary": "按姓名直接给某人发单聊消息(自动解析 userId)", + "agent_summary": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -7767,14 +8028,14 @@ ], "field_provenance": { "agent_summary": { - "value": "按姓名直接给某人发单聊消息(自动解析 userId)", + "value": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "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": "按姓名直接给某人发单聊消息(自动解析 userId)", + "value": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, @@ -9077,14 +9338,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { "value": "available", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." } ] }, @@ -9113,14 +9374,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { "value": "not_required", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." } ] }, @@ -9129,14 +9390,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { "value": "read", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." } ] }, @@ -9165,14 +9426,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { "value": "idempotent", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." } ] }, @@ -9181,30 +9442,30 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { "value": "composite", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." } ] }, "interface_reason": { - "value": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", + "value": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { - "value": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", + "value": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." } ] }, @@ -9229,14 +9490,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { "value": true, "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." }, { "value": true, @@ -9252,14 +9513,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "candidates": [ { "value": "low", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false." } ] }, @@ -9286,7 +9547,7 @@ }, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", + "interface_reason": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting.", "reviewed": true, "risk": "low", "source_refs": [ @@ -9816,34 +10077,33 @@ "当你想知道自己发出的某条消息有哪些人已读/未读时使用;只读,需传会话 openConversationId 和该消息 openMessageId,可指定目标成员列表。" ] }, - "chat +messages-send": { - "agent_summary": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "chat +messages-reply": { + "agent_summary": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" ], "confirmation": "user_required", "effect": "write", "effect_source": "agent-hint", "examples": [ - "dws chat +messages-send --as user --chat-id \u003copenConversationId\u003e --markdown \"## 周报\" --idempotency-key \u003ckey\u003e", - "dws chat +messages-send --as user --user \u003cuserId\u003e --msg-type file --file ./report.pdf" + "dws chat +messages-reply --conversation-id \u003copenConversationId\u003e --message-id \u003copenMessageId\u003e --text \"收到\" --idempotency-key \u003ckey\u003e" ], "field_provenance": { "agent_summary": { - "value": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "value": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", "candidates": [ { - "value": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "value": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media." + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing." } ] }, @@ -9852,34 +10112,34 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { "value": "available", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." } ] }, "avoid_when": { "value": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", "candidates": [ { "value": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media." + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing." } ] }, @@ -9888,20 +10148,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { "value": "user_required", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." - }, - { - "value": "not_required", - "source": "risk-default", - "precedence": "inference_or_default", - "selected": false + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." } ] }, @@ -9910,42 +10164,34 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { "value": "write", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." - }, - { - "value": "write", - "source": "command-verb", - "precedence": "inference_or_default", - "selected": false + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." } ] }, "examples": { "value": [ - "dws chat +messages-send --as user --chat-id \u003copenConversationId\u003e --markdown \"## 周报\" --idempotency-key \u003ckey\u003e", - "dws chat +messages-send --as user --user \u003cuserId\u003e --msg-type file --file ./report.pdf" + "dws chat +messages-reply --conversation-id \u003copenConversationId\u003e --message-id \u003copenMessageId\u003e --text \"收到\" --idempotency-key \u003ckey\u003e" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", "candidates": [ { "value": [ - "dws chat +messages-send --as user --chat-id \u003copenConversationId\u003e --markdown \"## 周报\" --idempotency-key \u003ckey\u003e", - "dws chat +messages-send --as user --user \u003cuserId\u003e --msg-type file --file ./report.pdf" + "dws chat +messages-reply --conversation-id \u003copenConversationId\u003e --message-id \u003copenMessageId\u003e --text \"收到\" --idempotency-key \u003ckey\u003e" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media." + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing." } ] }, @@ -9954,20 +10200,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { "value": "unknown", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." - }, - { - "value": "unknown", - "source": "effect-default", - "precedence": "inference_or_default", - "selected": false + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." } ] }, @@ -9976,30 +10216,30 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { "value": "composite", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." } ] }, "interface_reason": { - "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow.", + "value": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { - "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow.", + "value": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." } ] }, @@ -10024,21 +10264,21 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { "value": true, "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." }, { "value": true, "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": false, - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media." + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing." } ] }, @@ -10047,14 +10287,293 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", "candidates": [ { "value": "medium", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists." + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists." + } + ] + }, + "use_when": { + "value": [ + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "candidates": [ + { + "value": [ + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing." + } + ] + } + }, + "idempotency": "unknown", + "interface_mode": "composite", + "interface_reason": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload.", + "reviewed": true, + "risk": "medium", + "source_refs": [ + "ShortcutRegistry:chat +messages-reply", + "cobra-help:dws chat +messages-reply", + "internal/cli/schema_command_registry/products/chat.json#chat.shortcut_messages_reply", + "internal/cli/schema_hints/metadata/chat.json", + "internal/cli/schema_hints/selection/chat.json" + ], + "use_when": [ + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" + ] + }, + "chat +messages-send": { + "agent_summary": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "agent_summary_source": "dws-agent-selection/chat", + "availability": "available", + "avoid_when": [ + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" + ], + "confirmation": "user_required", + "effect": "write", + "effect_source": "agent-hint", + "examples": [ + "dws chat +messages-send --as user --chat-id \u003copenConversationId\u003e --markdown \"## 周报\" --idempotency-key \u003ckey\u003e", + "dws chat +messages-send --as user --user \u003cuserId\u003e --msg-type file --file ./report.pdf" + ], + "field_provenance": { + "agent_summary": { + "value": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", + "candidates": [ + { + "value": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media." + } + ] + }, + "availability": { + "value": "available", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": "available", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." + } + ] + }, + "avoid_when": { + "value": [ + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", + "candidates": [ + { + "value": [ + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media." + } + ] + }, + "confirmation": { + "value": "user_required", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": "user_required", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." + }, + { + "value": "not_required", + "source": "risk-default", + "precedence": "inference_or_default", + "selected": false + } + ] + }, + "effect": { + "value": "write", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": "write", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." + }, + { + "value": "write", + "source": "command-verb", + "precedence": "inference_or_default", + "selected": false + } + ] + }, + "examples": { + "value": [ + "dws chat +messages-send --as user --chat-id \u003copenConversationId\u003e --markdown \"## 周报\" --idempotency-key \u003ckey\u003e", + "dws chat +messages-send --as user --user \u003cuserId\u003e --msg-type file --file ./report.pdf" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", + "candidates": [ + { + "value": [ + "dws chat +messages-send --as user --chat-id \u003copenConversationId\u003e --markdown \"## 周报\" --idempotency-key \u003ckey\u003e", + "dws chat +messages-send --as user --user \u003cuserId\u003e --msg-type file --file ./report.pdf" + ], + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media." + } + ] + }, + "idempotency": { + "value": "unknown", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": "unknown", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." + }, + { + "value": "unknown", + "source": "effect-default", + "precedence": "inference_or_default", + "selected": false + } + ] + }, + "interface_mode": { + "value": "composite", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": "composite", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." + } + ] + }, + "interface_reason": { + "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." + } + ] + }, + "interface_ref": { + "value": null, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "interface_disposition_matrix", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "candidates": [ + { + "value": null, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "final interface mode composite forbids a direct MCP interface_ref" + } + ] + }, + "reviewed": { + "value": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." + }, + { + "value": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "precedence": "reviewed_explicit", + "selected": false, + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media." + } + ] + }, + "risk": { + "value": "medium", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", + "candidates": [ + { + "value": "medium", + "source": "internal/cli/schema_hints/metadata/chat.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists." }, { "value": "medium", @@ -10066,28 +10585,28 @@ }, "use_when": { "value": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "candidates": [ { "value": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media." + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media." } ] } }, "idempotency": "unknown", "interface_mode": "composite", - "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow.", + "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow.", "reviewed": true, "risk": "medium", "source_refs": [ @@ -10099,7 +10618,7 @@ "skills/mono/references/intent-guide.md" ], "use_when": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" ] }, "chat +messages-send-by-webhook": { @@ -11141,7 +11660,7 @@ ] }, "chat +search-msg": { - "agent_summary": "多维搜索消息,可全量翻页并批量富化详情", + "agent_summary": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -11156,18 +11675,18 @@ ], "field_provenance": { "agent_summary": { - "value": "多维搜索消息,可全量翻页并批量富化详情", + "value": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "candidates": [ { - "value": "多维搜索消息,可全量翻页并批量富化详情", + "value": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." } ] }, @@ -11176,14 +11695,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { "value": "available", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." } ] }, @@ -11194,7 +11713,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "candidates": [ { "value": [ @@ -11203,7 +11722,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." } ] }, @@ -11212,14 +11731,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { "value": "not_required", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." } ] }, @@ -11228,14 +11747,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { "value": "read", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." } ] }, @@ -11247,7 +11766,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "candidates": [ { "value": [ @@ -11257,7 +11776,7 @@ "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." } ] }, @@ -11266,14 +11785,14 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { "value": "idempotent", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." } ] }, @@ -11282,30 +11801,30 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { "value": "composite", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." } ] }, "interface_reason": { - "value": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.", + "value": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { - "value": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.", + "value": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." } ] }, @@ -11330,21 +11849,21 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { "value": true, "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." }, { "value": true, "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": false, - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." } ] }, @@ -11353,41 +11872,41 @@ "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { "value": "low", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op." } ] }, "use_when": { "value": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "candidates": [ { "value": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ], "source": "internal/cli/schema_hints/selection/chat.json", "precedence": "reviewed_explicit", "selected": true, - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow." } ] } }, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.", + "interface_reason": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting.", "reviewed": true, "risk": "low", "source_refs": [ @@ -11399,7 +11918,7 @@ "skills/mono/references/intent-guide.md" ], "use_when": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ] }, "chat +send-to-group": { @@ -11814,14 +12333,14 @@ ] }, "interface_reason": { - "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.", + "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "Reviewed against the executable thread/topic alias contract and real list_topic_replies mapping. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "candidates": [ { - "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.", + "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness.", "source": "internal/cli/schema_hints/metadata/chat.json", "precedence": "reviewed_explicit", "selected": true, @@ -11907,7 +12426,7 @@ }, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.", + "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness.", "reviewed": true, "risk": "low", "source_refs": [ diff --git a/internal/cli/schema_agent_metadata/event.json b/internal/cli/schema_agent_metadata/event.json index fd6f490a..cb78644a 100644 --- a/internal/cli/schema_agent_metadata/event.json +++ b/internal/cli/schema_agent_metadata/event.json @@ -1,6 +1,268 @@ { "product_id": "event", "tools": { + "event +listen-im": { + "agent_summary": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件", + "agent_summary_source": "dws-agent-selection/event", + "availability": "available", + "avoid_when": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ], + "confirmation": "not_required", + "effect": "write", + "effect_source": "agent-hint", + "examples": [ + "dws event +listen-im --kind at-me --max-events 1", + "dws event +listen-im --kind group --events message,reaction --chat-id \u003copenConversationId\u003e --duration 10m" + ], + "field_provenance": { + "agent_summary": { + "value": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件", + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "candidates": [ + { + "value": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件", + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume." + } + ] + }, + "availability": { + "value": "available", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": "available", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + } + ] + }, + "avoid_when": { + "value": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ], + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "candidates": [ + { + "value": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ], + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume." + } + ] + }, + "confirmation": { + "value": "not_required", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": "not_required", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + } + ] + }, + "effect": { + "value": "write", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": "write", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + } + ] + }, + "examples": { + "value": [ + "dws event +listen-im --kind at-me --max-events 1", + "dws event +listen-im --kind group --events message,reaction --chat-id \u003copenConversationId\u003e --duration 10m" + ], + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "candidates": [ + { + "value": [ + "dws event +listen-im --kind at-me --max-events 1", + "dws event +listen-im --kind group --events message,reaction --chat-id \u003copenConversationId\u003e --duration 10m" + ], + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume." + } + ] + }, + "idempotency": { + "value": "non_idempotent", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": "non_idempotent", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + } + ] + }, + "interface_mode": { + "value": "composite", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": "composite", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + } + ] + }, + "interface_reason": { + "value": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle.", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle.", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + } + ] + }, + "interface_ref": { + "value": null, + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "interface_disposition_matrix", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "candidates": [ + { + "value": null, + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "final interface mode composite forbids a direct MCP interface_ref" + } + ] + }, + "reviewed": { + "value": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + }, + { + "value": true, + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "selected": false, + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume." + } + ] + }, + "risk": { + "value": "medium", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "candidates": [ + { + "value": "medium", + "source": "internal/cli/schema_hints/metadata/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings." + } + ] + }, + "use_when": { + "value": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ], + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "candidates": [ + { + "value": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ], + "source": "internal/cli/schema_hints/selection/event.json", + "precedence": "reviewed_explicit", + "selected": true, + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume." + } + ] + } + }, + "idempotency": "non_idempotent", + "interface_mode": "composite", + "interface_reason": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle.", + "reviewed": true, + "risk": "medium", + "source_refs": [ + "cobra-help:dws event +listen-im", + "internal/app/event_listen_im.go", + "internal/cli/schema_command_registry/products/event.json#event.listen_im", + "internal/cli/schema_hints/metadata/event.json", + "internal/cli/schema_hints/selection/event.json", + "internal/event/personal/registry.go" + ], + "use_when": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ] + }, "event consume": { "agent_summary": "订阅并持续消费一个或多个兼容的个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "agent_summary_source": "dws-agent-selection/event", diff --git a/internal/cli/schema_agent_metadata/index.json b/internal/cli/schema_agent_metadata/index.json index f4bb0712..0c37c7f9 100644 --- a/internal/cli/schema_agent_metadata/index.json +++ b/internal/cli/schema_agent_metadata/index.json @@ -1,17 +1,17 @@ { "version": 1, - "source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613", - "surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16", + "source_hash": "sha256:78125a5eb3b3621bcbe2c0f504379fe80f1e0f14bd0ebba32c775958af505c5d", + "surface_hash": "sha256:6823217b0d98e1cdebdea7e1b421a6920ce30a985016cf164ade5e14f69a6c30", "coverage": { "surface_products": 26, "products_with_metadata": 26, - "surface_tools": 845, - "tools_with_metadata": 845, - "tools_with_agent_summary": 845, - "tools_with_use_when": 845, - "tools_with_avoid_when": 845, - "tools_with_examples": 845, - "tools_with_interface_mode": 845, + "surface_tools": 848, + "tools_with_metadata": 848, + "tools_with_agent_summary": 848, + "tools_with_use_when": 848, + "tools_with_avoid_when": 848, + "tools_with_examples": 848, + "tools_with_interface_mode": 848, "unmatched_skill_tools": 122, "unreviewed_skill_tools": 11 }, diff --git a/internal/cli/schema_agent_metadata_audit.json b/internal/cli/schema_agent_metadata_audit.json index 5cb7f08c..1bc83428 100644 --- a/internal/cli/schema_agent_metadata_audit.json +++ b/internal/cli/schema_agent_metadata_audit.json @@ -1,11 +1,11 @@ { "version": 1, - "source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613", - "surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16", + "source_hash": "sha256:78125a5eb3b3621bcbe2c0f504379fe80f1e0f14bd0ebba32c775958af505c5d", + "surface_hash": "sha256:6823217b0d98e1cdebdea7e1b421a6920ce30a985016cf164ade5e14f69a6c30", "source_files": 160, "hint_files": 54, "hint_products": 43, - "hint_tools": 1842, + "hint_tools": 1848, "interface_metadata": { "source": "mcp-tools-list+cli-registry", "revision": "4574f7022c32cf4c033e9b7b4156e2fec815fed8", @@ -29,13 +29,13 @@ "coverage": { "surface_products": 26, "products_with_metadata": 26, - "surface_tools": 845, - "tools_with_metadata": 845, - "tools_with_agent_summary": 845, - "tools_with_use_when": 845, - "tools_with_avoid_when": 845, - "tools_with_examples": 845, - "tools_with_interface_mode": 845, + "surface_tools": 848, + "tools_with_metadata": 848, + "tools_with_agent_summary": 848, + "tools_with_use_when": 848, + "tools_with_avoid_when": 848, + "tools_with_examples": 848, + "tools_with_interface_mode": 848, "unmatched_skill_tools": 122, "unreviewed_skill_tools": 11 }, @@ -2805,8 +2805,8 @@ "line": 117, "candidates": [ "event consume", - "event list", - "event schema" + "event +listen-im", + "event list" ] }, { @@ -2815,8 +2815,8 @@ "line": 118, "candidates": [ "event consume", - "event list", - "event schema" + "event +listen-im", + "event list" ] }, { @@ -2825,8 +2825,8 @@ "line": 134, "candidates": [ "event consume", - "event list", - "event schema" + "event +listen-im", + "event list" ] }, { @@ -2835,8 +2835,8 @@ "line": 142, "candidates": [ "event consume", - "event list", - "event schema" + "event +listen-im", + "event list" ] }, { diff --git a/internal/cli/schema_catalog/catalog.json b/internal/cli/schema_catalog/catalog.json index 05b97f39..5cd5f5bd 100644 --- a/internal/cli/schema_catalog/catalog.json +++ b/internal/cli/schema_catalog/catalog.json @@ -1,17 +1,17 @@ { "version": 1, - "surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16", - "source_hash": "sha256:862734fb976a4c9c78d010b7391c74822f37805f5acc650551f4ac60f701c13a", + "surface_hash": "sha256:6823217b0d98e1cdebdea7e1b421a6920ce30a985016cf164ade5e14f69a6c30", + "source_hash": "sha256:480a0b5b5a8d323b73bd8efc07799c52e18980f0c0f0c334e23fbbb95d5ae7e0", "catalog": { "agent_metadata": { "products_with_metadata": 26, "source": "embedded-skill-metadata", - "source_hash": "sha256:670ca810a83bf2aa6f387a18c3af746393d5ea9de24570cbcebbcf994eb7b613", - "surface_hash": "sha256:60eee8e2f37d6d9d60689efce85082798eb9ad38b7ba7c0b471c3de676a85a16", + "source_hash": "sha256:78125a5eb3b3621bcbe2c0f504379fe80f1e0f14bd0ebba32c775958af505c5d", + "surface_hash": "sha256:6823217b0d98e1cdebdea7e1b421a6920ce30a985016cf164ade5e14f69a6c30", "surface_products": 26, - "surface_tools": 845, - "tools_with_agent_summary": 845, - "tools_with_metadata": 845, + "surface_tools": 848, + "tools_with_agent_summary": 848, + "tools_with_metadata": 848, "unmatched_skill_tools": 122, "version": 1 }, @@ -7722,7 +7722,7 @@ "id": "chat", "name": "群聊 / 消息 / 机器人", "runtime": true, - "tool_count": 129, + "tool_count": 131, "tools": [ { "agent_metadata_source": "embedded-skill-metadata", @@ -10022,6 +10022,32 @@ "当你想查看某个群里已添加了哪些机器人时使用;需传群 openConversationId,只读返回群内机器人列表(含 openBotId,供后续移除)。" ] }, + { + "agent_metadata_source": "embedded-skill-metadata", + "agent_summary": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + "agent_summary_source": "dws-agent-selection/chat", + "availability": "available", + "avoid_when": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ], + "canonical_path": "chat.shortcut_chat_create", + "cli_name": "+chat-create", + "cli_path": "chat +chat-create", + "confirmation": "user_required", + "description": "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。\n\n参数约束:\n - --users、--member-query 至少指定一个", + "effect": "write", + "idempotency": "unknown", + "interface_mode": "composite", + "interface_reason": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity.", + "name": "shortcut_chat_create", + "primary_cli_path": "chat +chat-create", + "reviewed": true, + "risk": "medium", + "title": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + "use_when": [ + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ] + }, { "agent_metadata_source": "embedded-skill-metadata", "agent_summary": "解散群聊(不可逆,需群主权限)", @@ -10154,7 +10180,7 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "agent_summary": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -10164,18 +10190,18 @@ "cli_name": "+chat-messages", "cli_path": "chat +chat-messages", "confirmation": "not_required", - "description": "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - --group、--conversation-id、--id、--user、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", + "description": "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。\n\n参数约束:\n - --group、--conversation-id、--id、--chat-query、--user、--user-query、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", "effect": "read", "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.", + "interface_reason": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness.", "name": "shortcut_chat_messages", "primary_cli_path": "chat +chat-messages", "reviewed": true, "risk": "low", - "title": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "title": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", "use_when": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ] }, { @@ -10570,7 +10596,7 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "按姓名直接给某人发单聊消息(自动解析 userId)", + "agent_summary": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -10589,7 +10615,7 @@ "primary_cli_path": "chat +dm", "reviewed": true, "risk": "medium", - "title": "按姓名直接给某人发单聊消息(自动解析 userId)", + "title": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "use_when": [ "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。" ] @@ -10714,7 +10740,7 @@ "effect": "read", "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", + "interface_reason": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting.", "name": "shortcut_messages_mget", "primary_cli_path": "chat +messages-mget", "reviewed": true, @@ -10778,28 +10804,54 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "agent_summary": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" + ], + "canonical_path": "chat.shortcut_messages_reply", + "cli_name": "+messages-reply", + "cli_path": "chat +messages-reply", + "confirmation": "user_required", + "description": "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。\n\n参数约束:\n - --ref-msg-id、--message-id 必须且只能指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个", + "effect": "write", + "idempotency": "unknown", + "interface_mode": "composite", + "interface_reason": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload.", + "name": "shortcut_messages_reply", + "primary_cli_path": "chat +messages-reply", + "reviewed": true, + "risk": "medium", + "title": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", + "use_when": [ + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" + ] + }, + { + "agent_metadata_source": "embedded-skill-metadata", + "agent_summary": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "agent_summary_source": "dws-agent-selection/chat", + "availability": "available", + "avoid_when": [ + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" ], "canonical_path": "chat.shortcut_messages_send", "cli_name": "+messages-send", "cli_path": "chat +messages-send", "confirmation": "user_required", - "description": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持", + "description": "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持", "effect": "write", "idempotency": "unknown", "interface_mode": "composite", - "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow.", + "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow.", "name": "shortcut_messages_send", "primary_cli_path": "chat +messages-send", "reviewed": true, "risk": "medium", - "title": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "title": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", "use_when": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" ] }, { @@ -10908,7 +10960,7 @@ }, { "agent_metadata_source": "embedded-skill-metadata", - "agent_summary": "多维搜索消息,可全量翻页并批量富化详情", + "agent_summary": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -10918,18 +10970,18 @@ "cli_name": "+search-msg", "cli_path": "chat +search-msg", "confirmation": "not_required", - "description": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", + "description": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", "effect": "read", "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.", + "interface_reason": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting.", "name": "shortcut_search_msg", "primary_cli_path": "chat +search-msg", "reviewed": true, "risk": "low", - "title": "多维搜索消息,可全量翻页并批量富化详情", + "title": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "use_when": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ] }, { @@ -10974,7 +11026,7 @@ "effect": "read", "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.", + "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness.", "name": "shortcut_thread_replies", "primary_cli_path": "chat +thread-replies", "reviewed": true, @@ -17589,7 +17641,7 @@ "id": "event", "name": "事件订阅 (DingTalk Stream 长连接)", "runtime": true, - "tool_count": 5, + "tool_count": 6, "tools": [ { "agent_metadata_source": "embedded-skill-metadata", @@ -17650,6 +17702,32 @@ "尚不知道可用 event_key,需要先盘点个人事件目录" ] }, + { + "agent_metadata_source": "embedded-skill-metadata", + "agent_summary": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件", + "agent_summary_source": "dws-agent-selection/event", + "availability": "available", + "avoid_when": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ], + "canonical_path": "event.listen_im", + "cli_name": "+listen-im", + "cli_path": "event +listen-im", + "confirmation": "not_required", + "description": "把 @我、指定发送人、指定群、全部单聊或全部群聊等用户意图确定性编译为个人 EventKey,自然姓名/群名会先唯一解析,再复用 event consume 的订阅、ready marker、NDJSON、取消、回滚和清理生命周期。", + "effect": "write", + "idempotency": "non_idempotent", + "interface_mode": "composite", + "interface_reason": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle.", + "name": "listen_im", + "primary_cli_path": "event +listen-im", + "reviewed": true, + "risk": "medium", + "title": "按 IM 意图解析目标并监听一个或多个个人消息事件", + "use_when": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ] + }, { "agent_metadata_source": "embedded-skill-metadata", "agent_summary": "查询指定个人事件码的输出字段结构;Agent 应查询 --flatten 模式", @@ -26644,6 +26722,6 @@ } ], "source": "embedded-command-catalog", - "tool_count": 845 + "tool_count": 848 } } diff --git a/internal/cli/schema_catalog/tools/chat.json b/internal/cli/schema_catalog/tools/chat.json index 3b71ae25..15ef66b4 100644 --- a/internal/cli/schema_catalog/tools/chat.json +++ b/internal/cli/schema_catalog/tools/chat.json @@ -57391,6 +57391,820 @@ "当你想查看某个群里已添加了哪些机器人时使用;需传群 openConversationId,只读返回群内机器人列表(含 openBotId,供后续移除)。" ] }, + "chat.shortcut_chat_create": { + "agent_metadata_source": "embedded-skill-metadata", + "agent_source_refs": [ + "ShortcutRegistry:chat +chat-create", + "cobra-help:dws chat +chat-create", + "internal/cli/schema_command_registry/products/chat.json#chat.shortcut_chat_create", + "internal/cli/schema_hints/metadata/chat.json", + "internal/cli/schema_hints/selection/chat.json" + ], + "agent_summary": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + "agent_summary_source": "dws-agent-selection/chat", + "availability": "available", + "avoid_when": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ], + "canonical_path": "chat.shortcut_chat_create", + "cli_name": "+chat-create", + "cli_path": "chat +chat-create", + "confirmation": "user_required", + "constraints": { + "require_one_of": [ + [ + "users", + "member-query" + ] + ] + }, + "description": "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。\n\n参数约束:\n - --users、--member-query 至少指定一个", + "display": "群聊 / 消息 / 机器人", + "effect": "write", + "effect_source": "agent-hint", + "examples": [ + "dws chat +chat-create --name \"项目冲刺群\" --users \u003cuserId1\u003e,\u003cuserId2\u003e", + "dws chat +chat-create --name \"合作群\" --member-query \"张三,李四\" --type EXTERNAL" + ], + "field_provenance": { + "agent_summary": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": "按成员 ID 或姓名全量预检后创建一个钉钉群聊" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": "按成员 ID 或姓名全量预检后创建一个钉钉群聊" + }, + "availability": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "available" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "available" + }, + "avoid_when": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ] + }, + "canonical_path": { + "candidates": [ + { + "precedence": "command_registry", + "selected": true, + "source": "reviewed_command_registry", + "source_ref": "chat +chat-create", + "value": "chat.shortcut_chat_create" + } + ], + "precedence": "command_registry", + "resolution": "registry_identity", + "source": "reviewed_command_registry", + "source_ref": "chat +chat-create", + "value": "chat.shortcut_chat_create" + }, + "confirmation": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "user_required" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "user_required" + }, + "description": { + "candidates": [ + { + "precedence": "cobra_help", + "selected": true, + "source": "cobra_help", + "value": "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。\n\n参数约束:\n - --users、--member-query 至少指定一个" + } + ], + "precedence": "cobra_help", + "resolution": "highest_precedence", + "source": "cobra_help", + "value": "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。\n\n参数约束:\n - --users、--member-query 至少指定一个" + }, + "effect": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "write" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "write" + }, + "examples": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "dws chat +chat-create --name \"项目冲刺群\" --users \u003cuserId1\u003e,\u003cuserId2\u003e", + "dws chat +chat-create --name \"合作群\" --member-query \"张三,李四\" --type EXTERNAL" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "dws chat +chat-create --name \"项目冲刺群\" --users \u003cuserId1\u003e,\u003cuserId2\u003e", + "dws chat +chat-create --name \"合作群\" --member-query \"张三,李四\" --type EXTERNAL" + ] + }, + "idempotency": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "unknown" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "unknown" + }, + "interface_mode": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "composite" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "composite" + }, + "interface_reason": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity." + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity." + }, + "interface_ref": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": null + } + ], + "precedence": "reviewed_explicit", + "resolution": "interface_disposition_matrix", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": null + }, + "reviewed": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": true + }, + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "selected": false, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": true + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": true + }, + "risk": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "medium" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "medium" + }, + "title": { + "candidates": [ + { + "precedence": "cobra_help", + "selected": true, + "source": "cobra_help", + "value": "按成员 ID 或姓名全量预检后创建一个钉钉群聊" + } + ], + "precedence": "cobra_help", + "resolution": "highest_precedence", + "source": "cobra_help", + "value": "按成员 ID 或姓名全量预检后创建一个钉钉群聊" + }, + "use_when": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ] + } + }, + "has_parameters": true, + "idempotency": "unknown", + "interface_mode": "composite", + "interface_reason": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity.", + "is_alias": false, + "name": "shortcut_chat_create", + "parameter_count": 5, + "parameters": { + "member-query": { + "description": "按姓名/花名唯一解析的初始成员,可逗号分隔或重复传入", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按姓名/花名唯一解析的初始成员,可逗号分隔或重复传入" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按姓名/花名唯一解析的初始成员,可逗号分隔或重复传入" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "memberQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "memberQuery" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "array" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "array" + } + }, + "property": "memberQuery", + "required": false, + "type": "array" + }, + "name": { + "description": "群名称(必填)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "群名称(必填)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "群名称(必填)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "name" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "name" + }, + "required": { + "candidates": [ + { + "precedence": "native_annotation", + "selected": true, + "source": "native_annotation", + "value": true + }, + { + "precedence": "inference", + "selected": false, + "source": "usage_required_inference", + "value": true + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "native_annotation", + "resolution": "highest_precedence", + "source": "native_annotation", + "value": true + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "name", + "required": true, + "type": "string" + }, + "thread": { + "description": "创建为话题群", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "创建为话题群" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "创建为话题群" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "thread" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "thread" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "boolean" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "boolean" + } + }, + "property": "thread", + "required": false, + "type": "boolean" + }, + "type": { + "default": "INTERNAL", + "description": "群类型(可选值: INTERNAL, EXTERNAL, NORMAL)", + "enum": [ + "INTERNAL", + "EXTERNAL", + "NORMAL" + ], + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "群类型(可选值: INTERNAL, EXTERNAL, NORMAL)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "群类型(可选值: INTERNAL, EXTERNAL, NORMAL)" + }, + "enum": { + "candidates": [ + { + "precedence": "native_annotation", + "selected": true, + "source": "native_annotation", + "value": [ + "INTERNAL", + "EXTERNAL", + "NORMAL" + ] + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": [] + } + ], + "precedence": "native_annotation", + "resolution": "highest_precedence", + "source": "native_annotation", + "value": [ + "INTERNAL", + "EXTERNAL", + "NORMAL" + ] + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "type" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "type" + }, + "required": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_nonzero_default", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_nonzero_default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "type", + "required": false, + "type": "string" + }, + "users": { + "description": "初始成员 userId 或 openDingTalkId 列表", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "初始成员 userId 或 openDingTalkId 列表" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "初始成员 userId 或 openDingTalkId 列表" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "users" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "users" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "array" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "array" + } + }, + "property": "users", + "required": false, + "type": "array" + } + }, + "path": "chat.shortcut_chat_create", + "primary_cli_path": "chat +chat-create", + "product_id": "chat", + "reviewed": true, + "risk": "medium", + "source": "reviewed_command_registry", + "title": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + "use_when": [ + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ] + }, "chat.shortcut_chat_dismiss": { "agent_metadata_source": "embedded-skill-metadata", "agent_source_refs": [ @@ -59904,7 +60718,7 @@ "internal/cli/schema_hints/selection/chat.json", "skills/mono/references/intent-guide.md" ], - "agent_summary": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "agent_summary": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -59918,19 +60732,23 @@ "mutually_exclusive": [ [ "group", + "chat-query", "user", + "user-query", "open-dingtalk-id" ] ], "require_one_of": [ [ "group", + "chat-query", "user", + "user-query", "open-dingtalk-id" ] ] }, - "description": "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - --group、--conversation-id、--id、--user、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", + "description": "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。\n\n参数约束:\n - --group、--conversation-id、--id、--chat-query、--user、--user-query、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", "display": "群聊 / 消息 / 机器人", "effect": "read", "effect_source": "agent-hint", @@ -59943,23 +60761,23 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", - "value": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间" + "value": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "source": "internal/cli/schema_hints/selection/chat.json", - "value": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间" + "value": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影" }, "availability": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" @@ -59967,7 +60785,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" }, @@ -59975,7 +60793,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ @@ -59985,7 +60803,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ "要跨多个会话按关键词、发送者或消息类型检索时使用 +search-msg;已有一批精确消息 ID 时使用 +messages-mget" @@ -60011,7 +60829,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "not_required" @@ -60019,7 +60837,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "not_required" }, @@ -60029,19 +60847,19 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - --group、--conversation-id、--id、--user、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" + "value": "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。\n\n参数约束:\n - --group、--conversation-id、--id、--chat-query、--user、--user-query、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - --group、--conversation-id、--id、--user、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" + "value": "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。\n\n参数约束:\n - --group、--conversation-id、--id、--chat-query、--user、--user-query、--open-dingtalk-id 必须且只能指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" }, "effect": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "read" @@ -60049,7 +60867,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "read" }, @@ -60057,7 +60875,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ @@ -60068,7 +60886,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ "dws chat +chat-messages --group \u003copenConversationId\u003e --direction older", @@ -60079,7 +60897,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "idempotent" @@ -60087,7 +60905,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "idempotent" }, @@ -60095,7 +60913,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" @@ -60103,7 +60921,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" }, @@ -60111,17 +60929,17 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger." + "value": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness." } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger." + "value": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness." }, "interface_ref": { "candidates": [ @@ -60143,14 +60961,14 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": true }, { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "selected": false, "source": "internal/cli/schema_hints/selection/chat.json", "value": true @@ -60158,7 +60976,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": true }, @@ -60166,7 +60984,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "low" @@ -60174,7 +60992,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "low" }, @@ -60184,43 +61002,133 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间" + "value": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间" + "value": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影" }, "use_when": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ] } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ] } }, "has_parameters": true, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.", + "interface_reason": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness.", "is_alias": false, "name": "shortcut_chat_messages", - "parameter_count": 10, + "parameter_count": 12, "parameters": { + "chat-query": { + "description": "按群名解析唯一 openConversationId", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按群名解析唯一 openConversationId" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按群名解析唯一 openConversationId" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "chatQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "chatQuery" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "chatQuery", + "required": false, + "type": "string" + }, "direction": { "description": "时间方向 newer/older;省略时为 older,从时间边界向前读取(可选值: newer, older)", "enum": [ @@ -61127,6 +62035,96 @@ "property": "user", "required": false, "type": "string" + }, + "user-query": { + "description": "按姓名解析唯一 openDingTalkId", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按姓名解析唯一 openDingTalkId" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按姓名解析唯一 openDingTalkId" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "userQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "userQuery" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "userQuery", + "required": false, + "type": "string" } }, "path": "chat.shortcut_chat_messages", @@ -61135,9 +62133,9 @@ "reviewed": true, "risk": "low", "source": "reviewed_command_registry", - "title": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "title": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", "use_when": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ] }, "chat.shortcut_chat_mute": { @@ -69210,7 +70208,7 @@ "internal/cli/schema_hints/metadata/chat.json", "internal/cli/schema_hints/selection/chat.json" ], - "agent_summary": "按姓名直接给某人发单聊消息(自动解析 userId)", + "agent_summary": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -69235,14 +70233,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": "按姓名直接给某人发单聊消息(自动解析 userId)" + "value": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)" } ], "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": "按姓名直接给某人发单聊消息(自动解析 userId)" + "value": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)" }, "availability": { "candidates": [ @@ -69471,13 +70469,13 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "按姓名直接给某人发单聊消息(自动解析 userId)" + "value": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "按姓名直接给某人发单聊消息(自动解析 userId)" + "value": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)" }, "use_when": { "candidates": [ @@ -69798,7 +70796,7 @@ "reviewed": true, "risk": "medium", "source": "reviewed_command_registry", - "title": "按姓名直接给某人发单聊消息(自动解析 userId)", + "title": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "use_when": [ "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。" ] @@ -72189,7 +73187,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" @@ -72197,7 +73195,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" }, @@ -72241,7 +73239,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "not_required" @@ -72249,7 +73247,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "not_required" }, @@ -72271,7 +73269,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "read" @@ -72279,7 +73277,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "read" }, @@ -72307,7 +73305,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "idempotent" @@ -72315,7 +73313,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "idempotent" }, @@ -72323,7 +73321,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" @@ -72331,7 +73329,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" }, @@ -72339,17 +73337,17 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref." + "value": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting." } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref." + "value": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting." }, "interface_ref": { "candidates": [ @@ -72371,7 +73369,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": true @@ -72386,7 +73384,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": true }, @@ -72394,7 +73392,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "low" @@ -72402,7 +73400,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "low" }, @@ -72444,7 +73442,7 @@ "has_parameters": true, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", + "interface_reason": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting.", "is_alias": false, "name": "shortcut_messages_mget", "parameter_count": 5, @@ -73916,6 +74914,1058 @@ "当你想知道自己发出的某条消息有哪些人已读/未读时使用;只读,需传会话 openConversationId 和该消息 openMessageId,可指定目标成员列表。" ] }, + "chat.shortcut_messages_reply": { + "agent_metadata_source": "embedded-skill-metadata", + "agent_source_refs": [ + "ShortcutRegistry:chat +messages-reply", + "cobra-help:dws chat +messages-reply", + "internal/cli/schema_command_registry/products/chat.json#chat.shortcut_messages_reply", + "internal/cli/schema_hints/metadata/chat.json", + "internal/cli/schema_hints/selection/chat.json" + ], + "agent_summary": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", + "agent_summary_source": "dws-agent-selection/chat", + "availability": "available", + "avoid_when": [ + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" + ], + "canonical_path": "chat.shortcut_messages_reply", + "cli_name": "+messages-reply", + "cli_path": "chat +messages-reply", + "confirmation": "user_required", + "constraints": { + "mutually_exclusive": [ + [ + "ref-msg-id", + "message-id" + ], + [ + "uuid", + "idempotency-key" + ] + ], + "require_one_of": [ + [ + "ref-msg-id", + "message-id" + ] + ] + }, + "description": "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。\n\n参数约束:\n - --ref-msg-id、--message-id 必须且只能指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个", + "display": "群聊 / 消息 / 机器人", + "effect": "write", + "effect_source": "agent-hint", + "examples": [ + "dws chat +messages-reply --conversation-id \u003copenConversationId\u003e --message-id \u003copenMessageId\u003e --text \"收到\" --idempotency-key \u003ckey\u003e" + ], + "field_provenance": { + "agent_summary": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文" + }, + "availability": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "available" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "available" + }, + "avoid_when": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" + ] + }, + "canonical_path": { + "candidates": [ + { + "precedence": "command_registry", + "selected": true, + "source": "reviewed_command_registry", + "source_ref": "chat +messages-reply", + "value": "chat.shortcut_messages_reply" + } + ], + "precedence": "command_registry", + "resolution": "registry_identity", + "source": "reviewed_command_registry", + "source_ref": "chat +messages-reply", + "value": "chat.shortcut_messages_reply" + }, + "confirmation": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "user_required" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "user_required" + }, + "description": { + "candidates": [ + { + "precedence": "cobra_help", + "selected": true, + "source": "cobra_help", + "value": "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。\n\n参数约束:\n - --ref-msg-id、--message-id 必须且只能指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个" + } + ], + "precedence": "cobra_help", + "resolution": "highest_precedence", + "source": "cobra_help", + "value": "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。\n\n参数约束:\n - --ref-msg-id、--message-id 必须且只能指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个" + }, + "effect": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "write" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "write" + }, + "examples": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "dws chat +messages-reply --conversation-id \u003copenConversationId\u003e --message-id \u003copenMessageId\u003e --text \"收到\" --idempotency-key \u003ckey\u003e" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "dws chat +messages-reply --conversation-id \u003copenConversationId\u003e --message-id \u003copenMessageId\u003e --text \"收到\" --idempotency-key \u003ckey\u003e" + ] + }, + "idempotency": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "unknown" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "unknown" + }, + "interface_mode": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "composite" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "composite" + }, + "interface_reason": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload." + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload." + }, + "interface_ref": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": null + } + ], + "precedence": "reviewed_explicit", + "resolution": "interface_disposition_matrix", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": null + }, + "reviewed": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": true + }, + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "selected": false, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": true + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": true + }, + "risk": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "medium" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "source": "internal/cli/schema_hints/metadata/chat.json", + "value": "medium" + }, + "title": { + "candidates": [ + { + "precedence": "cobra_help", + "selected": true, + "source": "cobra_help", + "value": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文" + } + ], + "precedence": "cobra_help", + "resolution": "highest_precedence", + "source": "cobra_help", + "value": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文" + }, + "use_when": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "selected": true, + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "source": "internal/cli/schema_hints/selection/chat.json", + "value": [ + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" + ] + } + }, + "has_parameters": true, + "idempotency": "unknown", + "interface_mode": "composite", + "interface_reason": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload.", + "is_alias": false, + "name": "shortcut_messages_reply", + "parameter_count": 8, + "parameters": { + "ai-tag": { + "default": "true", + "description": "消息是否带 AI 发送角标(默认 true)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "消息是否带 AI 发送角标(默认 true)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "消息是否带 AI 发送角标(默认 true)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "aiTag" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "aiTag" + }, + "required": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_nonzero_default", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_nonzero_default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "boolean" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "boolean" + } + }, + "property": "aiTag", + "required": false, + "type": "boolean" + }, + "conversation-id": { + "description": "会话 openConversationId(必填)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "会话 openConversationId(必填)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "会话 openConversationId(必填)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "conversationId" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "conversationId" + }, + "required": { + "candidates": [ + { + "precedence": "native_annotation", + "selected": true, + "source": "native_annotation", + "value": true + }, + { + "precedence": "inference", + "selected": false, + "source": "usage_required_inference", + "value": true + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "native_annotation", + "resolution": "highest_precedence", + "source": "native_annotation", + "value": true + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "conversationId", + "required": true, + "type": "string" + }, + "idempotency-key": { + "description": "--uuid 的 lark-cli 对齐别名", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "--uuid 的 lark-cli 对齐别名" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "--uuid 的 lark-cli 对齐别名" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "idempotencyKey" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "idempotencyKey" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "idempotencyKey", + "required": false, + "type": "string" + }, + "message-id": { + "description": "--ref-msg-id 的 lark-cli 对齐别名", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "--ref-msg-id 的 lark-cli 对齐别名" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "--ref-msg-id 的 lark-cli 对齐别名" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "messageId" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "messageId" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "messageId", + "required": false, + "type": "string" + }, + "ref-msg-id": { + "description": "被引用消息 openMessageId", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "被引用消息 openMessageId" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "被引用消息 openMessageId" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "refMsgId" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "refMsgId" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "refMsgId", + "required": false, + "type": "string" + }, + "ref-sender": { + "description": "原消息发送者 openDingTalkId/userId(userId 通过通讯录搜索精确匹配;不传则自动读取)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "原消息发送者 openDingTalkId/userId(userId 通过通讯录搜索精确匹配;不传则自动读取)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "原消息发送者 openDingTalkId/userId(userId 通过通讯录搜索精确匹配;不传则自动读取)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "refSender" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "refSender" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "refSender", + "required": false, + "type": "string" + }, + "text": { + "description": "纯文本回复内容(必填)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "纯文本回复内容(必填)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "纯文本回复内容(必填)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "text" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "text" + }, + "required": { + "candidates": [ + { + "precedence": "native_annotation", + "selected": true, + "source": "native_annotation", + "value": true + }, + { + "precedence": "inference", + "selected": false, + "source": "usage_required_inference", + "value": true + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "native_annotation", + "resolution": "highest_precedence", + "source": "native_annotation", + "value": true + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "text", + "required": true, + "type": "string" + }, + "uuid": { + "description": "幂等键(可选)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "幂等键(可选)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "幂等键(可选)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "uuid" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "uuid" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "uuid", + "required": false, + "type": "string" + } + }, + "path": "chat.shortcut_messages_reply", + "primary_cli_path": "chat +messages-reply", + "product_id": "chat", + "reviewed": true, + "risk": "medium", + "source": "reviewed_command_registry", + "title": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", + "use_when": [ + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" + ] + }, "chat.shortcut_messages_send": { "agent_metadata_source": "embedded-skill-metadata", "agent_source_refs": [ @@ -73926,11 +75976,11 @@ "internal/cli/schema_hints/selection/chat.json", "skills/mono/references/intent-guide.md" ], - "agent_summary": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "agent_summary": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" ], "canonical_path": "chat.shortcut_messages_send", "cli_name": "+messages-send", @@ -73972,7 +76022,7 @@ ] ] }, - "description": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持", + "description": "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持", "display": "群聊 / 消息 / 机器人", "effect": "write", "effect_source": "agent-hint", @@ -73985,23 +76035,23 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", - "value": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" + "value": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "source": "internal/cli/schema_hints/selection/chat.json", - "value": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" + "value": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" }, "availability": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" @@ -74009,7 +76059,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" }, @@ -74017,20 +76067,20 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" ] } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" ] }, "canonical_path": { @@ -74053,7 +76103,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "user_required" @@ -74067,7 +76117,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "user_required" }, @@ -74077,19 +76127,19 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持" + "value": "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持" + "value": "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。\n\n参数约束:\n - --text、--markdown、--media-id、--file、--file-path 至少指定一个\n - --text、--markdown、--media-id、--file、--file-path 互斥,最多指定一个\n - --identity、--as 互斥,最多指定一个\n - --group、--chat-id 互斥,最多指定一个\n - --user、--open-dingtalk-id 互斥,最多指定一个\n - --uuid、--idempotency-key 互斥,最多指定一个\n - 目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持" }, "effect": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "write" @@ -74103,7 +76153,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "write" }, @@ -74111,7 +76161,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ @@ -74122,7 +76172,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ "dws chat +messages-send --as user --chat-id \u003copenConversationId\u003e --markdown \"## 周报\" --idempotency-key \u003ckey\u003e", @@ -74133,7 +76183,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "unknown" @@ -74147,7 +76197,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "unknown" }, @@ -74155,7 +76205,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" @@ -74163,7 +76213,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" }, @@ -74171,17 +76221,17 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow." + "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow." } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow." + "value": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow." }, "interface_ref": { "candidates": [ @@ -74203,14 +76253,14 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": true }, { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "selected": false, "source": "internal/cli/schema_hints/selection/chat.json", "value": true @@ -74218,7 +76268,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": true }, @@ -74226,7 +76276,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "medium" @@ -74240,7 +76290,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "medium" }, @@ -74250,42 +76300,42 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" + "value": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" + "value": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片" }, "use_when": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" ] } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" ] } }, "has_parameters": true, "idempotency": "unknown", "interface_mode": "composite", - "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow.", + "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow.", "is_alias": false, "name": "shortcut_messages_send", - "parameter_count": 24, + "parameter_count": 26, "parameters": { "ai-tag": { "default": "true", @@ -74915,6 +76965,90 @@ "required": false, "type": "string" }, + "chat-query": { + "description": "按群名解析唯一群聊(仅 user 的高级发送场景);受发送身份能力矩阵约束", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按群名解析唯一群聊(仅 user 的高级发送场景);受发送身份能力矩阵约束" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按群名解析唯一群聊(仅 user 的高级发送场景);受发送身份能力矩阵约束" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "chatQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "chatQuery" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "chatQuery", + "required": false, + "type": "string" + }, "file": { "description": "工作目录内安全相对文件路径(仅 user 的 file/audio/video)", "field_provenance": { @@ -76203,6 +78337,90 @@ "required": false, "type": "string" }, + "user-query": { + "description": "按姓名解析唯一 openDingTalkId(仅 user 的高级发送场景);受发送身份能力矩阵约束", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按姓名解析唯一 openDingTalkId(仅 user 的高级发送场景);受发送身份能力矩阵约束" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按姓名解析唯一 openDingTalkId(仅 user 的高级发送场景);受发送身份能力矩阵约束" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "userQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "userQuery" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "userQuery", + "required": false, + "type": "string" + }, "users": { "description": "批量单聊接收者 userId(bot);受发送身份能力矩阵约束", "field_provenance": { @@ -76462,9 +78680,9 @@ "reviewed": true, "risk": "medium", "source": "reviewed_command_registry", - "title": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "title": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", "use_when": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" ] }, "chat.shortcut_messages_send_by_webhook": { @@ -79331,7 +81549,7 @@ "internal/cli/schema_hints/selection/chat.json", "skills/mono/references/intent-guide.md" ], - "agent_summary": "多维搜索消息,可全量翻页并批量富化详情", + "agent_summary": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "agent_summary_source": "dws-agent-selection/chat", "availability": "available", "avoid_when": [ @@ -79374,8 +81592,10 @@ "group", "groups", "chat-id", + "chat-query", "senders", "sender", + "sender-query", "at-me", "is-at-me", "at-ids", @@ -79386,7 +81606,7 @@ ] ] }, - "description": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", + "description": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸", "display": "群聊 / 消息 / 机器人", "effect": "read", "effect_source": "agent-hint", @@ -79399,23 +81619,23 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", - "value": "多维搜索消息,可全量翻页并批量富化详情" + "value": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化" } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "source": "internal/cli/schema_hints/selection/chat.json", - "value": "多维搜索消息,可全量翻页并批量富化详情" + "value": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化" }, "availability": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" @@ -79423,7 +81643,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "available" }, @@ -79431,7 +81651,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ @@ -79441,7 +81661,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID 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" @@ -79467,7 +81687,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "not_required" @@ -79475,7 +81695,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "not_required" }, @@ -79485,19 +81705,19 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" + "value": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" + "value": "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。\n\n参数约束:\n - 至少指定一个内容、身份、会话或消息类型过滤条件\n - 需与 --end 一起传\n - 需与 --start 一起传\n - --groups、--chat-id 互斥,最多指定一个\n - --senders、--sender 互斥,最多指定一个\n - --at-me、--is-at-me 互斥,最多指定一个\n - --conversation-type、--chat-type 互斥,最多指定一个\n - --limit、--page-size 互斥,最多指定一个\n - --cursor、--page-token 互斥,最多指定一个\n - --output-dir 必须是工作目录内的相对路径,不允许绝对路径或 .. 逃逸" }, "effect": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "read" @@ -79505,7 +81725,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "read" }, @@ -79513,7 +81733,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ @@ -79524,7 +81744,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ "dws chat +search-msg --query \"周报\" --senders \u003copenDingTalkId\u003e --days 3 --page-all", @@ -79535,7 +81755,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "idempotent" @@ -79543,7 +81763,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "idempotent" }, @@ -79551,7 +81771,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" @@ -79559,7 +81779,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "composite" }, @@ -79567,17 +81787,17 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads." + "value": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting." } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads." + "value": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting." }, "interface_ref": { "candidates": [ @@ -79599,14 +81819,14 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": true }, { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "selected": false, "source": "internal/cli/schema_hints/selection/chat.json", "value": true @@ -79614,7 +81834,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": true }, @@ -79622,7 +81842,7 @@ "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", "value": "low" @@ -79630,7 +81850,7 @@ ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", "value": "low" }, @@ -79640,42 +81860,42 @@ "precedence": "cobra_help", "selected": true, "source": "cobra_help", - "value": "多维搜索消息,可全量翻页并批量富化详情" + "value": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化" } ], "precedence": "cobra_help", "resolution": "highest_precedence", "source": "cobra_help", - "value": "多维搜索消息,可全量翻页并批量富化详情" + "value": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化" }, "use_when": { "candidates": [ { "precedence": "reviewed_explicit", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "selected": true, "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ] } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "source": "internal/cli/schema_hints/selection/chat.json", "value": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ] } }, "has_parameters": true, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.", + "interface_reason": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting.", "is_alias": false, "name": "shortcut_search_msg", - "parameter_count": 27, + "parameter_count": 29, "parameters": { "at-ids": { "description": "@对象 userId/openDingTalkId 列表", @@ -79947,6 +82167,96 @@ "required": false, "type": "array" }, + "chat-query": { + "description": "按群名解析一个或多个唯一 openConversationId", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按群名解析一个或多个唯一 openConversationId" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按群名解析一个或多个唯一 openConversationId" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "chatQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "chatQuery" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "array" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "array" + } + }, + "property": "chatQuery", + "required": false, + "type": "array" + }, "chat-type": { "description": "--conversation-type 的 lark-cli 对齐别名", "field_provenance": { @@ -81917,6 +84227,96 @@ "required": false, "type": "array" }, + "sender-query": { + "description": "按姓名解析一个或多个唯一发送者", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按姓名解析一个或多个唯一发送者" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按姓名解析一个或多个唯一发送者" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "senderQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "senderQuery" + }, + "required": { + "candidates": [ + { + "precedence": "command_constraint", + "selected": true, + "source": "require_one_of_constraint", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "command_constraint", + "resolution": "highest_precedence", + "source": "require_one_of_constraint", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "array" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "array" + } + }, + "property": "senderQuery", + "required": false, + "type": "array" + }, "senders": { "description": "发送者 userId/openDingTalkId 列表", "field_provenance": { @@ -82119,9 +84519,9 @@ "reviewed": true, "risk": "low", "source": "reviewed_command_registry", - "title": "多维搜索消息,可全量翻页并批量富化详情", + "title": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "use_when": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ] }, "chat.shortcut_send_to_group": { @@ -82944,14 +85344,14 @@ "review_reason": "Reviewed against the executable thread/topic alias contract and real list_topic_replies mapping. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "selected": true, "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely." + "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness." } ], "precedence": "reviewed_explicit", "resolution": "highest_precedence", "review_reason": "Reviewed against the executable thread/topic alias contract and real list_topic_replies mapping. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "source": "internal/cli/schema_hints/metadata/chat.json", - "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely." + "value": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness." }, "interface_ref": { "candidates": [ @@ -83046,7 +85446,7 @@ "has_parameters": true, "idempotency": "idempotent", "interface_mode": "composite", - "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.", + "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness.", "is_alias": false, "name": "shortcut_thread_replies", "parameter_count": 9, diff --git a/internal/cli/schema_catalog/tools/event.json b/internal/cli/schema_catalog/tools/event.json index 62d877f5..834fe346 100644 --- a/internal/cli/schema_catalog/tools/event.json +++ b/internal/cli/schema_catalog/tools/event.json @@ -3430,6 +3430,1353 @@ "尚不知道可用 event_key,需要先盘点个人事件目录" ] }, + "event.listen_im": { + "agent_metadata_source": "embedded-skill-metadata", + "agent_source_refs": [ + "cobra-help:dws event +listen-im", + "internal/app/event_listen_im.go", + "internal/cli/schema_command_registry/products/event.json#event.listen_im", + "internal/cli/schema_hints/metadata/event.json", + "internal/cli/schema_hints/selection/event.json", + "internal/event/personal/registry.go" + ], + "agent_summary": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件", + "agent_summary_source": "dws-agent-selection/event", + "availability": "available", + "avoid_when": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ], + "canonical_path": "event.listen_im", + "cli_name": "+listen-im", + "cli_path": "event +listen-im", + "confirmation": "not_required", + "constraints": { + "mutually_exclusive": [ + [ + "user", + "open-dingtalk-id", + "user-query", + "chat-id", + "chat-query" + ] + ] + }, + "description": "把 @我、指定发送人、指定群、全部单聊或全部群聊等用户意图确定性编译为个人 EventKey,自然姓名/群名会先唯一解析,再复用 event consume 的订阅、ready marker、NDJSON、取消、回滚和清理生命周期。", + "display": "事件订阅 (DingTalk Stream 长连接)", + "effect": "write", + "effect_source": "agent-hint", + "examples": [ + "dws event +listen-im --kind at-me --max-events 1", + "dws event +listen-im --kind group --events message,reaction --chat-id \u003copenConversationId\u003e --duration 10m" + ], + "field_provenance": { + "agent_summary": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "selected": true, + "source": "internal/cli/schema_hints/selection/event.json", + "value": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "source": "internal/cli/schema_hints/selection/event.json", + "value": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件" + }, + "availability": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "available" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "available" + }, + "avoid_when": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "selected": true, + "source": "internal/cli/schema_hints/selection/event.json", + "value": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "source": "internal/cli/schema_hints/selection/event.json", + "value": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ] + }, + "canonical_path": { + "candidates": [ + { + "precedence": "command_registry", + "selected": true, + "source": "reviewed_command_registry", + "source_ref": "event +listen-im", + "value": "event.listen_im" + } + ], + "precedence": "command_registry", + "resolution": "registry_identity", + "source": "reviewed_command_registry", + "source_ref": "event +listen-im", + "value": "event.listen_im" + }, + "confirmation": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "not_required" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "not_required" + }, + "description": { + "candidates": [ + { + "precedence": "cobra_help", + "selected": true, + "source": "cobra_help", + "value": "把 @我、指定发送人、指定群、全部单聊或全部群聊等用户意图确定性编译为个人 EventKey,自然姓名/群名会先唯一解析,再复用 event consume 的订阅、ready marker、NDJSON、取消、回滚和清理生命周期。" + } + ], + "precedence": "cobra_help", + "resolution": "highest_precedence", + "source": "cobra_help", + "value": "把 @我、指定发送人、指定群、全部单聊或全部群聊等用户意图确定性编译为个人 EventKey,自然姓名/群名会先唯一解析,再复用 event consume 的订阅、ready marker、NDJSON、取消、回滚和清理生命周期。" + }, + "effect": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "write" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "write" + }, + "examples": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "selected": true, + "source": "internal/cli/schema_hints/selection/event.json", + "value": [ + "dws event +listen-im --kind at-me --max-events 1", + "dws event +listen-im --kind group --events message,reaction --chat-id \u003copenConversationId\u003e --duration 10m" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "source": "internal/cli/schema_hints/selection/event.json", + "value": [ + "dws event +listen-im --kind at-me --max-events 1", + "dws event +listen-im --kind group --events message,reaction --chat-id \u003copenConversationId\u003e --duration 10m" + ] + }, + "idempotency": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "non_idempotent" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "non_idempotent" + }, + "interface_mode": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "composite" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "composite" + }, + "interface_reason": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle." + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle." + }, + "interface_ref": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": null + } + ], + "precedence": "reviewed_explicit", + "resolution": "interface_disposition_matrix", + "review_reason": "final interface mode composite forbids a direct MCP interface_ref", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": null + }, + "reviewed": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": true + }, + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "selected": false, + "source": "internal/cli/schema_hints/selection/event.json", + "value": true + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": true + }, + "risk": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "selected": true, + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "medium" + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "source": "internal/cli/schema_hints/metadata/event.json", + "value": "medium" + }, + "title": { + "candidates": [ + { + "precedence": "cobra_help", + "selected": true, + "source": "cobra_help", + "value": "按 IM 意图解析目标并监听一个或多个个人消息事件" + } + ], + "precedence": "cobra_help", + "resolution": "highest_precedence", + "source": "cobra_help", + "value": "按 IM 意图解析目标并监听一个或多个个人消息事件" + }, + "use_when": { + "candidates": [ + { + "precedence": "reviewed_explicit", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "selected": true, + "source": "internal/cli/schema_hints/selection/event.json", + "value": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ] + } + ], + "precedence": "reviewed_explicit", + "resolution": "highest_precedence", + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "source": "internal/cli/schema_hints/selection/event.json", + "value": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ] + } + }, + "has_parameters": true, + "idempotency": "non_idempotent", + "interface_mode": "composite", + "interface_reason": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle.", + "is_alias": false, + "name": "listen_im", + "parameter_count": 11, + "parameters": { + "chat-id": { + "description": "指定群 openConversationId", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "指定群 openConversationId" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "指定群 openConversationId" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "chatId" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "chatId" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "chatId", + "required": false, + "type": "string" + }, + "chat-query": { + "description": "按群名唯一解析指定群", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按群名唯一解析指定群" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按群名唯一解析指定群" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "chatQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "chatQuery" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "chatQuery", + "required": false, + "type": "string" + }, + "dry-run": { + "description": "解析目标并打印订阅计划,不创建订阅或连接 bus", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "解析目标并打印订阅计划,不创建订阅或连接 bus" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "解析目标并打印订阅计划,不创建订阅或连接 bus" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "dryRun" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "dryRun" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "boolean" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "boolean" + } + }, + "property": "dryRun", + "required": false, + "type": "boolean" + }, + "duration": { + "description": "运行时长上限 (Go duration,如 30s/5m;0 = 不限)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "运行时长上限 (Go duration,如 30s/5m;0 = 不限)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "运行时长上限 (Go duration,如 30s/5m;0 = 不限)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "duration" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "duration" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "duration", + "required": false, + "type": "string" + }, + "events": { + "default": "[message]", + "description": "事件种类: message,reaction,read,recall", + "enum": [ + "message", + "reaction", + "read", + "recall" + ], + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "事件种类: message,reaction,read,recall" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "事件种类: message,reaction,read,recall" + }, + "enum": { + "candidates": [ + { + "precedence": "native_annotation", + "selected": true, + "source": "native_annotation", + "value": [ + "message", + "reaction", + "read", + "recall" + ] + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": [] + } + ], + "precedence": "native_annotation", + "resolution": "highest_precedence", + "source": "native_annotation", + "value": [ + "message", + "reaction", + "read", + "recall" + ] + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "events" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "events" + }, + "required": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_nonzero_default", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_nonzero_default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "array" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "array" + } + }, + "property": "events", + "required": false, + "type": "array" + }, + "kind": { + "default": "at-me", + "description": "监听意图: at-me|sender|group|all-direct|all-group", + "enum": [ + "at-me", + "sender", + "group", + "all-direct", + "all-group" + ], + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "监听意图: at-me|sender|group|all-direct|all-group" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "监听意图: at-me|sender|group|all-direct|all-group" + }, + "enum": { + "candidates": [ + { + "precedence": "native_annotation", + "selected": true, + "source": "native_annotation", + "value": [ + "at-me", + "sender", + "group", + "all-direct", + "all-group" + ] + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": [] + } + ], + "precedence": "native_annotation", + "resolution": "highest_precedence", + "source": "native_annotation", + "value": [ + "at-me", + "sender", + "group", + "all-direct", + "all-group" + ] + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "kind" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "kind" + }, + "required": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_nonzero_default", + "value": false + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": false + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_nonzero_default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "kind", + "required": false, + "type": "string" + }, + "max-events": { + "description": "收到 N 条后退出 (0 = 不限)", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "收到 N 条后退出 (0 = 不限)" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "收到 N 条后退出 (0 = 不限)" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "maxEvents" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "maxEvents" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "integer" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "integer" + } + }, + "property": "maxEvents", + "required": false, + "type": "integer" + }, + "open-dingtalk-id": { + "description": "指定发送人/单聊对端 openDingTalkId", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "指定发送人/单聊对端 openDingTalkId" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "指定发送人/单聊对端 openDingTalkId" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "openDingtalkId" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "openDingtalkId" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "openDingtalkId", + "required": false, + "type": "string" + }, + "query": { + "description": "消息文本关键词过滤,逗号分隔;仅 message 事件", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "消息文本关键词过滤,逗号分隔;仅 message 事件" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "消息文本关键词过滤,逗号分隔;仅 message 事件" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "query" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "query" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "query", + "required": false, + "type": "string" + }, + "user": { + "description": "指定发送人/单聊对端 userId", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "指定发送人/单聊对端 userId" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "指定发送人/单聊对端 userId" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "user" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "user" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "user", + "required": false, + "type": "string" + }, + "user-query": { + "description": "按姓名/花名唯一解析指定发送人", + "field_provenance": { + "description": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_usage", + "value": "按姓名/花名唯一解析指定发送人" + }, + { + "precedence": "default", + "selected": false, + "source": "default", + "value": "" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_usage", + "value": "按姓名/花名唯一解析指定发送人" + }, + "property": { + "candidates": [ + { + "precedence": "inference", + "selected": true, + "source": "flag_name_inference", + "value": "userQuery" + } + ], + "precedence": "inference", + "resolution": "highest_precedence", + "source": "flag_name_inference", + "value": "userQuery" + }, + "required": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": false + } + ], + "precedence": "default", + "resolution": "fallback", + "source": "default", + "value": false + }, + "required_when": { + "candidates": [ + { + "precedence": "default", + "selected": true, + "source": "default", + "value": "" + } + ], + "precedence": "default", + "resolution": "highest_precedence", + "source": "default", + "value": "" + }, + "type": { + "candidates": [ + { + "precedence": "cobra_contract", + "selected": true, + "source": "cobra_flag_type", + "value": "string" + } + ], + "precedence": "cobra_contract", + "resolution": "highest_precedence", + "source": "cobra_flag_type", + "value": "string" + } + }, + "property": "userQuery", + "required": false, + "type": "string" + } + }, + "path": "event.listen_im", + "primary_cli_path": "event +listen-im", + "product_id": "event", + "reviewed": true, + "risk": "medium", + "source": "reviewed_command_registry", + "title": "按 IM 意图解析目标并监听一个或多个个人消息事件", + "use_when": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ] + }, "event.schema": { "agent_metadata_source": "embedded-skill-metadata", "agent_source_refs": [ diff --git a/internal/cli/schema_command_exclusions.json b/internal/cli/schema_command_exclusions.json index 50b8707b..ffdbeeae 100644 --- a/internal/cli/schema_command_exclusions.json +++ b/internal/cli/schema_command_exclusions.json @@ -164,7 +164,6 @@ "chat +category-remove-conversation", "chat +chat-add-bot", "chat +chat-audit-join", - "chat +chat-create", "chat +chat-get-by-id", "chat +chat-members-get", "chat +chat-members-list", @@ -201,7 +200,6 @@ "chat +messages-recall-by-bot", "chat +messages-remove-emoji", "chat +messages-remove-text-emotion", - "chat +messages-reply", "chat +messages-resource-download", "chat +messages-resource-url", "chat +messages-send-by-bot", diff --git a/internal/cli/schema_command_registry/products/chat.json b/internal/cli/schema_command_registry/products/chat.json index 91559ada..89bba5db 100644 --- a/internal/cli/schema_command_registry/products/chat.json +++ b/internal/cli/schema_command_registry/products/chat.json @@ -497,10 +497,18 @@ "canonical_path": "chat.set_top_conversation", "cli_path": "chat set-top" }, + { + "canonical_path": "chat.shortcut_chat_create", + "cli_path": "chat +chat-create" + }, { "canonical_path": "chat.shortcut_chat_messages", "cli_path": "chat +chat-messages" }, + { + "canonical_path": "chat.shortcut_messages_reply", + "cli_path": "chat +messages-reply" + }, { "canonical_path": "chat.shortcut_messages_send", "cli_path": "chat +messages-send" diff --git a/internal/cli/schema_command_registry/products/event.json b/internal/cli/schema_command_registry/products/event.json index 8c0d1b35..af21fb87 100644 --- a/internal/cli/schema_command_registry/products/event.json +++ b/internal/cli/schema_command_registry/products/event.json @@ -1,6 +1,10 @@ { "id": "event", "tools": [ + { + "canonical_path": "event.listen_im", + "cli_path": "event +listen-im" + }, { "canonical_path": "event.consume", "cli_path": "event consume" diff --git a/internal/cli/schema_hints/metadata/chat.json b/internal/cli/schema_hints/metadata/chat.json index 21ee7b18..a772ff8f 100644 --- a/internal/cli/schema_hints/metadata/chat.json +++ b/internal/cli/schema_hints/metadata/chat.json @@ -1152,6 +1152,19 @@ "cli_path": "chat +chat-role-query-user", "runtime_gate": "none" }, + "chat.shortcut_chat_create": { + "effect": "write", + "risk": "medium", + "confirmation": "user_required", + "idempotency": "unknown", + "interface_mode": "composite", + "availability": "available", + "interface_reason": "Reviewed create-chat adapter: it batch-resolves every natural member query, reads the current profile to guarantee owner membership, de-duplicates stable member identities, creates one group, and normalizes the public conversation identity.", + "reviewed": true, + "review_reason": "Reviewed against the executable ID/query member contract, all-or-nothing structured resolution failures, dry-run resolution parity, current-owner precondition, and single create_group_conversation write. It intentionally does not invent unsupported owner, description, initial-bot, or Lark visibility semantics.", + "cli_path": "chat +chat-create", + "runtime_gate": "typed_yes" + }, "chat.shortcut_chat_messages": { "effect": "read", "risk": "low", @@ -1159,12 +1172,25 @@ "idempotency": "idempotent", "interface_mode": "composite", "availability": "available", - "interface_reason": "Reviewed built-in Shortcut adapter: it routes group or direct-message history reads, projects a stable message shape, and optionally orchestrates safe resource downloads with a failure ledger.", + "interface_reason": "Reviewed built-in Shortcut adapter: it accepts a stable group/user identity or deterministically resolves one natural group/user query, routes the resulting group or direct-message history read, projects the additive im.message-list.v1 shape, publishes proof-oriented pagination facts, and optionally orchestrates safe resource downloads whose failures fold into task completeness.", "reviewed": true, - "review_reason": "Reviewed against the executable Shortcut and group/direct lower read tools. Optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", + "review_reason": "Reviewed against the executable Shortcut, structured zero/multiple-candidate resolution failures, and group/direct lower read tools. Ambiguity stops before the history read; optional downloads stay read/not_required under the existing safe local-download convention: workspace-relative paths, no overwrite by default, and atomic publication.", "cli_path": "chat +chat-messages", "runtime_gate": "none" }, + "chat.shortcut_messages_reply": { + "effect": "write", + "risk": "medium", + "confirmation": "user_required", + "idempotency": "unknown", + "interface_mode": "composite", + "availability": "available", + "interface_reason": "Reviewed quote-reply adapter: it resolves or reads the referenced sender before one personal-message write, constructs DingTalk's reply content, and enriches the successful response with stable continuation context while preserving the lower payload.", + "reviewed": true, + "review_reason": "Reviewed against explicit/open identity resolution, exact referenced-message lookup, no-write dry-run behavior, optional idempotency key, single send_personal_message write, and additive im.message-reply.v1 result fields. Thread reply is intentionally not advertised because no verified writer exists.", + "cli_path": "chat +messages-reply", + "runtime_gate": "typed_yes" + }, "chat.shortcut_messages_send": { "effect": "write", "risk": "medium", @@ -1172,9 +1198,9 @@ "idempotency": "unknown", "interface_mode": "composite", "availability": "available", - "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports live-compatible contact search with exact userId matching, mediaId images, and the native init/upload/commit local-file flow.", + "interface_reason": "Reviewed composite send adapter: it selects current-user, bot, or webhook transport; current-user additionally supports deterministic user/chat query resolution, live-compatible exact userId conversion, mediaId images, and the native init/upload/commit local-file flow.", "reviewed": true, - "review_reason": "Reviewed against every executable identity/content/target validation branch, authenticated contact-search response shape, and the real lower tool mappings; bot and webhook media are intentionally rejected because no equivalent lower capability exists.", + "review_reason": "Reviewed against every executable identity/content/target validation branch, structured ambiguity handling, dry-run resolution parity, authenticated contact/group search response shapes, and the real lower tool mappings; bot and webhook natural targets or media are intentionally rejected because no equivalent contract exists.", "cli_path": "chat +messages-send", "runtime_gate": "typed_yes" }, @@ -1243,9 +1269,9 @@ "idempotency": "idempotent", "interface_mode": "composite", "availability": "available", - "interface_reason": "Reviewed built-in shortcut adapter: the executable CLI owns validation, optional multi-step orchestration, output projection, and confirmation; the complete command contract is not represented by one pinned MCP interface_ref.", + "interface_reason": "Reviewed exact-ID message reader: it validates and de-duplicates up to 50 IDs, projects them through the shared additive im.message-list.v1 builder, accounts for every missing ID, and optionally downloads resources with task-level failure accounting.", "reviewed": true, - "review_reason": "Reviewed against the built-in Shortcut registry and mounted Cobra command. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable ID bounds, shared identity/context/reaction/quote/forward/resource projection, missing-ID completeness ledger, and safe relative-path download workflow. Downloads stay read/not_required and any failure preserves messages while setting complete=false.", "cli_path": "chat +messages-mget", "runtime_gate": "none" }, @@ -1373,9 +1399,9 @@ "idempotency": "idempotent", "interface_mode": "composite", "availability": "available", - "interface_reason": "Reviewed search adapter: it combines filters, cursor pagination, batched mget enrichment, stable projection, completeness accounting, and optional safe resource downloads.", + "interface_reason": "Reviewed search adapter: it batch-resolves natural group and sender queries before search, combines resolved stable IDs with explicit filters, cursor pagination and batched mget enrichment, projects im.message-list.v1, and folds search/enrichment/resource failures into completeness accounting.", "reviewed": true, - "review_reason": "Reviewed against the executable advanced-search flags plus page and enrichment ledgers. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", + "review_reason": "Reviewed against the executable advanced-search flags, all-or-nothing structured target resolution, and page/enrichment ledgers. Any unresolved or ambiguous query prevents the search call; optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "cli_path": "chat +search-msg", "runtime_gate": "none" }, @@ -1399,7 +1425,7 @@ "idempotency": "idempotent", "interface_mode": "composite", "availability": "available", - "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects stable message fields, and optionally downloads reply resources safely.", + "interface_reason": "Reviewed thread reader adapter: it accepts threadId/topicId, reads lower topic replies, projects the shared additive im.message-list.v1 fields with proof-oriented pagination, and optionally downloads reply resources whose failures fold into task completeness.", "reviewed": true, "review_reason": "Reviewed against the executable thread/topic alias contract and real list_topic_replies mapping. Optional downloads stay read/not_required under the existing safe local-download convention and therefore do not create a non-interactive confirmation no-op.", "cli_path": "chat +thread-replies", diff --git a/internal/cli/schema_hints/metadata/event.json b/internal/cli/schema_hints/metadata/event.json index 414a8b9d..ccf972bd 100644 --- a/internal/cli/schema_hints/metadata/event.json +++ b/internal/cli/schema_hints/metadata/event.json @@ -8,6 +8,18 @@ "reviewed": true }, "tools": { + "event.listen_im": { + "effect": "write", + "risk": "medium", + "confirmation": "not_required", + "idempotency": "non_idempotent", + "interface_mode": "composite", + "availability": "available", + "interface_reason": "Reviewed IM event facade: it deterministically maps kind/events to public personal EventKeys, resolves one natural user/chat target with the shared typed resolver, then delegates one single- or multi-event invocation to the existing subscription, bus, ready-marker, NDJSON, rollback, cancellation, and cleanup lifecycle.", + "reviewed": true, + "review_reason": "Reviewed against every kind/event mapping and conditional target branch, structured ambiguity/no-match behavior, same-profile read resolution, all-or-nothing multi-subscription preparation, dry-run resolution parity, and reuse of runPersonalEventConsume. It does not create a second event system or expose unverified group lifecycle mappings.", + "runtime_gate": "none" + }, "event.consume": { "effect": "write", "risk": "medium", diff --git a/internal/cli/schema_hints/runtime-surface-completeness.json b/internal/cli/schema_hints/runtime-surface-completeness.json index e80c4705..03196388 100644 --- a/internal/cli/schema_hints/runtime-surface-completeness.json +++ b/internal/cli/schema_hints/runtime-surface-completeness.json @@ -7,7 +7,7 @@ "channel": "open-source" }, "coverage": { - "source_tools": 845, + "source_tools": 848, "matched_tools": 71 }, "tools": { diff --git a/internal/cli/schema_hints/selection/chat.json b/internal/cli/schema_hints/selection/chat.json index bd250c60..f22a2679 100644 --- a/internal/cli/schema_hints/selection/chat.json +++ b/internal/cli/schema_hints/selection/chat.json @@ -2110,10 +2110,30 @@ "ShortcutRegistry:chat +chat-role-query-user" ] }, - "chat.shortcut_chat_messages": { - "agent_summary": "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", + "chat.shortcut_chat_create": { + "agent_summary": "按成员 ID 或姓名全量预检后创建一个钉钉群聊", "use_when": [ - "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。" + ], + "avoid_when": [ + "需要指定其他 owner、群 description、初始机器人、幂等查重或 Lark public/private 可见性时不要使用;这些语义当前没有可靠底层能力" + ], + "examples": [ + "dws chat +chat-create --name \"项目冲刺群\" --users ,", + "dws chat +chat-create --name \"合作群\" --member-query \"张三,李四\" --type EXTERNAL" + ], + "reviewed": true, + "review_reason": "Agent-authored from the executable all-or-nothing natural member resolver, stable-ID member path, current-owner invariant, single-write boundary, dry-run parity, and deliberately unsupported Lark-only creation fields.", + "source_refs": [ + "internal/cli/schema_command_registry/products/chat.json#chat.shortcut_chat_create", + "cobra-help:dws chat +chat-create", + "ShortcutRegistry:chat +chat-create" + ] + }, + "chat.shortcut_chat_messages": { + "agent_summary": "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", + "use_when": [ + "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。" ], "avoid_when": [ "要跨多个会话按关键词、发送者或消息类型检索时使用 +search-msg;已有一批精确消息 ID 时使用 +messages-mget" @@ -2123,27 +2143,46 @@ "dws chat +chat-messages --open-dingtalk-id --download-resources --output-dir ./downloads" ], "reviewed": true, - "review_reason": "Agent-authored from the verified group/direct routing, stable projection, pagination, and optional safe resource workflow; it distinguishes conversation history from cross-conversation search and exact-ID mget.", + "review_reason": "Agent-authored from the verified stable-ID and deterministic natural-target group/direct routing, structured ambiguity behavior, stable projection, pagination, and optional safe resource workflow; it distinguishes one-conversation history from cross-conversation search and exact-ID mget.", "source_refs": [ "internal/cli/schema_command_registry.json#chat.shortcut_chat_messages", "cobra-help:dws chat +chat-messages", "ShortcutRegistry:chat +chat-messages" ] }, - "chat.shortcut_messages_send": { - "agent_summary": "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "chat.shortcut_messages_reply": { + "agent_summary": "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", "use_when": [ - "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。" + "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。" ], "avoid_when": [ - "需要 bot/webhook 发送媒体、卡片或 thread 回复时不要假设等价支持;改用真实存在的专用下层命令,缺少下层能力时停止" + "要在话题 Thread 内新增回复、用 bot 身份回复、发送富媒体或卡片时不要使用;当前入口只实现普通引用回复,不猜测不存在的 Thread writer" + ], + "examples": [ + "dws chat +messages-reply --conversation-id --message-id --text \"收到\" --idempotency-key " + ], + "reviewed": true, + "review_reason": "Agent-authored from the verified pre-write sender lookup, plain-text quote contract, dry-run no-write behavior, optional idempotency, additive continuation DTO, and explicit exclusion of unverified Thread writing.", + "source_refs": [ + "internal/cli/schema_command_registry/products/chat.json#chat.shortcut_messages_reply", + "cobra-help:dws chat +messages-reply", + "ShortcutRegistry:chat +messages-reply" + ] + }, + "chat.shortcut_messages_send": { + "agent_summary": "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + "use_when": [ + "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。" + ], + "avoid_when": [ + "只需按姓名或群名发送简单文本时分别使用 +dm 或 +send-to-group;bot/webhook 不支持自然目标或媒体,卡片和 thread 回复使用真实存在的专用入口" ], "examples": [ "dws chat +messages-send --as user --chat-id --markdown \"## 周报\" --idempotency-key ", "dws chat +messages-send --as user --user --msg-type file --file ./report.pdf" ], "reviewed": true, - "review_reason": "Agent-authored from the verified identity/content capability matrix, live-compatible contact search with exact userId matching, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook media.", + "review_reason": "Agent-authored from the verified Golden Route boundary, identity/content capability matrix, structured user/chat resolution with dry-run parity, native local-file upload flow, and fail-closed rejection of unsupported bot/webhook natural targets or media.", "source_refs": [ "internal/cli/schema_command_registry.json#chat.shortcut_messages_send", "cobra-help:dws chat +messages-send", @@ -2362,7 +2401,7 @@ ] }, "chat.shortcut_dm": { - "agent_summary": "按姓名直接给某人发单聊消息(自动解析 userId)", + "agent_summary": "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", "use_when": [ "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。" ], @@ -2420,9 +2459,9 @@ ] }, "chat.shortcut_search_msg": { - "agent_summary": "多维搜索消息,可全量翻页并批量富化详情", + "agent_summary": "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", "use_when": [ - "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。" + "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query,多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。--download-resources 使用安全本地路径、默认不覆盖和原子落盘。" ], "avoid_when": [ "只想读取一个已知会话的连续历史时使用 +chat-messages;已有精确消息 ID 时使用 +messages-mget" @@ -2432,7 +2471,7 @@ "dws chat +search-msg --group --message-type file --download-resources --output-dir ./downloads" ], "reviewed": true, - "review_reason": "Agent-authored from the verified filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", + "review_reason": "Agent-authored from the verified all-or-nothing natural group/sender resolution, stable-ID filter mapping, cursor pagination, batched mget enrichment, completeness ledger, and shared safe resource workflow.", "source_refs": [ "internal/cli/schema_command_registry.json#chat.shortcut_search_msg", "cobra-help:dws chat +search-msg", diff --git a/internal/cli/schema_hints/selection/event.json b/internal/cli/schema_hints/selection/event.json index ceb03b48..5d52fb07 100644 --- a/internal/cli/schema_hints/selection/event.json +++ b/internal/cli/schema_hints/selection/event.json @@ -8,6 +8,27 @@ "reviewed": true }, "tools": { + "event.listen_im": { + "agent_summary": "按 @我、姓名、群名或全量范围监听一个或多个 IM 消息事件", + "use_when": [ + "已知要监听 @我、指定发送人、指定群、全部单聊或全部群聊的 message/reaction/read/recall 事件时使用;姓名用 --user-query、群名用 --chat-query,CLI 会唯一解析目标并把多个兼容事件合并到一个消费生命周期。" + ], + "avoid_when": [ + "需要群标题/成员/解散等生命周期事件、显式 EventKey、复用 subscribe_id、Filter DSL、原始 transport envelope 或其它底层 consume 控制时使用 event consume;只查历史消息时使用 chat 查询入口" + ], + "examples": [ + "dws event +listen-im --kind at-me --max-events 1", + "dws event +listen-im --kind group --events message,reaction --chat-id --duration 10m" + ], + "reviewed": true, + "review_reason": "Agent-authored from the verified deterministic mapping table, typed natural target resolver, one-process multi-event delegation, flatten/NDJSON defaults, bounded execution flags, and explicit fallback boundary to low-level event consume.", + "source_refs": [ + "internal/cli/schema_command_registry/products/event.json#event.listen_im", + "cobra-help:dws event +listen-im", + "internal/event/personal/registry.go", + "internal/app/event_listen_im.go" + ] + }, "event.consume": { "agent_summary": "订阅并持续消费一个或多个兼容的个人事件;Agent 使用 --flatten 输出顶层业务 NDJSON", "use_when": [ diff --git a/internal/errors/errors.go b/internal/errors/errors.go index 0514cac6..b657e6d0 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -55,6 +55,7 @@ type Error struct { Actions []string AvailableFlags []string Snapshot string + Details map[string]any RPCCode int `json:"rpc_code,omitempty"` RPCData json.RawMessage `json:"rpc_data,omitempty"` ServerDiag ServerDiagnostics `json:"-"` @@ -186,6 +187,21 @@ func WithSnapshot(path string) Option { } } +// WithDetails records an additive machine-readable payload for errors whose +// recovery needs typed context, such as ambiguous target-resolution +// candidates. Callers must keep credentials and other secrets out of details. +func WithDetails(details map[string]any) Option { + return func(err *Error) { + if len(details) == 0 { + return + } + err.Details = make(map[string]any, len(details)) + for key, value := range details { + err.Details[key] = value + } + } +} + // WithRPCCode records the original JSON-RPC error code. func WithRPCCode(code int) Option { return func(err *Error) { @@ -316,6 +332,9 @@ func PrintJSON(w io.Writer, err error) error { if typed.Snapshot != "" { errorPayload["snapshot_path"] = typed.Snapshot } + if len(typed.Details) > 0 { + errorPayload["details"] = typed.Details + } if typed.RPCCode != 0 { errorPayload["rpc_code"] = typed.RPCCode } diff --git a/internal/errors/errors_test.go b/internal/errors/errors_test.go index e09bfdb1..2fd70f66 100644 --- a/internal/errors/errors_test.go +++ b/internal/errors/errors_test.go @@ -53,6 +53,10 @@ func TestPrintJSON(t *testing.T) { WithRetryable(true), WithActions("dws schema doc.create_document", "retry command"), WithSnapshot("/tmp/dws-recovery/snapshot.json"), + WithDetails(map[string]any{ + "type": "resolution", + "query": "项目群", + }), )); err != nil { t.Fatalf("PrintJSON() error = %v", err) } @@ -76,6 +80,9 @@ func TestPrintJSON(t *testing.T) { if !strings.Contains(got, "\"snapshot_path\": \"/tmp/dws-recovery/snapshot.json\"") { t.Fatalf("expected snapshot path in output, got %q", got) } + if !strings.Contains(got, "\"type\": \"resolution\"") || !strings.Contains(got, "\"query\": \"项目群\"") { + t.Fatalf("expected structured details in output, got %q", got) + } } func TestCrossPlatformCoverageRetryabilityTriStateAndRetryTiming(t *testing.T) { diff --git a/internal/shortcut/chat/chat_message.go b/internal/shortcut/chat/chat_message.go index 4ec4aa8e..84474644 100644 --- a/internal/shortcut/chat/chat_message.go +++ b/internal/shortcut/chat/chat_message.go @@ -286,46 +286,22 @@ func listMessageProjectOne(m map[string]any) map[string]any { } func listMessageProjectOneWithReactions(m map[string]any, includeReactions bool) map[string]any { - row := map[string]any{} - if v, ok := listMessagesFirst(m, "openMessageId", "openMsgId", "messageId", "msgId"); ok { - row["messageId"] = v - } - if v, ok := listMessagesFirst(m, "senderOpenDingTalkId", "senderUserId", "senderId", "senderStaffId"); ok { - row["senderId"] = v - } - if v, ok := listMessagesFirst(m, "msgType", "messageType", "type"); ok { - row["msgType"] = v - } - if v, ok := listMessagesFirst(m, "createTime", "sendTime", "gmtCreate", "messageTime"); ok { - row["createTime"] = v - } - if text := chatmsg.Text(m); text != nil { - row["text"] = text - } - if conversationID := chatmsg.ConversationID(m); conversationID != nil { - row["conversationId"] = conversationID - } - if threadID := chatmsg.ThreadID(m); threadID != nil { - row["threadId"] = threadID - } - if updateTime := chatmsg.UpdateTime(m); updateTime != nil { - row["updateTime"] = updateTime - } - if includeReactions { - if reactions := chatmsg.Reactions(m); len(reactions) > 0 { - row["reactions"] = reactions + row := chatmsg.ProjectMessageV1(m, includeReactions) + // The established mget/list projection omits absent scalar fields; keep + // that wire behavior even though the shared chat/search view retains them. + for _, key := range []string{"sender", "text", "createTime"} { + if row[key] == nil { + delete(row, key) } } - if quoted := chatmsg.QuotedMessage(m); len(quoted) > 0 { - row["quotedMessage"] = quoted + // Keep the historical mget/list msgType alias while adding the canonical + // messageType field from MessageViewV1. + if messageType := chatmsg.MessageType(m); messageType != nil { + row["msgType"] = messageType } - if resources := chatmsg.ResourcesDeep(m); len(resources) > 0 { - row["resourceRefs"] = resources - } - projectForwarded := func(item map[string]any) map[string]any { + if forwarded := chatmsg.Forwarded(m, func(item map[string]any) map[string]any { return listMessageProjectOneWithReactions(item, includeReactions) - } - if forwarded := chatmsg.Forwarded(m, projectForwarded); len(forwarded) > 0 { + }); len(forwarded) > 0 { row["forwarded"] = forwarded } return row @@ -562,20 +538,35 @@ var MessagesMget = shortcut.Shortcut{ } } notFound := make([]string, 0) + failures := make([]map[string]any, 0) for _, id := range ids { if !found[id] { notFound = append(notFound, id) + failures = append(failures, map[string]any{ + "stage": "mget", + "messageId": id, + "error": "下层未返回该消息", + }) } } payload := map[string]any{ + "contractVersion": chatmsg.MessageListContractVersion, "requestedCount": len(ids), "foundCount": len(ids) - len(notFound), "notFoundCount": len(notFound), "notFoundMessageIds": notFound, "messages": messages, + "complete": len(notFound) == 0, + "hasMore": false, + "nextCursor": "", + "paginationKnown": true, + "pagesFetched": 1, + "enrichedCount": 0, + "failedCount": len(failures), + "failures": failures, } if rt.Bool("download-resources") { - payload["resourceDownloads"] = DownloadMessageResources(rt, rawMessages, "") + AttachMessageResourceDownloads(payload, DownloadMessageResources(rt, rawMessages, "")) } return rt.Output(payload) }, @@ -799,6 +790,47 @@ func DownloadMessageResources( } } +// AttachMessageResourceDownloads publishes the download ledger and folds any +// resource failure into the task-level completeness contract without dropping +// successfully read messages or downloaded files. +func AttachMessageResourceDownloads(payload, ledger map[string]any) { + payload["resourceDownloads"] = ledger + failed := messageLedgerInt(ledger["failedCount"]) + if failed == 0 { + return + } + payload["complete"] = false + payload["failedCount"] = messageLedgerInt(payload["failedCount"]) + failed + taskFailures, _ := payload["failures"].([]map[string]any) + resourceFailures, _ := ledger["failures"].([]map[string]any) + for _, failure := range resourceFailures { + row := make(map[string]any, len(failure)+1) + row["stage"] = "resource-download" + for key, value := range failure { + row[key] = value + } + taskFailures = append(taskFailures, row) + } + payload["failures"] = taskFailures +} + +func messageLedgerInt(value any) int { + switch typed := value.(type) { + case int: + return typed + case int32: + return int(typed) + case int64: + return int(typed) + case float32: + return int(typed) + case float64: + return int(typed) + default: + return 0 + } +} + func disambiguateResourceDownloadFilename(filename string, used map[string]bool) string { if !used[strings.ToLower(filename)] { return filename diff --git a/internal/shortcut/chat/chat_message_project_test.go b/internal/shortcut/chat/chat_message_project_test.go index 41065aea..f4834bd1 100644 --- a/internal/shortcut/chat/chat_message_project_test.go +++ b/internal/shortcut/chat/chat_message_project_test.go @@ -75,6 +75,34 @@ func TestListMessageProjectOne(t *testing.T) { } } +func TestAttachMessageResourceDownloadsPreservesMessagesAndMarksIncomplete(t *testing.T) { + payload := map[string]any{ + "messages": []map[string]any{{"messageId": "msg-1"}}, + "complete": true, + "failedCount": 0, + "failures": []map[string]any{}, + } + ledger := map[string]any{ + "failedCount": 1, + "failures": []map[string]any{{ + "messageId": "msg-1", + "error": "download failed", + }}, + } + AttachMessageResourceDownloads(payload, ledger) + if payload["complete"] != false || payload["failedCount"] != 1 { + t.Fatalf("task completeness = %#v", payload) + } + messages, _ := payload["messages"].([]map[string]any) + if len(messages) != 1 || messages[0]["messageId"] != "msg-1" { + t.Fatalf("messages were dropped: %#v", messages) + } + failures, _ := payload["failures"].([]map[string]any) + if len(failures) != 1 || failures[0]["stage"] != "resource-download" { + t.Fatalf("resource failures = %#v", failures) + } +} + func TestListPinProjectPreservesThreadIdentity(t *testing.T) { got := listPinProject(map[string]any{ "result": map[string]any{ diff --git a/internal/shortcut/chat/gap_fill_test.go b/internal/shortcut/chat/gap_fill_test.go index 0af89941..f7ac7d05 100644 --- a/internal/shortcut/chat/gap_fill_test.go +++ b/internal/shortcut/chat/gap_fill_test.go @@ -23,7 +23,7 @@ import ( func TestMessagesSendPublishesCompleteIdentityConstraintInputs(t *testing.T) { want := []string{ - "identity", "as", "group", "chat-id", "user", "open-dingtalk-id", + "identity", "as", "group", "chat-id", "chat-query", "user", "user-query", "open-dingtalk-id", "users", "open-dingtalk-ids", "robot-code", "webhook-token", "uuid", "idempotency-key", } diff --git a/internal/shortcut/chat/lark_alignment.go b/internal/shortcut/chat/lark_alignment.go index f7679d22..57fbaaa5 100644 --- a/internal/shortcut/chat/lark_alignment.go +++ b/internal/shortcut/chat/lark_alignment.go @@ -13,6 +13,7 @@ import ( apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/chatmsg" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver" ) // ChatCreate creates a DingTalk group as the current user. It intentionally @@ -22,20 +23,32 @@ var ChatCreate = shortcut.Shortcut{ Service: "chat", Command: "+chat-create", Product: "im", - Description: "以当前用户身份创建钉钉群聊", - Intent: "当你要创建一个基础钉钉群聊时使用;自动把当前用户加入成员列表并作为群主,支持 INTERNAL、EXTERNAL、NORMAL 和话题模式。它不支持指定其他 owner、群 description、初始机器人或 Lark public/private 语义。", + Description: "按成员 ID 或姓名全量预检后创建一个钉钉群聊", + Intent: "当你要创建基础钉钉群聊时使用;已知成员 userId/openDingTalkId 传 --users,只知道姓名/花名传 --member-query,也可混合使用。所有自然成员会先完成唯一解析并按稳定 ID 去重,任一零命中或多命中都会在读取当前用户和创建群前整体停止;成功后自动把当前用户加入成员并作为群主。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "name", Type: shortcut.FlagString, Desc: "群名称", Required: true}, - {Name: "users", Type: shortcut.FlagStringSlice, Desc: "初始成员 userId 或 openDingTalkId 列表", Required: true}, + {Name: "users", Type: shortcut.FlagStringSlice, Desc: "初始成员 userId 或 openDingTalkId 列表"}, + {Name: "member-query", Type: shortcut.FlagStringSlice, Desc: "按姓名/花名唯一解析的初始成员,可逗号分隔或重复传入"}, {Name: "type", Type: shortcut.FlagString, Default: "INTERNAL", Desc: "群类型", Enum: []string{"INTERNAL", "EXTERNAL", "NORMAL"}}, {Name: "thread", Type: shortcut.FlagBool, Desc: "创建为话题群"}, }, + Constraints: []shortcut.Constraint{ + {Kind: shortcut.ConstraintAtLeastOne, Flags: []string{"users", "member-query"}}, + }, Tips: []string{ `dws chat +chat-create --name "项目冲刺群" --users userId1,userId2`, - `dws chat +chat-create --name "合作群" --users userId1,userId2 --type EXTERNAL`, + `dws chat +chat-create --name "合作群" --member-query "张三,李四" --type EXTERNAL`, }, Execute: func(rt *shortcut.RuntimeContext) error { + resolvedMembers, err := targetresolver.ResolveUsers( + rt, + rt.StrSlice("member-query"), + targetresolver.IdentityAny, + ) + if err != nil { + return err + } profile, err := rt.CallMCPData("contact", "get_current_user_profile", nil) if err != nil { return fmt.Errorf("读取当前用户以设置群主失败: %w", err) @@ -51,6 +64,13 @@ var ChatCreate = shortcut.Shortcut{ members = appendUniqueShortcutString(members, member) } } + for _, resolved := range resolvedMembers { + member := resolved.Selected.UserID + if member == "" { + member = resolved.Selected.OpenDingTalkID + } + members = appendUniqueShortcutString(members, member) + } params := map[string]any{ "groupName": rt.Str("name"), "groupMembers": members, @@ -138,8 +158,8 @@ var MessagesReply = shortcut.Shortcut{ Service: "chat", Command: "+messages-reply", Product: "chat", - Description: "以当前用户身份引用回复消息(自动补全原发送者)", - Intent: "当你要以当前用户身份对已有消息发送纯文本引用回复时使用;提供会话和被引用消息即可,默认通过 mget 自动读取原发送者,也可显式传 openDingTalkId/userId;userId 会通过通讯录搜索精确匹配 openDingTalkId。它不支持 bot 身份、富媒体、卡片或 thread 内回复。", + Description: "引用回复一条已有消息,并返回可继续查询或撤回的发送上下文", + Intent: "当你要以当前用户身份对一条已有消息发送纯文本引用回复时使用;传会话和原消息 ID,CLI 会先读取原发送者,也可显式传 --ref-sender。成功结果在保留下层响应的同时增量返回 messageId(下层提供时)、conversationId、threadId(适用时)、deliveryStatus、idempotencyKey 和 referencedMessage 来源上下文。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "conversation-id", Type: shortcut.FlagString, Desc: "会话 openConversationId", Required: true}, @@ -175,10 +195,79 @@ var MessagesReply = shortcut.Shortcut{ if value := rt.StrFirst("idempotency-key", "uuid"); value != "" { params["uuid"] = value } - return rt.CallMCP("send_personal_message", params) + if rt.DryRun() { + return rt.Output(map[string]any{ + "contractVersion": "im.message-reply.v1", + "dryRun": true, + "willSend": false, + "transport": "chat/send_personal_message", + "arguments": params, + "conversationId": rt.Str("conversation-id"), + "referencedMessage": map[string]any{ + "messageId": replyMessageID(rt), + "senderOpenDingTalkId": refSender, + }, + }) + } + data, err := rt.CallMCPWriteData("chat", "send_personal_message", params) + if err != nil { + return err + } + enrichReplyResult(data, rt, refSender) + return rt.Output(data) }, } +func enrichReplyResult(data map[string]any, rt *shortcut.RuntimeContext, refSender string) { + data["contractVersion"] = "im.message-reply.v1" + data["conversationId"] = rt.Str("conversation-id") + data["referencedMessage"] = map[string]any{ + "messageId": replyMessageID(rt), + "senderOpenDingTalkId": refSender, + "resolutionSource": func() string { + if rt.Str("ref-sender") != "" { + return "explicit" + } + return "message_lookup" + }(), + } + if key := rt.StrFirst("idempotency-key", "uuid"); key != "" { + data["idempotencyKey"] = key + } + if value := replyResponseValue(data, "openMessageId", "openMsgId", "messageId", "msgId"); value != nil { + data["messageId"] = value + } + if value := replyResponseValue(data, "openConvThreadId", "threadId", "topicId"); value != nil { + data["threadId"] = value + } + if value := replyResponseValue(data, "deliveryStatus", "sendStatus", "status"); value != nil { + data["deliveryStatus"] = value + data["deliveryStatusKnown"] = true + } else { + data["deliveryStatus"] = "unknown" + data["deliveryStatusKnown"] = false + } +} + +func replyResponseValue(data map[string]any, keys ...string) any { + scopes := []map[string]any{data} + for _, wrapper := range []string{"result", "data"} { + if nested, ok := data[wrapper].(map[string]any); ok { + scopes = append(scopes, nested) + } + } + for _, scope := range scopes { + for _, key := range keys { + if value, ok := scope[key]; ok && value != nil { + if text, isString := value.(string); !isString || strings.TrimSpace(text) != "" { + return value + } + } + } + } + return nil +} + func resolveReplySender(rt *shortcut.RuntimeContext) (string, error) { if value := rt.Str("ref-sender"); value != "" { if isOpenID(value) { diff --git a/internal/shortcut/chat/lark_alignment_test.go b/internal/shortcut/chat/lark_alignment_test.go index 4331a82f..fa4adbfb 100644 --- a/internal/shortcut/chat/lark_alignment_test.go +++ b/internal/shortcut/chat/lark_alignment_test.go @@ -127,6 +127,79 @@ func TestChatCreateAddsCurrentUserAndNormalizesResult(t *testing.T) { } } +func TestChatCreateResolvesEveryNaturalMemberBeforeCreating(t *testing.T) { + fake := &larkAlignmentCaller{responses: map[string]string{ + "contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"resolved-user","openDingTalkId":"D-resolved"}]}`, + }} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+chat-create", + "--name", "测试群", + "--users", "explicit-user", + "--member-query", "张三", + "--yes", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 3 { + t.Fatalf("calls = %#v, want member resolve + current profile + create", fake.calls) + } + if fake.calls[0].tool != "search_contact_by_key_word" || + fake.calls[1].tool != "get_current_user_profile" || + fake.calls[2].tool != "create_group_conversation" { + t.Fatalf("call order = %#v", fake.calls) + } + if got, want := fake.calls[2].args["groupMembers"], []string{"self-user", "explicit-user", "resolved-user"}; !reflect.DeepEqual(got, want) { + t.Fatalf("groupMembers = %#v, want %#v", got, want) + } +} + +func TestChatCreateNaturalMemberAmbiguityStopsBeforeProfileAndCreate(t *testing.T) { + fake := &larkAlignmentCaller{responses: map[string]string{ + "contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"u1"},{"name":"张三","userId":"u2"}]}`, + }} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+chat-create", + "--name", "测试群", + "--member-query", "张三", + "--yes", + }) + if err := root.Execute(); err == nil { + t.Fatal("ambiguous member unexpectedly created a group") + } + if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" { + t.Fatalf("ambiguous member reached profile/create: %#v", fake.calls) + } +} + +func TestChatCreateNaturalMemberDryRunUsesSameResolutionChain(t *testing.T) { + fake := &larkAlignmentCaller{responses: map[string]string{ + "contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"resolved-user"}]}`, + }} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+chat-create", + "--name", "测试群", + "--member-query", "张三", + "--dry-run", + "--yes", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 3 || + fake.calls[0].tool != "search_contact_by_key_word" || + fake.calls[1].tool != "get_current_user_profile" || + fake.calls[2].tool != "create_group_conversation" { + t.Fatalf("dry-run calls = %#v", fake.calls) + } +} + func TestMessagesSendRoutesIdentitySpecificTransports(t *testing.T) { tests := []struct { name string @@ -292,9 +365,13 @@ func TestLarkAlignmentWriteMappings(t *testing.T) { } func TestMessagesReplyPublishesPlainTextBoundary(t *testing.T) { - fake := &larkAlignmentCaller{} + fake := &larkAlignmentCaller{responses: map[string]string{ + "chat/send_personal_message": `{"result":{"openMessageId":"new-msg","openConvThreadId":"thread-1","sendStatus":"accepted"}}`, + }} helpers.InitDeps(fake) root := newPlatformCoverageRoot() + var output bytes.Buffer + root.SetOut(&output) root.SetArgs([]string{ "chat", "+messages-reply", "--conversation-id", "cid", @@ -327,6 +404,43 @@ func TestMessagesReplyPublishesPlainTextBoundary(t *testing.T) { content["content"] != "收到" { t.Fatalf("reply content = %#v", content) } + var payload map[string]any + if err := json.Unmarshal(output.Bytes(), &payload); err != nil { + t.Fatal(err) + } + if payload["contractVersion"] != "im.message-reply.v1" || + payload["messageId"] != "new-msg" || + payload["conversationId"] != "cid" || + payload["threadId"] != "thread-1" || + payload["deliveryStatus"] != "accepted" || + payload["idempotencyKey"] != "reply-uuid" { + t.Fatalf("reply result context = %#v", payload) + } + referenced, _ := payload["referencedMessage"].(map[string]any) + if referenced["messageId"] != "msg" || referenced["resolutionSource"] != "explicit" { + t.Fatalf("referenced message context = %#v", referenced) + } +} + +func TestMessagesReplyDryRunStopsBeforeWrite(t *testing.T) { + fake := &larkAlignmentCaller{} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+messages-reply", + "--conversation-id", "cid", + "--message-id", "msg", + "--ref-sender", "D-sender", + "--text", "收到", + "--dry-run", + "--yes", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 0 { + t.Fatalf("reply dry-run reached write transport: %#v", fake.calls) + } } func TestFlagBatchContinuesAndPublishesFailureLedger(t *testing.T) { diff --git a/internal/shortcut/chat/unified_send.go b/internal/shortcut/chat/unified_send.go index d0024bf6..f1ada33e 100644 --- a/internal/shortcut/chat/unified_send.go +++ b/internal/shortcut/chat/unified_send.go @@ -15,6 +15,7 @@ import ( 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/targetresolver" ) const messagesSendFileUploadTimeout = 10 * time.Minute @@ -27,15 +28,17 @@ var MessagesSend = shortcut.Shortcut{ Service: "chat", Command: "+messages-send", Product: "chat", - Description: "统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", - Intent: "当你希望用同一个入口选择 current-user、bot 或 webhook 身份发送消息时使用;命令会按身份校验目标、内容和凭据并路由到真实下层。current-user 支持文本/Markdown、已有 mediaId 图片、安全相对路径文件上传和幂等键;--user 传 userId 时包括在 --dry-run 中也会先通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId。bot 支持群聊或批量单聊文本/Markdown;webhook 的目标由 token 所在群决定。不会把 user 文件能力伪装成 bot/webhook 等价能力。", + Description: "按身份和目标统一发送文本、Markdown、当前用户文件或已有 mediaId 图片", + Intent: "当你需要文件、复杂 @、幂等,或选择 current-user、bot、webhook 身份发送消息时使用;current-user 可直接传稳定 ID,也可用 --user-query/--chat-query 在 CLI 内唯一解析自然目标,dry-run 与真实执行使用同一解析链。文件上传和已有 mediaId 图片仅 current-user 支持;bot/webhook 只支持文本或 Markdown,webhook 目标由 token 所在群决定。", Risk: shortcut.RiskWrite, Flags: []shortcut.Flag{ {Name: "identity", Type: shortcut.FlagString, Default: "user", Enum: []string{"user", "bot", "webhook"}, Desc: "发送身份;目标、凭据和幂等参数受发送身份能力矩阵约束"}, {Name: "as", Type: shortcut.FlagString, Enum: []string{"user", "bot", "webhook"}, Desc: "--identity 的 lark-cli 对齐别名;受发送身份能力矩阵约束"}, {Name: "group", Type: shortcut.FlagString, Desc: "群 openConversationId(user/bot 群聊);受发送身份能力矩阵约束"}, {Name: "chat-id", Type: shortcut.FlagString, Desc: "--group 的 lark-cli 对齐别名;受发送身份能力矩阵约束"}, + {Name: "chat-query", Type: shortcut.FlagString, Desc: "按群名解析唯一群聊(仅 user 的高级发送场景);受发送身份能力矩阵约束"}, {Name: "user", Type: shortcut.FlagString, Desc: "单聊接收者 userId(user;包括 --dry-run 也会先通过通讯录搜索精确匹配 openDingTalkId);受发送身份能力矩阵约束"}, + {Name: "user-query", Type: shortcut.FlagString, Desc: "按姓名解析唯一 openDingTalkId(仅 user 的高级发送场景);受发送身份能力矩阵约束"}, {Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊接收者 openDingTalkId(user);受发送身份能力矩阵约束"}, {Name: "users", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 userId(bot);受发送身份能力矩阵约束"}, {Name: "open-dingtalk-ids", Type: shortcut.FlagStringSlice, Desc: "批量单聊接收者 openDingTalkId(bot);受发送身份能力矩阵约束"}, @@ -65,7 +68,7 @@ var MessagesSend = shortcut.Shortcut{ {Kind: shortcut.ConstraintMutuallyExclusive, Flags: []string{"uuid", "idempotency-key"}}, { Kind: shortcut.ConstraintCustom, - Flags: []string{"identity", "as", "group", "chat-id", "user", "open-dingtalk-id", "users", "open-dingtalk-ids", "robot-code", "webhook-token", "uuid", "idempotency-key"}, + Flags: []string{"identity", "as", "group", "chat-id", "chat-query", "user", "user-query", "open-dingtalk-id", "users", "open-dingtalk-ids", "robot-code", "webhook-token", "uuid", "idempotency-key"}, Description: "目标、凭据和幂等参数受发送身份能力矩阵约束:user 必须指定一个群聊或单聊目标;bot 必须指定 robot-code 和一类目标;webhook 必须指定 webhook-token;幂等键仅 user 支持", }, }, @@ -81,7 +84,9 @@ var MessagesSend = shortcut.Shortcut{ func validateMessagesSend(rt *shortcut.RuntimeContext) error { identity := messagesSendIdentity(rt) group := rt.StrFirst("chat-id", "group") + chatQuery := rt.Str("chat-query") userID := rt.Str("user") + userQuery := rt.Str("user-query") openID := rt.Str("open-dingtalk-id") users := uniqueShortcutStrings(rt.StrSlice("users")) openIDs := uniqueShortcutStrings(rt.StrSlice("open-dingtalk-ids")) @@ -100,9 +105,9 @@ func validateMessagesSend(rt *shortcut.RuntimeContext) error { } switch identity { case "user": - targetCount := nonEmptyStringCount(group, userID, openID) + targetCount := nonEmptyStringCount(group, chatQuery, userID, userQuery, openID) if targetCount != 1 { - return apperrors.NewValidation("--identity user 时 --group、--user、--open-dingtalk-id 必须且只能指定一个") + return apperrors.NewValidation("--identity user 时 --group/--chat-id、--chat-query、--user、--user-query、--open-dingtalk-id 必须且只能指定一个") } if len(users) > 0 || len(openIDs) > 0 || rt.Str("robot-code") != "" || rt.Str("webhook-token") != "" { return apperrors.NewValidation("--identity user 不接受 bot/webhook 凭据或批量目标") @@ -110,13 +115,16 @@ func validateMessagesSend(rt *shortcut.RuntimeContext) error { if len(atUserIDs) > 0 || len(atMobiles) > 0 { return apperrors.NewValidation("--identity user 只接受 --at-open-dingtalk-ids") } - if (userID != "" || openID != "") && (len(atOpenIDs) > 0 || rt.Bool("at-all")) { + if (userID != "" || userQuery != "" || openID != "") && (len(atOpenIDs) > 0 || rt.Bool("at-all")) { return apperrors.NewValidation("user 单聊不接受 @ 参数;@ 只适用于群聊") } if contentType != "text" && contentType != "markdown" && (len(atOpenIDs) > 0 || rt.Bool("at-all")) { return apperrors.NewValidation("user image/file/audio/video 当前不接受 @ 参数") } case "bot": + if chatQuery != "" || userQuery != "" { + return apperrors.NewValidation("--identity bot 当前不接受 --chat-query 或 --user-query;请传真实群 ID 或批量用户 ID") + } if rt.Str("robot-code") == "" { return apperrors.NewValidation("--identity bot 必须指定 --robot-code") } @@ -140,6 +148,9 @@ func validateMessagesSend(rt *shortcut.RuntimeContext) error { return apperrors.NewValidation("--identity bot 当前下层只支持 text/markdown") } case "webhook": + if chatQuery != "" || userQuery != "" { + return apperrors.NewValidation("--identity webhook 的目标由 token 所在群决定,不接受 --chat-query 或 --user-query") + } if rt.Str("webhook-token") == "" { return apperrors.NewValidation("--identity webhook 必须指定 --webhook-token") } @@ -191,33 +202,10 @@ func executeMessagesSend(rt *shortcut.RuntimeContext) error { if contentType == "file" || contentType == "audio" || contentType == "video" { return executeMessagesSendUserFile(rt, group, openID, contentType) } - if group != "" { - body = helpers.NormalizeMessageMentions( - body, - uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids")), - rt.Bool("at-all"), - true, - ) - } - content, _ := json.Marshal(map[string]string{"title": title, "text": body}) - params := rt.AddAIMessageTag(map[string]any{ - "msgType": "markdown", - "content": string(content), - }) - if group != "" { - params["openConversationId"] = group - if values := uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids")); len(values) > 0 { - params["atOpenDingTalkIds"] = values - } - if rt.Bool("at-all") { - params["atAll"] = true - } - } else { - params["receiverOpenDingTalkId"] = openID - } - if value := messagesSendIdempotencyKey(rt); value != "" { - params["uuid"] = value - } + params := resolvedUserMarkdownParams(rt, ResolvedUserMessageTarget{ + GroupID: group, + OpenDingTalkID: openID, + }, title, body, uniqueShortcutStrings(rt.StrSlice("at-open-dingtalk-ids")), rt.Bool("at-all"), messagesSendIdempotencyKey(rt)) return executeUnifiedMessageWrite(rt, "chat", "send_personal_message", params) case "bot": body = helpers.NormalizeMessageMentions( @@ -359,6 +347,20 @@ func messagesSendUserTarget(rt *shortcut.RuntimeContext) (group, openID string, if openID != "" || group != "" { return group, openID, nil } + if query := rt.Str("chat-query"); query != "" { + resolved, resolveErr := targetresolver.ResolveChat(rt, query) + if resolveErr != nil { + return "", "", resolveErr + } + return resolved.Selected.OpenConversationID, "", nil + } + if query := rt.Str("user-query"); query != "" { + resolved, resolveErr := targetresolver.ResolveUser(rt, query, targetresolver.IdentityOpenDingTalkID) + if resolveErr != nil { + return "", "", resolveErr + } + return "", resolved.Selected.OpenDingTalkID, nil + } openID, err = resolveUserOpenDingTalkID(rt, rt.Str("user")) if err != nil { return "", "", err @@ -366,6 +368,64 @@ func messagesSendUserTarget(rt *shortcut.RuntimeContext) (group, openID string, return "", openID, nil } +// ResolvedUserMessageTarget is the stable target accepted by the shared user +// send engine after natural-name resolution has completed. +type ResolvedUserMessageTarget struct { + GroupID string + OpenDingTalkID string +} + +// ExecuteResolvedUserMarkdown lets narrow semantic shortcuts such as +dm and +// +send-to-group reuse the same target/content/AI-tag parameter builder while +// preserving their existing raw lower-response output contract. +func ExecuteResolvedUserMarkdown( + rt *shortcut.RuntimeContext, + target ResolvedUserMessageTarget, + text string, +) error { + params := resolvedUserMarkdownParams( + rt, + target, + text, + text, + nil, + false, + "", + ) + return rt.CallMCP("send_personal_message", params) +} + +func resolvedUserMarkdownParams( + rt *shortcut.RuntimeContext, + target ResolvedUserMessageTarget, + title, body string, + atOpenIDs []string, + atAll bool, + idempotencyKey string, +) map[string]any { + if target.GroupID != "" { + body = helpers.NormalizeMessageMentions(body, atOpenIDs, atAll, true) + } + content, _ := json.Marshal(map[string]string{"title": title, "text": body}) + params := rt.AddAIMessageTag(map[string]any{ + "msgType": "markdown", + "content": string(content), + }) + addMessagesSendUserTarget(params, target.GroupID, target.OpenDingTalkID) + if target.GroupID != "" { + if len(atOpenIDs) > 0 { + params["atOpenDingTalkIds"] = atOpenIDs + } + if atAll { + params["atAll"] = true + } + } + if idempotencyKey != "" { + params["uuid"] = idempotencyKey + } + return params +} + func resolveUserOpenDingTalkID(rt *shortcut.RuntimeContext, userID string) (string, error) { userID = strings.TrimSpace(userID) data, err := rt.CallMCPData("contact", "search_contact_by_key_word", map[string]any{ diff --git a/internal/shortcut/chat/unified_send_resolution_test.go b/internal/shortcut/chat/unified_send_resolution_test.go new file mode 100644 index 00000000..4a200f4f --- /dev/null +++ b/internal/shortcut/chat/unified_send_resolution_test.go @@ -0,0 +1,132 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package chat + +import ( + stderrors "errors" + "testing" + + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" +) + +func TestMessagesSendResolvesNaturalUserAndChatTargets(t *testing.T) { + tests := []struct { + name string + args []string + responses map[string]string + wantTarget string + wantValue string + }{ + { + name: "user query", + args: []string{ + "chat", "+messages-send", "--as", "user", + "--user-query", "张三", "--text", "你好", "--yes", + }, + responses: map[string]string{ + "contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"}]}`, + }, + wantTarget: "receiverOpenDingTalkId", + wantValue: "D1", + }, + { + name: "chat query exact wins", + args: []string{ + "chat", "+messages-send", "--as", "user", + "--chat-query", "项目群", "--text", "你好", "--yes", + }, + responses: map[string]string{ + "im/search_groups": `{"result":[{"title":"项目群-归档","openConversationId":"c2"},{"title":"项目群","openConversationId":"c1"}]}`, + }, + wantTarget: "openConversationId", + wantValue: "c1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fake := &larkAlignmentCaller{responses: tt.responses} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs(tt.args) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 2 { + t.Fatalf("calls = %#v, want resolve + send", fake.calls) + } + send := fake.calls[1] + if send.product != "chat" || send.tool != "send_personal_message" { + t.Fatalf("send = %#v", send) + } + if send.args[tt.wantTarget] != tt.wantValue { + t.Fatalf("%s = %#v, want %q", tt.wantTarget, send.args[tt.wantTarget], tt.wantValue) + } + }) + } +} + +func TestMessagesSendNaturalTargetAmbiguityHasNoWrite(t *testing.T) { + fake := &larkAlignmentCaller{responses: map[string]string{ + "contact/search_contact_by_key_word": `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"},{"name":"张三","userId":"u2","openDingTalkId":"D2"}]}`, + }} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+messages-send", "--as", "user", + "--user-query", "张三", "--text", "你好", "--yes", + }) + err := root.Execute() + if err == nil { + t.Fatal("ambiguous user unexpectedly sent") + } + if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" { + t.Fatalf("ambiguous resolution reached write: %#v", fake.calls) + } + var typed *apperrors.Error + if !stderrors.As(err, &typed) { + t.Fatalf("error type = %T", err) + } + if typed.Reason != "resolution_ambiguous" || typed.Details["type"] != "resolution" { + t.Fatalf("structured error = %#v", typed) + } +} + +func TestMessagesSendDryRunUsesRealNaturalTargetResolution(t *testing.T) { + fake := &larkAlignmentCaller{responses: map[string]string{ + "im/search_groups": `{"result":[{"title":"项目群","openConversationId":"c1"}]}`, + }} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+messages-send", "--as", "user", + "--chat-query", "项目群", "--text", "你好", "--dry-run", "--yes", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 1 { + t.Fatalf("dry-run calls = %#v, want one read-only resolution", fake.calls) + } + if fake.calls[0].product != "im" || fake.calls[0].tool != "search_groups" { + t.Fatalf("dry-run resolution = %#v", fake.calls[0]) + } +} + +func TestMessagesSendRejectsNaturalTargetForUnsupportedIdentity(t *testing.T) { + fake := &larkAlignmentCaller{} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+messages-send", "--as", "bot", "--robot-code", "r", + "--chat-query", "项目群", "--text", "你好", "--yes", + }) + if err := root.Execute(); err == nil { + t.Fatal("bot natural target unexpectedly accepted") + } + if len(fake.calls) != 0 { + t.Fatalf("invalid identity reached lower service: %#v", fake.calls) + } +} diff --git a/internal/shortcut/chatmsg/chatmsg.go b/internal/shortcut/chatmsg/chatmsg.go index 030ce8ae..1c2a07d6 100644 --- a/internal/shortcut/chatmsg/chatmsg.go +++ b/internal/shortcut/chatmsg/chatmsg.go @@ -38,6 +38,10 @@ import ( "strings" ) +// MessageListContractVersion identifies the additive, compatibility-preserving +// public envelope shared by message list/search/mget/thread projections. +const MessageListContractVersion = "im.message-list.v1" + // Sender reads a message's speaker display name, tolerating common sender-name // keys. The message-list responses carry the display name under the bare // "sender" key (verified live), so it is probed first; the remaining aliases and @@ -150,6 +154,88 @@ func MessageType(m map[string]any) any { return firstMessageValue(m, "msgType", "messageType", "message_type", "type") } +// SenderID preserves the stable sender identity without replacing the legacy +// scalar sender display field. Nested sender records and both userId families +// are accepted because list/search/mget currently expose different shapes. +func SenderID(m map[string]any) any { + for _, key := range []string{"sender", "from", "senderUser"} { + if nested, ok := m[key].(map[string]any); ok { + if value := firstMessageValue(nested, + "openDingTalkId", "openDingtalkId", "userId", "senderId", "id"); value != nil { + return value + } + } + } + return firstMessageValue(m, + "senderOpenDingTalkId", "senderOpenDingtalkId", "senderUserId", + "senderId", "sender_id", "senderStaffId", "openDingTalkId", "userId") +} + +// SenderType returns only an explicitly published lower sender type. It does +// not guess that every sender identity is a user because bot/system messages +// can share the same generic senderId key. +func SenderType(m map[string]any) any { + for _, key := range []string{"sender", "from", "senderUser"} { + if nested, ok := m[key].(map[string]any); ok { + if value := firstMessageValue(nested, "senderType", "type", "entityType"); value != nil { + return value + } + } + } + return firstMessageValue(m, "senderType", "sender_type", "fromType", "from_type") +} + +// ProjectMessageV1 is the single compatibility-preserving core projection for +// list, search, mget, @me, and thread readers. Public wrappers may retain +// legacy aliases such as time or msgType, but the underlying identity, +// context, reaction, quote, forward, and resource semantics come from here. +func ProjectMessageV1(m map[string]any, includeReactions bool) map[string]any { + row := map[string]any{ + "sender": Sender(m), + "text": Text(m), + "createTime": CreateTime(m), + } + if value := MessageID(m); value != nil { + row["messageId"] = value + } + if value := ConversationID(m); value != nil { + row["conversationId"] = value + } + if value := ThreadID(m); value != nil { + row["threadId"] = value + } + if value := SenderID(m); value != nil { + row["senderId"] = value + } + if value := SenderType(m); value != nil { + row["senderType"] = value + } + if value := MessageType(m); value != nil { + row["messageType"] = value + } + if value := UpdateTime(m); value != nil { + row["updateTime"] = value + } + if includeReactions { + if reactions := Reactions(m); len(reactions) > 0 { + row["reactions"] = reactions + } + } + if quoted := QuotedMessage(m); len(quoted) > 0 { + row["quotedMessage"] = quoted + } + if resources := ResourcesDeep(m); len(resources) > 0 { + row["resourceRefs"] = resources + } + projectForwarded := func(item map[string]any) map[string]any { + return ProjectMessageV1(item, includeReactions) + } + if forwarded := Forwarded(m, projectForwarded); len(forwarded) > 0 { + row["forwarded"] = forwarded + } + return row +} + // QuotedMessage projects one level of quoted/replied-to context. It is // deliberately non-recursive: a reply chain may be arbitrarily deep or even // cyclic after gateway reshaping, while an Agent primarily needs the quoted @@ -605,13 +691,35 @@ func ApplyPagination(payload, data map[string]any) { // message-list contract paginates with the boundary message createTime, so the // resume object uses exactly that accepted parameter. func ApplyMessagePagination(payload, data map[string]any, messages []map[string]any, direction string) { + payload["contractVersion"] = MessageListContractVersion + payload["pagesFetched"] = 1 + payload["enrichedCount"] = 0 + payload["failedCount"] = 0 + payload["failures"] = []map[string]any{} + payload["hasMore"] = false + payload["complete"] = false page := Pagination(data) if len(page) == 0 { + payload["paginationKnown"] = false + payload["failedCount"] = 1 + payload["failures"] = []map[string]any{{ + "stage": "pagination", + "error": "下层未返回可靠的 hasMore/nextCursor,无法证明结果完整", + }} return } - if value, ok := page["hasMore"]; ok { - payload["hasMore"] = value + value, hasMoreKnown := page["hasMore"] + if !hasMoreKnown { + payload["paginationKnown"] = false + payload["failedCount"] = 1 + payload["failures"] = []map[string]any{{ + "stage": "pagination", + "error": "下层仅返回 cursor、未返回 hasMore,无法证明结果完整", + }} + return } + payload["paginationKnown"] = true + payload["hasMore"] = value if value, ok := page["complete"]; ok { payload["complete"] = value } @@ -621,6 +729,11 @@ func ApplyMessagePagination(payload, data map[string]any, messages []map[string] } boundary := CreateTime(messages[len(messages)-1]) if boundary == nil { + payload["failedCount"] = 1 + payload["failures"] = []map[string]any{{ + "stage": "pagination", + "error": "下层返回 hasMore=true,但末条消息缺少可继续读取的 createTime", + }} return } next := map[string]any{"time": boundary} diff --git a/internal/shortcut/chatmsg/chatmsg_test.go b/internal/shortcut/chatmsg/chatmsg_test.go index 701b4088..32ed4144 100644 --- a/internal/shortcut/chatmsg/chatmsg_test.go +++ b/internal/shortcut/chatmsg/chatmsg_test.go @@ -48,6 +48,36 @@ func TestSender(t *testing.T) { } } +func TestProjectMessageV1PublishesSharedIdentityAndContext(t *testing.T) { + row := ProjectMessageV1(map[string]any{ + "openMessageId": "msg-1", + "openConversationId": "cid-1", + "openConvThreadId": "thread-1", + "sender": map[string]any{ + "name": "张三", + "openDingTalkId": "D1", + "senderType": "user", + }, + "msgType": "text", + "content": "你好", + "createTime": "2026-08-03 10:00:00", + }, true) + for key, want := range map[string]any{ + "messageId": "msg-1", + "conversationId": "cid-1", + "threadId": "thread-1", + "sender": "张三", + "senderId": "D1", + "senderType": "user", + "messageType": "text", + "text": "你好", + } { + if row[key] != want { + t.Errorf("%s = %#v, want %#v; row=%#v", key, row[key], want, row) + } + } +} + func TestCleanText(t *testing.T) { // Out-of-office auto-reply: readable body lives in items[].data.text; the // decorative preview/config JSON lines and "empty" placeholder are dropped. @@ -389,6 +419,20 @@ func TestApplyMessagePaginationUsesExecutableTimeBoundary(t *testing.T) { } } +func TestApplyMessagePaginationFailsClosedWhenCompletenessIsUnknown(t *testing.T) { + payload := map[string]any{} + ApplyMessagePagination(payload, map[string]any{"result": map[string]any{"messages": []any{}}}, nil, "older") + if payload["contractVersion"] != MessageListContractVersion || + payload["complete"] != false || payload["paginationKnown"] != false || + payload["failedCount"] != 1 { + t.Fatalf("unknown pagination contract = %#v", payload) + } + failures, _ := payload["failures"].([]map[string]any) + if len(failures) != 1 || failures[0]["stage"] != "pagination" { + t.Fatalf("unknown pagination failures = %#v", failures) + } +} + func TestResourcesBuildsActionableDownloadReferences(t *testing.T) { resources := Resources(map[string]any{ "openMessageId": "msg-1", diff --git a/internal/shortcut/smart/chat_messages.go b/internal/shortcut/smart/chat_messages.go index e3398cab..12823a1b 100644 --- a/internal/shortcut/smart/chat_messages.go +++ b/internal/shortcut/smart/chat_messages.go @@ -20,6 +20,7 @@ import ( "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" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver" ) var dingTalkMessageLocation = time.FixedZone("CST", 8*60*60) @@ -52,18 +53,19 @@ var ChatMessages = shortcut.Shortcut{ Service: "chat", Command: "+chat-messages", Product: "chat", - Description: "拉取某个会话(群聊或单聊)的消息列表并投影出发言人/文本/时间", - Intent: "当你想快速看某个会话里的消息(谁在什么时间说了什么),而不想拿到一大坨原始消息字段时使用;" + - "群聊传 --group(群会话 ID,openConversationId),单聊传 --user(对方 userId),两者互斥且必须二选一。" + + Description: "按会话 ID、群名或姓名读取一个群聊/单聊的消息并输出稳定投影", + Intent: "当你想快速看一个群聊或单聊里的消息(谁在什么时间说了什么),而不想拿到大段原始消息字段时使用;" + + "群聊可传 --group 或 --chat-query,单聊可传 --user、--open-dingtalk-id 或 --user-query,所有目标参数互斥且必须选一个。自然目标只在唯一解析后读取,多候选会返回结构化 candidates。" + "省略 --time 时默认从当前时间向前读取最近消息;也可指定时间边界并用 --direction newer/older 控制方向。" + - "内部据此调用群聊或单聊的消息列表接口,再在本地投影出每条消息的发言人、文本和时间。" + - "默认只读且不会发送或修改任何消息;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", + "默认只读;--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘。", Risk: shortcut.RiskRead, Flags: append([]shortcut.Flag{ {Name: "group", Type: shortcut.FlagString, Desc: "群会话 ID(openConversationId),与 --user 互斥"}, {Name: "conversation-id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true}, {Name: "id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true}, + {Name: "chat-query", Type: shortcut.FlagString, Desc: "按群名解析唯一 openConversationId"}, {Name: "user", Type: shortcut.FlagString, Desc: "单聊对方的 userId,与 --group 互斥"}, + {Name: "user-query", Type: shortcut.FlagString, Desc: "按姓名解析唯一 openDingTalkId"}, {Name: "open-dingtalk-id", Type: shortcut.FlagString, Desc: "单聊对方的 openDingTalkId,与 --group/--user 互斥"}, {Name: "time", Type: shortcut.FlagString, Desc: "时间边界,如 \"2025-03-01 00:00:00\";省略时从当前时间向前读取最近消息"}, {Name: "limit", Type: shortcut.FlagInt, Desc: "每页拉取的消息条数(可选)"}, @@ -72,7 +74,7 @@ var ChatMessages = shortcut.Shortcut{ {Name: "no-reactions", Type: shortcut.FlagBool, Desc: "不输出消息 reaction(默认输出)"}, }, chatshortcut.MessageResourceDownloadFlags()...), Constraints: append([]shortcut.Constraint{ - {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id", "user", "open-dingtalk-id"}}, + {Kind: shortcut.ConstraintExactlyOne, Flags: []string{"group", "conversation-id", "id", "chat-query", "user", "user-query", "open-dingtalk-id"}}, }, chatshortcut.MessageResourceDownloadConstraints()...), Tips: []string{ `dws chat +chat-messages --group --time "2025-03-01 00:00:00"`, @@ -87,6 +89,23 @@ var ChatMessages = shortcut.Shortcut{ var tool string params := map[string]any{} fallbackConversationID := "" + groupID := rt.StrFirst("group", "conversation-id", "id") + userID := rt.Str("user") + openID := rt.Str("open-dingtalk-id") + if query := rt.Str("chat-query"); query != "" { + resolved, err := targetresolver.ResolveChat(rt, query) + if err != nil { + return err + } + groupID = resolved.Selected.OpenConversationID + } + if query := rt.Str("user-query"); query != "" { + resolved, err := targetresolver.ResolveUser(rt, query, targetresolver.IdentityOpenDingTalkID) + if err != nil { + return err + } + openID = resolved.Selected.OpenDingTalkID + } if rt.Changed("time") && rt.Str("time") != "" { params["time"] = rt.Str("time") @@ -110,16 +129,16 @@ var ChatMessages = shortcut.Shortcut{ params["forward"] = false } - if group := rt.StrFirst("group", "conversation-id", "id"); group != "" { + if groupID != "" { tool = "list_conversation_message_v2" - params["openconversation_id"] = group - fallbackConversationID = group - } else if openID := strings.TrimSpace(rt.Str("open-dingtalk-id")); openID != "" { + params["openconversation_id"] = groupID + fallbackConversationID = groupID + } else if openID != "" { tool = "list_individual_chat_message" params["openDingTalkId"] = openID } else { tool = "list_individual_chat_message" - params["userId"] = rt.Str("user") + params["userId"] = userID } data, err := rt.CallMCPData("chat", tool, params) @@ -142,7 +161,10 @@ var ChatMessages = shortcut.Shortcut{ direction := strings.TrimSpace(strings.ToLower(rt.Str("direction"))) chatmsg.ApplyMessagePagination(payload, data, items, direction) if rt.Bool("download-resources") { - payload["resourceDownloads"] = chatshortcut.DownloadMessageResources(rt, items, fallbackConversationID) + chatshortcut.AttachMessageResourceDownloads( + payload, + chatshortcut.DownloadMessageResources(rt, items, fallbackConversationID), + ) } return rt.Output(payload) }, @@ -188,44 +210,7 @@ func projectChatMessage(m map[string]any) map[string]any { } func projectChatMessageWithReactions(m map[string]any, includeReactions bool) map[string]any { - row := map[string]any{ - "sender": chatmsg.Sender(m), - "text": chatmsg.Text(m), - "createTime": chatmsg.CreateTime(m), - } - if messageID := chatmsg.MessageID(m); messageID != nil { - row["messageId"] = messageID - } - if conversationID := chatmsg.ConversationID(m); conversationID != nil { - row["conversationId"] = conversationID - } - if threadID := chatmsg.ThreadID(m); threadID != nil { - row["threadId"] = threadID - } - if messageType := chatmsg.MessageType(m); messageType != nil { - row["messageType"] = messageType - } - if updateTime := chatmsg.UpdateTime(m); updateTime != nil { - row["updateTime"] = updateTime - } - if includeReactions { - if reactions := chatmsg.Reactions(m); len(reactions) > 0 { - row["reactions"] = reactions - } - } - if quoted := chatmsg.QuotedMessage(m); len(quoted) > 0 { - row["quotedMessage"] = quoted - } - if resources := chatmsg.ResourcesDeep(m); len(resources) > 0 { - row["resourceRefs"] = resources - } - projectForwarded := func(item map[string]any) map[string]any { - return projectChatMessageWithReactions(item, includeReactions) - } - if forwarded := chatmsg.Forwarded(m, projectForwarded); len(forwarded) > 0 { - row["forwarded"] = forwarded - } - return row + return chatmsg.ProjectMessageV1(m, includeReactions) } func init() { diff --git a/internal/shortcut/smart/dm.go b/internal/shortcut/smart/dm.go index f456afdd..41a1f2df 100644 --- a/internal/shortcut/smart/dm.go +++ b/internal/shortcut/smart/dm.go @@ -18,10 +18,9 @@ package smart import ( - "encoding/json" - - 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/targetresolver" ) // DM: message a person by NAME, no ID juggling. @@ -35,7 +34,7 @@ var DM = shortcut.Shortcut{ Service: "chat", Command: "+dm", Product: "chat", - Description: "按姓名直接给某人发单聊消息(自动解析 userId)", + Description: "按姓名直接给某人发单聊消息(自动解析唯一 openDingTalkId)", Intent: "当你只知道对方姓名、想直接发一条单聊消息而不想先查 userId 时使用;" + "内部先按姓名搜通讯录解析出唯一用户,并用其 openDingTalkId 发送,姓名匹配到多人时会列出候选让你区分。会真实发出消息。", Risk: shortcut.RiskWrite, @@ -48,22 +47,21 @@ var DM = shortcut.Shortcut{ Execute: func(rt *shortcut.RuntimeContext) error { text := rt.Str("text") - // Step 1 — resolve the recipient name to a unique userId. - user, err := resolveOpenDingTalkUser(rt, rt.Str("to")) + resolved, err := targetresolver.ResolveUser( + rt, + rt.Str("to"), + targetresolver.IdentityOpenDingTalkID, + ) if err != nil { return err } - if user.openDingTalkID == "" { - return apperrors.NewValidation("通讯录结果缺少 openDingTalkId,无法发送单聊消息;请改用 chat +messages-send --open-dingtalk-id") - } - - // Step 2 — send the single-chat message. - content, _ := json.Marshal(map[string]string{"title": text, "text": text}) - return rt.CallMCP("send_personal_message", rt.AddAIMessageTag(map[string]any{ - "receiverOpenDingTalkId": user.openDingTalkID, - "msgType": "markdown", - "content": string(content), - })) + return chatshortcut.ExecuteResolvedUserMarkdown( + rt, + chatshortcut.ResolvedUserMessageTarget{ + OpenDingTalkID: resolved.Selected.OpenDingTalkID, + }, + text, + ) }, } diff --git a/internal/shortcut/smart/natural_target_resolution_test.go b/internal/shortcut/smart/natural_target_resolution_test.go new file mode 100644 index 00000000..5724458a --- /dev/null +++ b/internal/shortcut/smart/natural_target_resolution_test.go @@ -0,0 +1,109 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package smart + +import ( + "reflect" + "testing" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/helpers" +) + +func TestChatMessagesResolvesNaturalChatAndUserTargets(t *testing.T) { + tests := []struct { + name string + args []string + wantTool string + wantKey string + wantValue string + }{ + { + name: "chat query", + args: []string{"chat", "+chat-messages", "--chat-query", "项目冲刺"}, + wantTool: "list_conversation_message_v2", + wantKey: "openconversation_id", + wantValue: "cid-1", + }, + { + name: "user query", + args: []string{"chat", "+chat-messages", "--user-query", "张三"}, + wantTool: "list_individual_chat_message", + wantKey: "openDingTalkId", + wantValue: "open1", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fake := &platformCoverageCaller{} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs(tt.args) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 2 { + t.Fatalf("calls = %#v, want resolve + read", fake.calls) + } + read := fake.calls[1] + if read.tool != tt.wantTool || read.args[tt.wantKey] != tt.wantValue { + t.Fatalf("read = %#v", read) + } + }) + } +} + +func TestChatMessagesNaturalUserAmbiguityStopsBeforeMessageRead(t *testing.T) { + fake := &platformCoverageCaller{contactSearchResult: `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"},{"name":"张三","userId":"u2","openDingTalkId":"D2"}]}`} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{"chat", "+chat-messages", "--user-query", "张三"}) + if err := root.Execute(); err == nil { + t.Fatal("ambiguous user unexpectedly reached message read") + } + if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" { + t.Fatalf("calls = %#v", fake.calls) + } +} + +func TestSearchMsgResolvesNaturalChatAndSenderBeforeSearch(t *testing.T) { + fake := &platformCoverageCaller{contactSearchResult: `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"}]}`} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{ + "chat", "+search-msg", + "--chat-query", "项目冲刺", + "--sender-query", "张三", + "--no-enrich", + }) + if err := root.Execute(); err != nil { + t.Fatal(err) + } + if len(fake.calls) != 3 { + t.Fatalf("calls = %#v, want chat resolve + user resolve + search", fake.calls) + } + search := fake.calls[2] + if search.product != "im" || search.tool != "search_messages" { + t.Fatalf("search = %#v", search) + } + if got, want := search.args["openConversationIds"], []string{"cid-1"}; !reflect.DeepEqual(got, want) { + t.Fatalf("openConversationIds = %#v, want %#v", got, want) + } + if got, want := search.args["senderOpenDingTakIds"], []string{"D1"}; !reflect.DeepEqual(got, want) { + t.Fatalf("senderOpenDingTakIds = %#v, want %#v", got, want) + } +} + +func TestSearchMsgNaturalSenderAmbiguityStopsBeforeSearch(t *testing.T) { + fake := &platformCoverageCaller{contactSearchResult: `{"result":[{"name":"张三","userId":"u1","openDingTalkId":"D1"},{"name":"张三","userId":"u2","openDingTalkId":"D2"}]}`} + helpers.InitDeps(fake) + root := newPlatformCoverageRoot() + root.SetArgs([]string{"chat", "+search-msg", "--sender-query", "张三", "--no-enrich"}) + if err := root.Execute(); err == nil { + t.Fatal("ambiguous sender unexpectedly reached search") + } + if len(fake.calls) != 1 || fake.calls[0].tool != "search_contact_by_key_word" { + t.Fatalf("calls = %#v", fake.calls) + } +} diff --git a/internal/shortcut/smart/resolve.go b/internal/shortcut/smart/resolve.go index f688134b..b9478758 100644 --- a/internal/shortcut/smart/resolve.go +++ b/internal/shortcut/smart/resolve.go @@ -1,131 +1,87 @@ // Copyright 2026 Alibaba Group // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. package smart import ( - "fmt" - "strings" - - apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver" ) -// contactUser is the minimal identity a smart shortcut needs after resolving a -// name. +// contactUser preserves the smart package's internal shape while resolution +// semantics live in targetresolver for reuse by chat and future facades. type contactUser struct { userID string openDingTalkID string name string } -// resolveUser turns a human name into a single unique contact, the "name → ID" -// intelligence that distinguishes a smart shortcut from a raw tool call -// (mirrors the shared name→ID resolver). It searches the directory and: -// - errors with a clear message if nobody matches; -// - errors listing the candidates if the name is ambiguous (never guesses). func resolveUser(rt *shortcut.RuntimeContext, name string) (contactUser, error) { return resolveUserByName(rt, name, false) } -// resolveOpenDingTalkUser also accepts external / cross-org contacts that have -// an openDingTalkId but no organization-scoped userId. Use it only for flows -// whose downstream interface consumes openDingTalkId. func resolveOpenDingTalkUser(rt *shortcut.RuntimeContext, name string) (contactUser, error) { return resolveUserByName(rt, name, true) } func resolveUserByName(rt *shortcut.RuntimeContext, name string, includeOpenIDOnly bool) (contactUser, error) { - data, err := rt.CallMCPData("contact", "search_contact_by_key_word", map[string]any{ - "keyword": name, - }) + requirement := targetresolver.IdentityUserID + if includeOpenIDOnly { + requirement = targetresolver.IdentityOpenDingTalkID + } + resolved, err := targetresolver.ResolveUser(rt, name, requirement) if err != nil { return contactUser{}, err } - users := extractUsers(data) - if !includeOpenIDOnly { - users = usersWithUserID(users) - } - switch { - case len(users) == 0: - return contactUser{}, apperrors.NewValidation( - fmt.Sprintf("通讯录里没找到叫 %q 的人;换个更完整的姓名再试。", name)) - case len(users) > 1: - return contactUser{}, apperrors.NewValidation(fmt.Sprintf( - "%q 匹配到 %d 个人:%s。请用更精确的姓名,或改用对应命令直接传用户 ID。", - name, len(users), strings.Join(userLabels(users), "、"))) - } - return users[0], nil + return fromResolvedUser(resolved.Selected), nil } func usersWithUserID(users []contactUser) []contactUser { - out := make([]contactUser, 0, len(users)) + resolved := make([]targetresolver.User, 0, len(users)) for _, user := range users { - if user.userID != "" { - out = append(out, user) - } + resolved = append(resolved, toResolvedUser(user)) + } + filtered := targetresolver.UsersWithUserID(resolved) + out := make([]contactUser, 0, len(filtered)) + for _, user := range filtered { + out = append(out, fromResolvedUser(user)) } return out } -// extractUsers pulls {userId, openDingTalkId, name} out of a -// search_contact_by_key_word response ({"result": [ {userId, name, ...} ]}). func extractUsers(data map[string]any) []contactUser { - raw, ok := data["result"].([]any) - if !ok { + resolved := targetresolver.ExtractUsers(data) + if len(resolved) == 0 { return nil } - var out []contactUser - for _, item := range raw { - m, ok := item.(map[string]any) - if !ok { - continue - } - id, _ := m["userId"].(string) - openID, _ := m["openDingTalkId"].(string) - if openID == "" { - openID, _ = m["openDingtalkId"].(string) - } - // External / cross-org contacts come back with an empty userId and only - // an openDingTalkId (verified live). Dropping them here made name→ID - // resolution miss those people, or collapse to the wrong single match - // when an in-org namesake also existed. Keep any row with at least one - // usable identity; downstream (e.g. +dm) can act on the openDingTalkId. - if id == "" && openID == "" { - continue - } - nm, _ := m["name"].(string) - if nm == "" { - for _, k := range []string{"nick", "showName", "flowerName", "staffName", "userName"} { - if v, _ := m[k].(string); v != "" { - nm = v - break - } - } - } - out = append(out, contactUser{userID: id, openDingTalkID: openID, name: nm}) + out := make([]contactUser, 0, len(resolved)) + for _, user := range resolved { + out = append(out, fromResolvedUser(user)) } return out } func userLabels(users []contactUser) []string { - out := make([]string, 0, len(users)) - for _, u := range users { - id := u.userID - if id == "" { - id = u.openDingTalkID - } - out = append(out, fmt.Sprintf("%s(%s)", u.name, id)) + resolved := make([]targetresolver.User, 0, len(users)) + for _, user := range users { + resolved = append(resolved, toResolvedUser(user)) + } + return targetresolver.UserLabels(resolved) +} + +func fromResolvedUser(user targetresolver.User) contactUser { + return contactUser{ + userID: user.UserID, + openDingTalkID: user.OpenDingTalkID, + name: user.Name, + } +} + +func toResolvedUser(user contactUser) targetresolver.User { + return targetresolver.User{ + UserID: user.userID, + OpenDingTalkID: user.openDingTalkID, + Name: user.name, } - return out } diff --git a/internal/shortcut/smart/search_msg.go b/internal/shortcut/smart/search_msg.go index 84fdcbb5..97c93cf8 100644 --- a/internal/shortcut/smart/search_msg.go +++ b/internal/shortcut/smart/search_msg.go @@ -23,6 +23,7 @@ import ( "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" + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/shortcut/targetresolver" ) // SearchMsg is the semantic message-search entry point. It exposes the native @@ -34,10 +35,11 @@ var SearchMsg = shortcut.Shortcut{ Service: "chat", Command: "+search-msg", Product: "im", - Description: "多维搜索消息,可全量翻页并批量富化详情", - Intent: "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;默认查询近 7 天," + - "也可指定精确起止时间。--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。" + - "--download-resources 使用工作目录内安全路径、默认不覆盖和原子落盘,按既有安全下载约定无需交互确认。", + Description: "按 ID、群名、姓名和内容等条件跨会话搜索消息,可全量翻页并批量富化", + Intent: "当你要按关键词、发送者、@对象、会话、消息类型或机器人来源组合搜索 IM 消息时使用;会话可传稳定 ID 或 --chat-query,发送者可传稳定 ID 或 --sender-query," + + "多个自然目标会先做全量解析,任一零命中/多命中都会在搜索前返回结构化失败。默认查询近 7 天,也可指定精确起止时间。" + + "--page-all 会连续拉取游标页,默认再按消息 ID 分批富化详情;任何续页或富化失败都会保留已取得结果并返回逐项失败 ledger,绝不把截断结果标成完整。" + + "--download-resources 使用安全本地路径、默认不覆盖和原子落盘。", Risk: shortcut.RiskRead, Flags: append([]shortcut.Flag{ {Name: "query", Type: shortcut.FlagString, Desc: "搜索关键词"}, @@ -47,8 +49,10 @@ var SearchMsg = shortcut.Shortcut{ {Name: "id", Type: shortcut.FlagString, Desc: "--group 的别名", Hidden: true}, {Name: "groups", Type: shortcut.FlagStringSlice, Desc: "多个会话 openConversationId"}, {Name: "chat-id", Type: shortcut.FlagStringSlice, Desc: "--groups 的 lark-cli 对齐别名"}, + {Name: "chat-query", Type: shortcut.FlagStringSlice, Desc: "按群名解析一个或多个唯一 openConversationId"}, {Name: "senders", Type: shortcut.FlagStringSlice, Desc: "发送者 userId/openDingTalkId 列表"}, {Name: "sender", Type: shortcut.FlagStringSlice, Desc: "--senders 的 lark-cli 对齐别名"}, + {Name: "sender-query", Type: shortcut.FlagStringSlice, Desc: "按姓名解析一个或多个唯一发送者"}, {Name: "at-me", Type: shortcut.FlagBool, Desc: "只搜索 @我 的消息"}, {Name: "is-at-me", Type: shortcut.FlagBool, Desc: "--at-me 的 lark-cli 对齐别名"}, {Name: "at-ids", Type: shortcut.FlagStringSlice, Desc: "@对象 userId/openDingTalkId 列表"}, @@ -71,7 +75,7 @@ var SearchMsg = shortcut.Shortcut{ Constraints: append([]shortcut.Constraint{ { Kind: shortcut.ConstraintAtLeastOne, - Flags: []string{"query", "keyword", "group", "conversation-id", "id", "groups", "chat-id", "senders", "sender", "at-me", "is-at-me", "at-ids", "message-type", "only-robot", "conversation-type", "chat-type"}, + Flags: []string{"query", "keyword", "group", "conversation-id", "id", "groups", "chat-id", "chat-query", "senders", "sender", "sender-query", "at-me", "is-at-me", "at-ids", "message-type", "only-robot", "conversation-type", "chat-type"}, Description: "至少指定一个内容、身份、会话或消息类型过滤条件", }, { @@ -114,6 +118,7 @@ var SearchMsg = shortcut.Shortcut{ complete := true hasMore := false nextCursor := "" + paginationKnown := true for pagesFetched < pageLimit { params["cursor"] = cursor @@ -153,6 +158,7 @@ var SearchMsg = shortcut.Shortcut{ "stage": "search-pagination", "error": "下层未返回 hasMore 或 nextCursor,无法证明结果完整", }) + paginationKnown = false complete = false break } @@ -195,20 +201,26 @@ var SearchMsg = shortcut.Shortcut{ results = append(results, searchMsgProjectWithReactions(m, !rt.Bool("no-reactions"))) } payload := map[string]any{ - "count": len(results), - "messages": results, - "pagesFetched": pagesFetched, - "enrichedCount": enrichedCount, - "complete": complete, - "hasMore": hasMore, - "failedCount": len(failures), - "failures": failures, + "contractVersion": chatmsg.MessageListContractVersion, + "count": len(results), + "messages": results, + "pagesFetched": pagesFetched, + "enrichedCount": enrichedCount, + "complete": complete, + "hasMore": hasMore, + "nextCursor": "", + "paginationKnown": paginationKnown, + "failedCount": len(failures), + "failures": failures, } if hasMore && nextCursor != "" && nextCursor != "" { payload["nextCursor"] = nextCursor } if rt.Bool("download-resources") { - payload["resourceDownloads"] = chatshortcut.DownloadMessageResources(rt, messages, "") + chatshortcut.AttachMessageResourceDownloads( + payload, + chatshortcut.DownloadMessageResources(rt, messages, ""), + ) } return rt.Output(payload) }, @@ -225,8 +237,10 @@ func validateSearchMsg(rt *shortcut.RuntimeContext) error { hasFilter := rt.StrFirst("query", "keyword", "group", "conversation-id", "id", "message-type", "conversation-type", "chat-type") != "" || len(rt.StrSlice("groups")) > 0 || len(rt.StrSlice("chat-id")) > 0 || + len(rt.StrSlice("chat-query")) > 0 || len(rt.StrSlice("senders")) > 0 || len(rt.StrSlice("sender")) > 0 || + len(rt.StrSlice("sender-query")) > 0 || len(rt.StrSlice("at-ids")) > 0 || rt.Bool("at-me") || rt.Bool("is-at-me") || @@ -260,11 +274,33 @@ func searchMsgParams(rt *shortcut.RuntimeContext) (map[string]any, error) { if value := rt.StrFirst("group", "conversation-id", "id"); value != "" { conversationIDs = append(conversationIDs, value) } + if queries := rt.StrSlice("chat-query"); len(queries) > 0 { + resolvedChats, err := targetresolver.ResolveChats(rt, queries) + if err != nil { + return nil, err + } + for _, resolved := range resolvedChats { + conversationIDs = append(conversationIDs, resolved.Selected.OpenConversationID) + } + } if values := uniqueSearchStrings(conversationIDs); len(values) > 0 { params["openConversationIds"] = values } senders := append([]string{}, rt.StrSlice("senders")...) senders = append(senders, rt.StrSlice("sender")...) + if queries := rt.StrSlice("sender-query"); len(queries) > 0 { + resolvedUsers, err := targetresolver.ResolveUsers(rt, queries, targetresolver.IdentityAny) + if err != nil { + return nil, err + } + for _, resolved := range resolvedUsers { + identity := resolved.Selected.OpenDingTalkID + if identity == "" { + identity = resolved.Selected.UserID + } + senders = append(senders, identity) + } + } appendSearchActorIDs(params, senders, "senderUserIds", "senderOpenDingTakIds") appendSearchActorIDs(params, rt.StrSlice("at-ids"), "atUserIds", "atOpenDingTakIds") if rt.Bool("at-me") || rt.Bool("is-at-me") { @@ -499,39 +535,16 @@ func searchMsgProject(m map[string]any) map[string]any { } func searchMsgProjectWithReactions(m map[string]any, includeReactions bool) map[string]any { - row := map[string]any{ - "sender": searchMsgSender(m), - "time": searchMsgTime(m), - "text": searchMsgCleanText(m), - "messageId": searchMsgMessageID(m), - } - if conversationID := chatmsg.ConversationID(m); conversationID != nil { - row["conversationId"] = conversationID - } - if threadID := chatmsg.ThreadID(m); threadID != nil { - row["threadId"] = threadID - } - if messageType := chatmsg.MessageType(m); messageType != nil { - row["messageType"] = messageType - } - if updateTime := chatmsg.UpdateTime(m); updateTime != nil { - row["updateTime"] = updateTime - } - if includeReactions { - if reactions := chatmsg.Reactions(m); len(reactions) > 0 { - row["reactions"] = reactions - } - } - if quoted := chatmsg.QuotedMessage(m); len(quoted) > 0 { - row["quotedMessage"] = quoted - } - if resources := chatmsg.ResourcesDeep(m); len(resources) > 0 { - row["resourceRefs"] = resources - } - projectForwarded := func(item map[string]any) map[string]any { + row := chatmsg.ProjectMessageV1(m, includeReactions) + // Preserve the established search aliases while also publishing the V1 + // canonical fields used by other message readers. + row["sender"] = searchMsgSender(m) + row["time"] = searchMsgTime(m) + row["text"] = searchMsgCleanText(m) + row["messageId"] = searchMsgMessageID(m) + if forwarded := chatmsg.Forwarded(m, func(item map[string]any) map[string]any { return searchMsgProjectWithReactions(item, includeReactions) - } - if forwarded := chatmsg.Forwarded(m, projectForwarded); len(forwarded) > 0 { + }); len(forwarded) > 0 { row["forwarded"] = forwarded } return row diff --git a/internal/shortcut/smart/send_to_group.go b/internal/shortcut/smart/send_to_group.go index 7fb2d9ee..a3f28df6 100644 --- a/internal/shortcut/smart/send_to_group.go +++ b/internal/shortcut/smart/send_to_group.go @@ -14,12 +14,9 @@ package smart import ( - "encoding/json" - "fmt" - "strings" - - 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/targetresolver" ) // SendToGroup: message a group by its NAME, no openConversationId juggling. @@ -52,33 +49,17 @@ var SendToGroup = shortcut.Shortcut{ groupName := rt.Str("group") text := rt.Str("text") - // Step 1 — search groups by name (keyword) on the im server. - data, err := rt.CallMCPData("im", "search_groups", map[string]any{ - "keyword": groupName, - "limit": 10, - "cursor": "0", - }) + resolved, err := targetresolver.ResolveChat(rt, groupName) if err != nil { return err } - groups := preferExactGroupMatches(extractGroupsForSend(data), groupName) - switch { - case len(groups) == 0: - return apperrors.NewValidation(fmt.Sprintf( - "没找到名字匹配 %q 的群;换用群名里连续的核心词再试。", groupName)) - case len(groups) > 1: - return apperrors.NewValidation(fmt.Sprintf( - "%q 匹配到 %d 个群:%s。请用更精确的群名,或直接用 dws chat +messages-send --group 指定群。", - groupName, len(groups), strings.Join(sendGroupLabels(groups), "、"))) - } - - // Step 2 — send the markdown message to the unique group. - content, _ := json.Marshal(map[string]string{"title": text, "text": text}) - return rt.CallMCP("send_personal_message", rt.AddAIMessageTag(map[string]any{ - "openConversationId": groups[0].id, - "msgType": "markdown", - "content": string(content), - })) + return chatshortcut.ExecuteResolvedUserMarkdown( + rt, + chatshortcut.ResolvedUserMessageTarget{ + GroupID: resolved.Selected.OpenConversationID, + }, + text, + ) }, } @@ -93,41 +74,10 @@ type sendGroupMatch struct { // result/result.items/result.groups (field names per chat search's real // response shape), and the name field may be "title" or "name". func extractGroupsForSend(data map[string]any) []sendGroupMatch { - var list []any - switch inner := data["result"].(type) { - case []any: - list = inner - case map[string]any: - if v, ok := inner["items"].([]any); ok { - list = v - } else if v, ok := inner["groups"].([]any); ok { - list = v - } - } - if list == nil { - if v, ok := data["items"].([]any); ok { - list = v - } - } - - var out []sendGroupMatch - for _, item := range list { - m, ok := item.(map[string]any) - if !ok { - continue - } - id, _ := m["openConversationId"].(string) - if id == "" { - id, _ = m["id"].(string) - } - if id == "" { - continue - } - name, _ := m["title"].(string) - if name == "" { - name, _ = m["name"].(string) - } - out = append(out, sendGroupMatch{id: id, name: name}) + chats := targetresolver.ExtractChats(data) + out := make([]sendGroupMatch, 0, len(chats)) + for _, chat := range chats { + out = append(out, sendGroupMatch{id: chat.OpenConversationID, name: chat.Name}) } return out } @@ -138,38 +88,24 @@ func extractGroupsForSend(data map[string]any) []sendGroupMatch { // group wins over prefix/suffix matches. Duplicate rows for the same // openConversationId are collapsed before selection. func preferExactGroupMatches(groups []sendGroupMatch, query string) []sendGroupMatch { - unique := make([]sendGroupMatch, 0, len(groups)) - seen := make(map[string]struct{}, len(groups)) + chats := make([]targetresolver.Chat, 0, len(groups)) for _, group := range groups { - if _, ok := seen[group.id]; ok { - continue - } - seen[group.id] = struct{}{} - unique = append(unique, group) + chats = append(chats, targetresolver.Chat{OpenConversationID: group.id, Name: group.name}) } - query = strings.TrimSpace(query) - exact := make([]sendGroupMatch, 0, 1) - for _, group := range unique { - if strings.EqualFold(strings.TrimSpace(group.name), query) { - exact = append(exact, group) - } + selected := targetresolver.PreferExactChats(chats, query) + out := make([]sendGroupMatch, 0, len(selected)) + for _, chat := range selected { + out = append(out, sendGroupMatch{id: chat.OpenConversationID, name: chat.Name}) } - if len(exact) > 0 { - return exact - } - return unique + return out } func sendGroupLabels(groups []sendGroupMatch) []string { - out := make([]string, 0, len(groups)) - for _, g := range groups { - name := g.name - if name == "" { - name = "(未命名群)" - } - out = append(out, fmt.Sprintf("%s(%s)", name, g.id)) + chats := make([]targetresolver.Chat, 0, len(groups)) + for _, group := range groups { + chats = append(chats, targetresolver.Chat{OpenConversationID: group.id, Name: group.name}) } - return out + return targetresolver.ChatLabels(chats) } func init() { diff --git a/internal/shortcut/smart/thread_replies.go b/internal/shortcut/smart/thread_replies.go index 8346416d..b83d88ef 100644 --- a/internal/shortcut/smart/thread_replies.go +++ b/internal/shortcut/smart/thread_replies.go @@ -99,7 +99,10 @@ var ThreadReplies = shortcut.Shortcut{ } chatmsg.ApplyMessagePagination(payload, data, items, "older") if rt.Bool("download-resources") { - payload["resourceDownloads"] = chatshortcut.DownloadMessageResources(rt, items, rt.Str("group")) + chatshortcut.AttachMessageResourceDownloads( + payload, + chatshortcut.DownloadMessageResources(rt, items, rt.Str("group")), + ) } return rt.Output(payload) }, diff --git a/internal/shortcut/targetresolver/resolver.go b/internal/shortcut/targetresolver/resolver.go new file mode 100644 index 00000000..100a4496 --- /dev/null +++ b/internal/shortcut/targetresolver/resolver.go @@ -0,0 +1,485 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +// Package targetresolver provides deterministic natural-target resolution for +// shortcuts. It owns extraction, stable-ID de-duplication, exact-match +// preference, ambiguity handling, and the machine-readable resolution error +// envelope shared by send/read/search/create/event facades. +package targetresolver + +import ( + stderrors "errors" + "fmt" + "strings" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth" + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" +) + +// Status is the stable resolution outcome used in successful plans and errors. +type Status string + +const ( + StatusResolved Status = "resolved" + StatusAmbiguous Status = "ambiguous" + StatusNotFound Status = "not_found" +) + +// IdentityRequirement filters contacts to identities accepted by the +// downstream interface. +type IdentityRequirement string + +const ( + IdentityAny IdentityRequirement = "any" + IdentityUserID IdentityRequirement = "user_id" + IdentityOpenDingTalkID IdentityRequirement = "open_dingtalk_id" +) + +// Reader is the minimal read-only transport needed by natural target +// resolution. Shortcut RuntimeContext implements it, and native facades (for +// example event +listen-im) can provide an adapter without duplicating +// extraction or ambiguity rules. +type Reader interface { + CallMCPData(product, tool string, params map[string]any) (map[string]any, error) +} + +// User is the public, credential-free identity returned by contact resolution. +type User struct { + UserID string `json:"userId,omitempty"` + OpenDingTalkID string `json:"openDingTalkId,omitempty"` + Name string `json:"name,omitempty"` +} + +// UserResolution is the typed successful user-resolution result. +type UserResolution struct { + Status Status `json:"status"` + EntityType string `json:"entityType"` + Query string `json:"query"` + MatchType string `json:"matchType"` + Selected User `json:"selected"` + Profile string `json:"profile,omitempty"` +} + +// Chat is the public identity returned by group resolution. +type Chat struct { + OpenConversationID string `json:"openConversationId"` + Name string `json:"name,omitempty"` +} + +// ChatResolution is the typed successful group-resolution result. +type ChatResolution struct { + Status Status `json:"status"` + EntityType string `json:"entityType"` + Query string `json:"query"` + MatchType string `json:"matchType"` + Selected Chat `json:"selected"` + Profile string `json:"profile,omitempty"` +} + +// ResolveUser searches the current profile's directory and resolves query to +// exactly one identity accepted by the downstream interface. +func ResolveUser(rt Reader, query string, requirement IdentityRequirement) (UserResolution, error) { + query = strings.TrimSpace(query) + data, err := rt.CallMCPData("contact", "search_contact_by_key_word", map[string]any{ + "keyword": query, + }) + if err != nil { + return UserResolution{}, err + } + users := filterUsersByIdentity(dedupeUsers(ExtractUsers(data)), requirement) + selected, matchType := selectUsers(users, query) + if len(selected) == 0 { + return UserResolution{}, newResolutionError(StatusNotFound, "user", query, users) + } + if len(selected) > 1 { + return UserResolution{}, newResolutionError(StatusAmbiguous, "user", query, selected) + } + return UserResolution{ + Status: StatusResolved, + EntityType: "user", + Query: query, + MatchType: matchType, + Selected: selected[0], + Profile: auth.RuntimeProfile(), + }, nil +} + +// ResolveChat searches groups and resolves query to exactly one stable +// openConversationId. Exact names win over substring matches, but multiple +// exact names remain ambiguous. +func ResolveChat(rt Reader, query string) (ChatResolution, error) { + query = strings.TrimSpace(query) + data, err := rt.CallMCPData("im", "search_groups", map[string]any{ + "keyword": query, + "limit": 10, + "cursor": "0", + }) + if err != nil { + return ChatResolution{}, err + } + chats := dedupeChats(ExtractChats(data)) + selected, matchType := preferExactChats(chats, query) + if len(selected) == 0 { + return ChatResolution{}, newResolutionError(StatusNotFound, "chat", query, chats) + } + if len(selected) > 1 { + return ChatResolution{}, newResolutionError(StatusAmbiguous, "chat", query, selected) + } + return ChatResolution{ + Status: StatusResolved, + EntityType: "chat", + Query: query, + MatchType: matchType, + Selected: selected[0], + Profile: auth.RuntimeProfile(), + }, nil +} + +// ResolveUsers resolves every query before returning. Resolution failures are +// collected into one typed envelope; upstream/auth failures still stop +// immediately. Successful identities are de-duplicated by stable ID. +func ResolveUsers( + rt Reader, + queries []string, + requirement IdentityRequirement, +) ([]UserResolution, error) { + results := make([]UserResolution, 0, len(queries)) + failures := make([]map[string]any, 0) + for _, query := range uniqueQueries(queries) { + resolved, err := ResolveUser(rt, query, requirement) + if err != nil { + if details, ok := resolutionDetails(err); ok { + failures = append(failures, details) + continue + } + return nil, err + } + results = append(results, resolved) + } + if len(failures) > 0 { + return nil, batchResolutionError("user", failures) + } + seenUserIDs := map[string]bool{} + seenOpenIDs := map[string]bool{} + deduped := make([]UserResolution, 0, len(results)) + for _, result := range results { + user := result.Selected + if (user.UserID != "" && seenUserIDs[user.UserID]) || + (user.OpenDingTalkID != "" && seenOpenIDs[user.OpenDingTalkID]) { + continue + } + seenUserIDs[user.UserID] = user.UserID != "" + seenOpenIDs[user.OpenDingTalkID] = user.OpenDingTalkID != "" + deduped = append(deduped, result) + } + return deduped, nil +} + +// ResolveChats is the batch equivalent of ResolveChat and returns only after +// every query has been preflighted. +func ResolveChats(rt Reader, queries []string) ([]ChatResolution, error) { + results := make([]ChatResolution, 0, len(queries)) + failures := make([]map[string]any, 0) + for _, query := range uniqueQueries(queries) { + resolved, err := ResolveChat(rt, query) + if err != nil { + if details, ok := resolutionDetails(err); ok { + failures = append(failures, details) + continue + } + return nil, err + } + results = append(results, resolved) + } + seen := map[string]bool{} + deduped := make([]ChatResolution, 0, len(results)) + for _, result := range results { + if seen[result.Selected.OpenConversationID] { + continue + } + seen[result.Selected.OpenConversationID] = true + deduped = append(deduped, result) + } + if len(failures) > 0 { + return nil, batchResolutionError("chat", failures) + } + return deduped, nil +} + +// ExtractUsers accepts the contact search response shapes used by the current +// directory MCP and keeps external contacts that only expose openDingTalkId. +func ExtractUsers(data map[string]any) []User { + raw := firstList(data, "result", "items", "users", "list") + if len(raw) == 0 { + return nil + } + users := make([]User, 0, len(raw)) + for _, item := range raw { + row, ok := item.(map[string]any) + if !ok { + continue + } + userID := stringValue(row, "userId", "orgUserId") + openID := stringValue(row, "openDingTalkId", "openDingtalkId") + if userID == "" && openID == "" { + continue + } + users = append(users, User{ + UserID: userID, + OpenDingTalkID: openID, + Name: stringValue(row, + "name", "nick", "showName", "flowerName", "staffName", "userName"), + }) + } + return users +} + +// ExtractChats accepts both bare and wrapped group search result lists. +func ExtractChats(data map[string]any) []Chat { + raw := firstList(data, "result", "items", "groups", "list") + if len(raw) == 0 { + return nil + } + chats := make([]Chat, 0, len(raw)) + for _, item := range raw { + row, ok := item.(map[string]any) + if !ok { + continue + } + id := stringValue(row, "openConversationId", "openconversationId", "id") + if id == "" { + continue + } + chats = append(chats, Chat{ + OpenConversationID: id, + Name: stringValue(row, "title", "name", "conversationName"), + }) + } + return chats +} + +// UsersWithUserID preserves the legacy smart-shortcut helper contract. +func UsersWithUserID(users []User) []User { + return filterUsersByIdentity(users, IdentityUserID) +} + +// PreferExactChats exposes the deterministic selection helper to legacy smart +// shortcuts while they migrate to ResolveChat. +func PreferExactChats(chats []Chat, query string) []Chat { + selected, _ := preferExactChats(dedupeChats(chats), query) + return selected +} + +// UserLabels renders credential-free disambiguation labels. +func UserLabels(users []User) []string { + labels := make([]string, 0, len(users)) + for _, user := range users { + id := user.UserID + if id == "" { + id = user.OpenDingTalkID + } + labels = append(labels, fmt.Sprintf("%s(%s)", user.Name, id)) + } + return labels +} + +// ChatLabels renders group disambiguation labels. +func ChatLabels(chats []Chat) []string { + labels := make([]string, 0, len(chats)) + for _, chat := range chats { + name := chat.Name + if name == "" { + name = "(未命名群)" + } + labels = append(labels, fmt.Sprintf("%s(%s)", name, chat.OpenConversationID)) + } + return labels +} + +func selectUsers(users []User, query string) ([]User, string) { + if len(users) != 1 { + return users, "ambiguous" + } + if strings.EqualFold(strings.TrimSpace(users[0].Name), strings.TrimSpace(query)) { + return users, "exact" + } + return users, "unique" +} + +func preferExactChats(chats []Chat, query string) ([]Chat, string) { + exact := make([]Chat, 0, len(chats)) + for _, chat := range chats { + if strings.EqualFold(strings.TrimSpace(chat.Name), strings.TrimSpace(query)) { + exact = append(exact, chat) + } + } + if len(exact) > 0 { + return exact, "exact" + } + return chats, "unique" +} + +func filterUsersByIdentity(users []User, requirement IdentityRequirement) []User { + filtered := make([]User, 0, len(users)) + for _, user := range users { + switch requirement { + case IdentityUserID: + if user.UserID == "" { + continue + } + case IdentityOpenDingTalkID: + if user.OpenDingTalkID == "" { + continue + } + } + filtered = append(filtered, user) + } + return filtered +} + +func dedupeUsers(users []User) []User { + result := make([]User, 0, len(users)) + seenUserIDs := map[string]bool{} + seenOpenIDs := map[string]bool{} + for _, user := range users { + if (user.UserID != "" && seenUserIDs[user.UserID]) || + (user.OpenDingTalkID != "" && seenOpenIDs[user.OpenDingTalkID]) { + continue + } + if user.UserID != "" { + seenUserIDs[user.UserID] = true + } + if user.OpenDingTalkID != "" { + seenOpenIDs[user.OpenDingTalkID] = true + } + result = append(result, user) + } + return result +} + +func dedupeChats(chats []Chat) []Chat { + result := make([]Chat, 0, len(chats)) + seen := map[string]bool{} + for _, chat := range chats { + if seen[chat.OpenConversationID] { + continue + } + seen[chat.OpenConversationID] = true + result = append(result, chat) + } + return result +} + +func newResolutionError(status Status, entityType, query string, candidates any) error { + details := map[string]any{ + "type": "resolution", + "subtype": status, + "entityType": entityType, + "query": query, + "candidates": candidates, + } + if profile := auth.RuntimeProfile(); profile != "" { + details["profile"] = profile + } + if status == StatusNotFound { + return apperrors.NewValidation( + fmt.Sprintf("没有找到与 %q 唯一匹配且可用于当前操作的%s", query, entityLabel(entityType)), + apperrors.WithReason("resolution_not_found"), + apperrors.WithRetryable(false), + apperrors.WithHint("请提供更完整的名称或直接传稳定 ID"), + apperrors.WithDetails(details), + ) + } + return apperrors.NewValidation( + fmt.Sprintf("%q 匹配到多个%s:%s;请提供更精确的名称或直接传稳定 ID", query, entityLabel(entityType), candidateLabels(candidates)), + apperrors.WithReason("resolution_ambiguous"), + apperrors.WithRetryable(false), + apperrors.WithHint("禁止默认选择第一个候选"), + apperrors.WithDetails(details), + ) +} + +func batchResolutionError(entityType string, failures []map[string]any) error { + return apperrors.NewValidation( + fmt.Sprintf("%d 个%s目标未能唯一解析;已停止后续操作", len(failures), entityLabel(entityType)), + apperrors.WithReason("resolution_batch_failed"), + apperrors.WithRetryable(false), + apperrors.WithHint("请逐项消歧或直接传稳定 ID"), + apperrors.WithDetails(map[string]any{ + "type": "resolution", + "subtype": "batch_failed", + "entityType": entityType, + "failures": failures, + }), + ) +} + +func resolutionDetails(err error) (map[string]any, bool) { + var typed *apperrors.Error + if !stderrors.As(err, &typed) || typed.Details["type"] != "resolution" { + return nil, false + } + return typed.Details, true +} + +func entityLabel(entityType string) string { + if entityType == "chat" { + return "群聊" + } + return "用户" +} + +func candidateLabels(candidates any) string { + switch values := candidates.(type) { + case []User: + return strings.Join(UserLabels(values), "、") + case []Chat: + return strings.Join(ChatLabels(values), "、") + default: + return "候选" + } +} + +func firstList(data map[string]any, keys ...string) []any { + if data == nil { + return nil + } + scopes := []map[string]any{data} + for _, wrapper := range []string{"result", "data"} { + if nested, ok := data[wrapper].(map[string]any); ok { + scopes = append(scopes, nested) + } + } + for _, scope := range scopes { + for _, key := range keys { + if raw, ok := scope[key].([]any); ok { + return raw + } + } + } + return nil +} + +func stringValue(row map[string]any, keys ...string) string { + for _, key := range keys { + if value, ok := row[key].(string); ok && strings.TrimSpace(value) != "" { + return strings.TrimSpace(value) + } + } + return "" +} + +func uniqueQueries(queries []string) []string { + seen := map[string]bool{} + result := make([]string, 0, len(queries)) + for _, query := range queries { + query = strings.TrimSpace(query) + if query == "" || seen[query] { + continue + } + seen[query] = true + result = append(result, query) + } + return result +} diff --git a/internal/shortcut/targetresolver/resolver_test.go b/internal/shortcut/targetresolver/resolver_test.go new file mode 100644 index 00000000..0e3f8de1 --- /dev/null +++ b/internal/shortcut/targetresolver/resolver_test.go @@ -0,0 +1,75 @@ +// Copyright 2026 Alibaba Group +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. + +package targetresolver + +import ( + stderrors "errors" + "testing" + + apperrors "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/errors" +) + +func TestExtractUsersKeepsUsableExternalContacts(t *testing.T) { + users := ExtractUsers(map[string]any{ + "result": []any{ + map[string]any{"userId": "u1", "openDingTalkId": "D1", "name": "张三"}, + map[string]any{"openDingtalkId": "D2", "nick": "外部张三"}, + map[string]any{"name": "无 ID"}, + "garbage", + }, + }) + if len(users) != 2 { + t.Fatalf("users = %#v", users) + } + if users[1].OpenDingTalkID != "D2" || users[1].Name != "外部张三" { + t.Fatalf("external user = %#v", users[1]) + } +} + +func TestUserSelectionDedupesButDoesNotHideNamesakes(t *testing.T) { + users := dedupeUsers([]User{ + {UserID: "u1", OpenDingTalkID: "D1", Name: "张三"}, + {UserID: "u1", OpenDingTalkID: "D1", Name: "duplicate"}, + {UserID: "u2", OpenDingTalkID: "D2", Name: "张三丰"}, + }) + selected, matchType := selectUsers(users, " 张三 ") + if len(selected) != 2 || matchType != "ambiguous" { + t.Fatalf("selected = %#v, matchType = %q", selected, matchType) + } +} + +func TestChatSelectionKeepsMultipleExactMatchesAmbiguous(t *testing.T) { + chats := dedupeChats([]Chat{ + {OpenConversationID: "c1", Name: "项目群"}, + {OpenConversationID: "c1", Name: "项目群"}, + {OpenConversationID: "c2", Name: "项目群"}, + {OpenConversationID: "c3", Name: "项目群-归档"}, + }) + selected, matchType := preferExactChats(chats, "项目群") + if len(selected) != 2 || matchType != "exact" { + t.Fatalf("selected = %#v, matchType = %q", selected, matchType) + } +} + +func TestResolutionErrorCarriesStructuredCandidates(t *testing.T) { + err := newResolutionError(StatusAmbiguous, "chat", "项目群", []Chat{ + {OpenConversationID: "c1", Name: "项目群"}, + {OpenConversationID: "c2", Name: "项目群"}, + }) + var typed *apperrors.Error + if !stderrors.As(err, &typed) { + t.Fatalf("error type = %T", err) + } + if typed.Reason != "resolution_ambiguous" { + t.Fatalf("reason = %q", typed.Reason) + } + if typed.Details["type"] != "resolution" || typed.Details["subtype"] != StatusAmbiguous { + t.Fatalf("details = %#v", typed.Details) + } + candidates, ok := typed.Details["candidates"].([]Chat) + if !ok || len(candidates) != 2 { + t.Fatalf("candidates = %#v", typed.Details["candidates"]) + } +} diff --git a/scripts/gen_skill_shortcut_sections.py b/scripts/gen_skill_shortcut_sections.py index f352432b..9f652e08 100644 --- a/scripts/gen_skill_shortcut_sections.py +++ b/scripts/gen_skill_shortcut_sections.py @@ -24,6 +24,10 @@ import gen_shortcut_comparison as shortcut_source # noqa: E402 CATALOG_PATH = ROOT / "docs" / "shortcut-public-catalog.json" MONO_SKILL = ROOT / "skills" / "mono" / "SKILL.md" +SHARED_SKILL = ROOT / "skills" / "multi" / "dws-shared" / "SKILL.md" +RUNTIME_CONTRACT_SOURCE = ( + ROOT / "skills" / "multi" / "dws-shared" / "references" / "runtime-contract.md" +) SERVICE_TO_SKILL = { "aitable": ROOT / "skills" / "multi" / "dingtalk-aitable" / "SKILL.md", @@ -48,6 +52,8 @@ MONO_START = "" MONO_END = "" PRODUCT_START = "" PRODUCT_END = "" +RUNTIME_CONTRACT_START = "" +RUNTIME_CONTRACT_END = "" # Large, high-frequency product skills should route known intents directly and # keep their full shortcut inventory in Runtime Catalog/Schema. Add services @@ -92,6 +98,29 @@ def replace_block(text: str, start: str, end: str, block: str, fallback_anchor: return text.replace(fallback_anchor, block + "\n\n" + fallback_anchor, 1) +def replace_required_block(text: str, start: str, end: str, block: str) -> str: + if text.count(start) != 1 or text.count(end) != 1: + raise RuntimeError( + f"expected exactly one generated block {start!r} ... {end!r}" + ) + before = text.split(start, 1)[0] + after = text.split(end, 1)[1] + return before + block + after + + +def runtime_contract_block() -> str: + contract = RUNTIME_CONTRACT_SOURCE.read_text(encoding="utf-8").strip() + if not contract.startswith("## 最小 DWS 执行契约"): + raise RuntimeError( + f"runtime contract must start with its canonical heading: {RUNTIME_CONTRACT_SOURCE}" + ) + return ( + f"{RUNTIME_CONTRACT_START}\n" + f"{contract}\n" + f"{RUNTIME_CONTRACT_END}" + ) + + def mono_overview(items: list[dict[str, Any]]) -> str: counts = Counter(item["service"] for item in items) rows = [] @@ -161,6 +190,26 @@ def update_mono(items: list[dict[str, Any]], check: bool) -> list[Path]: return [MONO_SKILL] if apply_update(MONO_SKILL, text, updated, check) else [] +def update_runtime_contract(check: bool) -> list[Path]: + block = runtime_contract_block() + changed = [] + targets = [ + ROOT / "skills" / "multi" / "dingtalk-chat" / "SKILL.md", + SHARED_SKILL, + ] + for path in targets: + text = path.read_text(encoding="utf-8") + updated = replace_required_block( + text, + RUNTIME_CONTRACT_START, + RUNTIME_CONTRACT_END, + block, + ) + if apply_update(path, text, updated, check): + changed.append(path) + return changed + + def update_product_skills(items: list[dict[str, Any]], check: bool) -> list[Path]: by_service: dict[str, list[dict[str, Any]]] = defaultdict(list) for item in items: @@ -190,7 +239,8 @@ def main() -> int: args = parser.parse_args() items = collect_visible() - changed = update_mono(items, args.check) + changed = update_runtime_contract(args.check) + changed.extend(update_mono(items, args.check)) changed.extend(update_product_skills(items, args.check)) if args.check and changed: print("run: python3 scripts/gen_skill_shortcut_sections.py", file=sys.stderr) diff --git a/scripts/policy/check-skill-context-budget.sh b/scripts/policy/check-skill-context-budget.sh index 788eaea7..d840c327 100755 --- a/scripts/policy/check-skill-context-budget.sh +++ b/scripts/policy/check-skill-context-budget.sh @@ -7,8 +7,12 @@ cd "$ROOT" python3 scripts/gen_skill_shortcut_sections.py --check chat_skill="skills/multi/dingtalk-chat/SKILL.md" +event_skill="skills/multi/dingtalk-event/SKILL.md" mono_skill="skills/mono/SKILL.md" -chat_max_bytes=14000 +runtime_contract="skills/multi/dws-shared/references/runtime-contract.md" +chat_max_bytes=10000 +event_max_bytes=10000 +runtime_contract_max_bytes=3000 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 +event_bytes="$(wc -c < "$event_skill" | tr -d ' ')" +if [ "$event_bytes" -gt "$event_max_bytes" ]; then + printf '%s\n' \ + "skill context budget exceeded: $event_skill is ${event_bytes} bytes (max ${event_max_bytes})" >&2 + exit 1 +fi + +runtime_contract_bytes="$(wc -c < "$runtime_contract" | tr -d ' ')" +if [ "$runtime_contract_bytes" -gt "$runtime_contract_max_bytes" ]; then + printf '%s\n' \ + "skill context budget exceeded: $runtime_contract is ${runtime_contract_bytes} bytes (max ${runtime_contract_max_bytes})" >&2 + exit 1 +fi + shortcut_rows="$( awk ' // { in_block = 1; next } @@ -31,6 +49,56 @@ if [ "$shortcut_rows" -ne 0 ]; then exit 1 fi +for required_heading in \ + "## 最小 DWS 执行契约" \ + "## Golden Route" \ + "## 关键结果语义" \ + "## 按需加载" \ + "## 错误最短路径" +do + if ! grep -Fq "$required_heading" "$chat_skill"; then + printf '%s\n' \ + "skill value regression: $chat_skill is missing $required_heading" >&2 + exit 1 + fi +done + +for required_route in \ + "+dm" \ + "+send-to-group" \ + "+messages-send" \ + "+chat-messages" \ + "+search-msg" \ + "--download-resources" \ + "+conversation-list-top" +do + if ! grep -Fq -- "$required_route" "$chat_skill"; then + printf '%s\n' \ + "skill route regression: $chat_skill is missing $required_route" >&2 + exit 1 + fi +done + +for forbidden_route in \ + "## 标准 SOP" \ + "dws aisearch person --keyword" \ + "dws chat message send-by-webhook" \ + "dt_media_upload" \ + "MUST 先用 Read" +do + if grep -Fq "$forbidden_route" "$chat_skill"; then + printf '%s\n' \ + "skill route regression: $chat_skill restored legacy route: $forbidden_route" >&2 + exit 1 + fi +done + +if grep -Fq "../dws-shared/SKILL.md" "$chat_skill"; then + printf '%s\n' \ + "skill context regression: $chat_skill requires full dws-shared cold-start loading" >&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 +106,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 event_bytes=$event_bytes event_max=$event_max_bytes runtime_contract_bytes=$runtime_contract_bytes runtime_contract_max=$runtime_contract_max_bytes shortcut_rows=$shortcut_rows)" diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index f0d7f8ad..20c3b862 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -11,11 +11,19 @@ metadata: # 钉钉群聊 / 消息 Skill -## 前置条件 — 执行操作前必读 + +## 最小 DWS 执行契约 -> **CRITICAL — 执行任何 `dws` 操作前,MUST 先用 Read 工具完整读取 [`dws-shared`](../dws-shared/SKILL.md)。**该轻量文件包含全局执行契约、安全底线及 shared references 的按需加载导航;不要预加载其全部 references。 - -> 命令参考:[chat.md](references/chat.md);表情:[chat-emoji-list.md](references/chat-emoji-list.md);剧本:[01-messaging.md](references/01-messaging.md)。 +- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。 +- 已知命令直接执行。只有 leaf 参数或安全语义不确定时读取精确 Schema,只有 Cobra flag 不确定时读取精确 leaf Help;不要加载产品级 Catalog 代替选路。 +- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。 +- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。 +- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。 +- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;需要确认时先说明对象、动作与影响,再追加 `--yes`。 +- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。 +- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。 +- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dws-shared` 中对应 reference;不要连续猜测替代命令。 + ## Shortcut 发现(按需) @@ -25,112 +33,71 @@ metadata: 仅当现有路由和 reference 都无法定位低频能力时,才执行 `dws shortcut list --service chat --format json` 做最后回退;不要为已知高频意图加载完整 Shortcut Catalog 或产品级 Schema。 -## IM Shortcut 优先路由 +## Golden Route -- 统一发送优先用 `chat +messages-send`:通过 `--as user|bot|webhook` 选择身份,并只传该身份真实支持的目标、内容和凭据。仅在需要原子命令的特定返回结构或兼容参数时,才降级到 `chat message send` / `send-by-bot` / `send-by-webhook`。 -- 消息查询按意图选择:指定会话用 `+chat-messages`,跨维度过滤/全量翻页用 `+search-msg`,@我用 `+at-me`,已知消息 ID 批量富化用 `+messages-mget`,已知 thread/topic ID 读取回复用 `+thread-replies`。 -- 查询结果中的 `resourceRefs` 是可继续执行的资源上下文。引用、回复或合并转发中的子消息必须使用该子消息返回的 `messageId`;子消息缺会话 ID 时才继承父消息的 `openConversationId`。 -- 上述五个查询 Shortcut 与 `+messages-resource-download` 都沿用安全本地下载的 `read/not_required` 契约,不应添加 `--yes` 或触发交互确认。下载只允许工作目录内相对路径、默认不覆盖并原子落盘;需要覆盖时必须由用户显式传 `--overwrite`。 -- `+messages-send` 会按身份规范化 @ 占位符:user 使用 `<@id>` / `<@all>`,bot/webhook 使用 `@id` / `@手机号` / `@all`;只需声明 `--at-*` / `--at-all`,缺失占位符会自动补齐。 -- `+messages-send --as user --user ` 会通过通讯录关键词搜索并按 userId 精确匹配 openDingTalkId,所有内容类型与 `--dry-run` 都使用同一只读解析链路;已有 openDingTalkId 时直接传 `--open-dingtalk-id`。 -- 流式卡片优先用 `+messages-send-card`:群聊传 `--group`,单聊 userId 传 `--receiver` 并由 CLI 通过通讯录关键词搜索做 userId 精确匹配,已有 openDingTalkId 则必须显式传 `--receiver-open-dingtalk-id`;三者严格三选一,禁止根据首字母猜身份类型。 +按用户任务选择最小充分入口。公开层按意图分流;Resolver、发送执行、消息投影和错误契约在 Runtime 内复用,不把所有能力塞进一个万能命令。 -## 意图表 +| 用户意图 | 唯一推荐入口 | 关键边界 | +|---|---|---| +| 按姓名发简单文本或 Markdown | `dws chat +dm --to <姓名> --text <内容>` | CLI 解析唯一用户;多候选时停止,不先手工查 ID | +| 按群名发简单文本或 Markdown | `dws chat +send-to-group --group <群名> --text <内容>` | CLI 精确匹配优先;多候选时停止,不先手工查群 ID | +| 文件、Bot、Webhook、复杂 @、已知 ID 或自然目标的高级发送 | `dws chat +messages-send` | user 可用 `--user-query` / `--chat-query`;具体身份矩阵以 leaf Schema/Runtime 为准 | +| 读取一个指定群聊或单聊 | `dws chat +chat-messages` | ID、`--chat-query`、`--user-query` 均可;`--time` 可省略 | +| 跨会话、多维过滤或自动翻页搜索 | `dws chat +search-msg` | 支持 `--chat-query` / `--sender-query`;全量时用 `--page-all` 并检查 `complete` | +| 读取并下载消息资源 | 查询命令加 `--download-resources` | 不另起手工下载循环;下载失败项保留在结果中 | +| 查看置顶会话 | `dws chat +conversation-list-top` | 会话 Top 与消息 Pin、消息 Top、Favorite 不同 | +| 监听 IM 事件 | 切换 `dingtalk-event` | 由事件 Skill 选择确定的 EventKey 和生命周期 | -| 用户说 | 命令 | -|--------|------| -| "发消息给张三" | `dws chat +messages-send --as user --open-dingtalk-id --text "<内容>"` | -| "发到XX群" | `dws chat +chat-search --query "<群名>"` → `dws chat +messages-send --as user --chat-id --text "<内容>"` | -| "建群" / "拉人进群" | `dws chat group create` / `dws chat group members add` | -| "改群名" / "踢人" | `dws chat group rename` / `dws chat group members remove --yes`(踢人不可逆,先确认目标) | -| "@我消息" | `dws chat +at-me` | -| "查群聊记录" | `dws chat +chat-messages --group ` | -| "收藏/取消收藏这条消息" | `dws chat +flag-create` / `dws chat +flag-cancel` | -| "查看我收藏的消息" | `dws chat +flag-list` | -| "用机器人发消息" | `dws chat +messages-send --as bot --robot-code --chat-id --text "<内容>"` | -| "Webhook 推一条" | `dws chat +messages-send --as webhook --webhook-token --text "<内容>"` | -| "发送流式卡片" | `dws chat +messages-send-card --group --content "<内容>"`;单聊 userId 改用 `--receiver`,openDingTalkId 改用 `--receiver-open-dingtalk-id` | -| "撤回消息" | `dws chat message recall --conversation-id --msg-id ` | -| "标记未读 / 清除红点 / 全部已读" | `dws chat mark-unread` / `dws chat clear-red-point` / `dws chat clear-all-red-point` | -| "置顶某条消息 / 取消消息置顶" | `dws chat message set-top-msg` / `dws chat message unset-top-msg` | -| "我加入的所有群 / 全部群列表" | `dws chat group list-all` | +以下次级入口在意图明确时直接使用,不需要先加载完整 Catalog: -## 标准 SOP(必遵流程) +| 用户意图 | 入口 | +|---|---| +| 已知消息 ID 批量读取详情 | `dws chat +messages-mget` | +| 已知资源引用单独下载 | `dws chat +messages-resource-download` | +| 引用回复 | `dws chat +messages-reply`;成功结果保留新消息/会话/投递与原消息来源上下文 | +| 已知 thread/topic ID 读取回复 | `dws chat +thread-replies` | +| 按成员 ID 或姓名创建群聊 | `dws chat +chat-create`;姓名用 `--member-query`,任一歧义都会在创建前整体停止 | +| @我的消息 | `dws chat +at-me` | -> 命中以下意图**必须**按对应 SOP 顺序执行;**禁止**跳步、替换命令、编造 ID。每条命令必须带 `--format json`,执行后必须按"解析"步取真实字段(`openDingTalkId` / `openConversationId` / `openMessageId`)。 +### 发送入口边界 -### SOP-1 发消息(send-message) +- `+dm`:姓名目标的简单文本/Markdown,参数空间最小。 +- `+send-to-group`:群名目标的简单文本/Markdown,避免暴露无关身份矩阵。 +- `+messages-send`:文件、Bot、Webhook、复杂 @ 或幂等控制。user 已知 ID 可直接传,也可用 `--user-query` / `--chat-query` 运行同一只读解析链;bot/webhook 只使用下层真实支持的文本/Markdown 能力。 +- 文件直接传 `+messages-send --file <相对路径>`;不要先独立上传并提取 mediaId。 +- Webhook 使用 `+messages-send --as webhook --webhook-token `;不要退回原子 Webhook 命令。 +- 流式卡片不是普通消息内容,使用 `+messages-send-card`,并按精确 leaf Schema 选择群或单聊目标。 -**触发**:发消息/单聊/通知某人/发到群里。 +## 关键结果语义 -1. **解析收件人(必须)**:人名 → 先 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 取 `openDingTalkId`(优先)或 `userId`;群名 → 先 `dws chat search --query "<群名>" --format json` 取 `openConversationId`。 -2. **执行(必须)**:单聊 `dws chat message send --open-dingtalk-id --text "<内容>" --format json`(只有拿不到 `openDingTalkId` 时才用 `--user `);群聊 `dws chat message send --group --text "<内容>" --format json`。 -3. **验证(必须)**:发送接口成功时可能只返回 `result.openTaskId`,它可用于确认提交成功,但**不是撤回所需消息 ID**。需要撤回时必须按 SOP-7 用带 `--time` 的 `message list` 回查,取得真实 `openMessageId`;返回非 `success` 必须如实报错,不要谎报已发。 +- 发送成功返回的 `openTaskId` 是发送任务 ID,可查询投递状态,但不是回复或撤回使用的消息 ID;需要消息 ID 时从真实消息查询结果取得。 +- `+chat-messages`、`+search-msg`、`+messages-mget` 默认保留 `messageId`、conversation/thread、sender、文本、时间、reaction、引用、转发和 `resourceRefs`;`--no-reactions` 可关闭 reaction。 +- 查询结果必须检查 `complete`、`hasMore`、`failures` 和资源下载 ledger;partial result 不得表述为完整成功。 +- 引用、转发或合并消息中的子消息使用子消息自己的 `messageId`;只有子消息缺会话 ID 时才继承父消息的 `conversationId`。 +- 下载只允许工作目录内安全相对路径,默认不覆盖并原子落盘;覆盖必须由用户显式传 `--overwrite`。读取和下载不需要 `--yes`。 +- Favorite、消息 Pin、消息 Top、会话 Top 是不同对象层级,不能互换。 -**禁止**:把人名/群名直接当 ID 传入、跳过 `aisearch person`/`chat search` 解析、跳过 `--format json`、未发送成功就答复"已发送"。 +## 按需加载 -### SOP-2 建群(create-group) +只在任务命中时读取一个精确 reference: -**触发**:建群/拉人进群/新建讨论组。 +| 场景 | Reference | +|---|---| +| 复杂发送、跨会话转发、共同群或组合流程 | [01-messaging.md](references/01-messaging.md) | +| 引用、转发、卡片、reaction、Pin/Top/Favorite | [chat-message.md](references/chat/chat-message.md) | +| 建群、成员、管理员、群公告、群设置 | [chat-group.md](references/chat/chat-group.md) | +| Bot 搜索、入群、群发和撤回 | [chat-bot.md](references/chat/chat-bot.md) | +| 会话置顶、分类、红点、免打扰和隐藏 | [chat-conversation.md](references/chat/chat-conversation.md) | +| 低频意图之间仍需消歧 | [intent-guide.md](references/intent-guide.md) | +| 表情名称与 ID | [chat-emoji-list.md](references/chat-emoji-list.md) | +| 只有上述 reference 仍无法定位的原子能力 | [chat.md](references/chat.md) 的对应章节 | -1. **解析成员(必须)**:对每个成员 `dws aisearch person --keyword "<姓名>" --dimension name --format json` 取 `userId`,多人英文逗号拼接。 -2. **执行(必须)**:`dws chat group create --name "<群名>" --users --format json`;外部群加 `--type EXTERNAL`,话题圈加 `--thread`。 -3. **验证(必须)**:从返回取 `openConversationId`,可用 `dws chat search --query "<群名>" --format json` 复核。 +不要预加载这些 reference。完整 Shortcut Catalog 只在根路由和精确 reference 都无法定位低频能力时使用。 -**禁止**:跳过成员 userId 解析直接传姓名、编造 `openConversationId`。 +## 错误最短路径 -### SOP-3 Webhook 推送(send-by-webhook) - -**触发**:用机器人群 webhook 推一条消息。 - -1. **执行(必须)**:`dws chat message send-by-webhook --token --title "<标题>" --text "<内容>" --format json`。 -2. **@ 人(必须)**:需要 @ 时,`--text` 中**必须**先包含对应 `@userId` / `@手机号` / `@10`,再配合 `--at-users` / `--at-mobiles` / `--at-all`;否则 @ 不生效。 - -**禁止**:只传 `--at-users` 而 `--text` 里不含 `@<标识>`。 - -### SOP-4 共同群查询(search-common-group) - -**触发**:"我和 XX 的共同群"。 - -1. **取昵称(必须)**:先 `dws contact user get-self --format json` 取自己昵称;对方昵称从历史/上下文取,拿不到必须先问用户。 -2. **执行(必须)**:`dws chat search-common --nicks "<昵称1>,<昵称2>" --limit 20 --cursor 0 --format json`;`hasMore=true` 时**必须**用 `nextCursor` 翻页,不要停在第一页。 - -**禁止**:跳过昵称解析、忽略 `hasMore` 不翻页。 - -### SOP-5 红点 / 未读管理(manage-red-point) - -**触发**:标记未读/清除红点/全部已读。 - -1. **执行(必须)**:标记某会话未读 `dws chat mark-unread --conversation-id --format json`;清除某会话红点 `dws chat clear-red-point --conversation-id --format json`;全部已读 `dws chat clear-all-red-point --format json`。 -2. **取会话 ID(必须)**:`openConversationId` 拿不准时先 `dws chat group list-all --format json` 或 `dws chat search --query "<群名>" --format json`,**禁止**编造。 - -**禁止**:未确认会话就批量"全部已读"(破坏性,必须先与用户确认)。 - -### SOP-6 特别关注消息(focus-messages) - -**触发**:"特别关注的人最近发了什么/聊了什么"。 - -1. **执行(必须)**:`dws chat message list-focused --limit 50 --format json`,直接基于返回答复。 -2. **边界(必须)**:只有用户终点是"我关注了谁"这种**人员列表**时,才切 `dingtalk-contact` 关系查询。 - -**禁止**:用普通 `message list` 冒充 focused、把人员列表需求硬塞进 chat。 - -### SOP-7 拉取 / 撤回消息(list-or-recall-message) - -**触发**:查某个群或单聊的聊天记录、撤回某条消息。 - -1. **定位会话(必须)**:群名先 `dws chat search --query "<群名>" --format json` 取 `openConversationId`;单聊对象先解析真实 `userId` 或 `openDingTalkId`。 -2. **拉取消息(必须)**:`dws chat message list --group --time "" --direction older --format json`;单聊将 `--group` 换成 `--user` 或 `--open-dingtalk-id`。`--time` 是必填参数,必须来自用户时间范围或明确收敛后的边界。 -3. **撤回(必须)**:仅在用户明确要求撤回时,从消息列表 `result.messages[].openMessageId` 取真实 ID,执行 `dws chat message recall --conversation-id --msg-id --format json`。 - -**禁止**:省略 `--time`、用发送返回的 `clientMsgId` 代替 `openMessageId`、只传 `--client-msg-id`、编造会话或消息 ID。 - -## 跨产品协作 - -- 收件人是人名 → 先用 `dingtalk-contact` 或 `dingtalk-aisearch` 拿 `openDingTalkId` / `userId` -- 要发图片/文件 → 先 `dt_media_upload` 上传 → `python scripts/extract_media_id.py ""` 提取 mediaId → 再用 `--media-id` -- 紧急升级(应用内/短信/电话)→ 切到 `dingtalk-misc`(`references/ding.md`) -- 发邮件 → 切到 `dingtalk-mail` -## 局部意图与短流程 - -- [局部意图消歧](references/intent-guide.md);[短流程](references/lite-recipes.md)。 +1. resolution 返回零命中或多候选:停止写操作,展示候选并让用户消歧;禁止默认第一项。 +2. `unknown command` / `unknown flag`:读取精确 leaf Help,修正后最多重试一次。 +3. 参数约束或 confirmation 不清楚:读取精确 leaf Schema,以 Runtime gate 为准。 +4. 认证、权限、profile 或 confirmation 错误:读取 `dws-shared` 的对应 reference;正常 IM 不读取完整 shared Skill。 +5. 其他错误:保留真实错误和已完成/失败项;不要连续尝试同义原子命令。 diff --git a/skills/multi/dingtalk-chat/references/01-messaging.md b/skills/multi/dingtalk-chat/references/01-messaging.md index 1f043cd6..66fa6a87 100644 --- a/skills/multi/dingtalk-chat/references/01-messaging.md +++ b/skills/multi/dingtalk-chat/references/01-messaging.md @@ -1,13 +1,76 @@ -# 消息沟通 +# 消息任务级流程 -> lite recipe(`send-message`、`create-group`、`send-by-webhook`、`search-common-group`、`focus-messages`)见 [lite-recipes.md](./lite-recipes.md)。 +只在单个 Golden Route 不能完成任务、需要跨步骤传递真实结果时读取本文件。简单姓名/群名文本发送、单会话读取和跨会话搜索直接按根 Skill 执行。 -| Recipe | 行动指南(固定路线) | -|--------|-------------------| -| query-group-chat | **优先**:`python scripts/chat_export_messages.py --query "<群名>" --time "" [--no-forward] [--limit N] [--output messages.json]`(自动搜群+翻页+导出)
备选:1. `chat search --query "<群名>"` → 取 `openConversationId`
2. `chat message list --group --time ""` → 取消息列表
3. **翻页**:`hasMore=true` 时取本页最后 `createTime` 作为下次 `--time`,重复至 `hasMore=false`
4. `--forward=false` 拉给定时间**之前**的消息
5. 合并全部消息后总结 | -| query-private-chat | **优先**:`python scripts/chat_history_with_user.py --name "<姓名>" --time "" [--no-forward] [--limit N] [--output messages.json]`(自动搜人+翻页+导出)
备选:1. `aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`
2. `chat message list --user --time ""` → 取消息列表
3. **翻页**:同 query-group-chat
4. 合并全部消息后总结 | -| escalate-ding | 三级升级:
1. `ding message send --robot-code --type app --users --content "<内容>"`(必填项见 `dingtalk-misc` 的 `references/ding.md`)
2. `chat message send --group --text "<内容>"` 群里提醒(可选 `--title` / `@` 见 [chat.md](./chat.md))
3. `todo task create --title "<标题>" --executors --priority 40` 建紧急待办
前置:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`;`chat search --query "<群名>"` → 取 `openConversationId` | -| send-by-bot | **多群批量优先**:`python scripts/bot_broadcast.py --robot-code --chats , --title "<标题>" --text "<内容>"`
单群:1. `chat bot search` → 取 `robotCode`
2. `chat search --query "<群名>"` → 取 `openConversationId`
3. `chat message send-by-bot --robot-code --group --title "<标题>" --text "<内容>"` | -| forward-message | 1. `chat search --query "<群名>"` → 取 `openConversationId` → `chat message list --group --time "<起始时间>"` 拉源消息
2. `aisearch person --keyword "<姓名>" --dimension name` → 取 `openDingTalkId`(推荐);或 `chat search --query "<群名>"` → 取目标 `openConversationId`
3. `chat message send --open-dingtalk-id --text "<内容>"`(推荐)或 `--group --text "<内容>"` 发送。仅当无法获取 openDingTalkId 时才用 `--user `(备选) | -| search-common-group | `chat search-common --nicks "<昵称1>,<昵称2>" --limit 20 --cursor 0`(`--match-mode AND`=全在/`OR`=任一在,翻页:`hasMore=true` 时用 `nextCursor`)
用户说"我和XX的共同群" → nicks 包含"我"时,需先 `contact user get-self` 取自己昵称再拼接 | -| focus-messages | **零参数一行命令**:`chat message list-focused --limit 50`(拉特别关注人发的消息聚合)
触发 query:`"我特别关注的人最近发了什么消息"`、`"关注的人最近聊了啥"`、`"星标联系人最近的动态"`
**强消歧**:query 含动词【发/说/聊/讲】或名词【消息/聊天/动态】 → **必须**走本命令,**不要**先去拉 `contact relation list-my-followings`;仅当用户终点是"人员列表"(如"我关注了谁")才走 `relation list-my-followings`(详见 `dingtalk-contact` 易混淆硬规则)
翻页:`hasMore=true` 时用 `nextCursor` 作为下次 `--cursor`
按人精控(可选):先 `contact relation list-my-followings` 取 `openDingTalkId`,再 `chat message list-by-sender --sender-open-dingtalk-id --start --end ` | +## 选择路线 + +1. 先选择任务语义最窄的 Shortcut。 +2. 只有 Shortcut 暂不接受自然目标或目标类型时,才用一个只读 leaf/Shortcut 解析 ID。 +3. 解析全部完成并消歧后再写入;不要边解析边产生部分副作用。 +4. 后续步骤只使用真实返回字段,不从名称、URL 或上下文猜 ID。 + +## 群聊消息 + +已知群 ID 时直接读取: + +```bash +dws chat +chat-messages --group --format json +``` + +只有群名时,读取历史直接用 `+chat-messages --chat-query <群名>`,普通文本发送直接用 `+send-to-group`。其它尚不接受群名的高级动作才先用 `+chat-search --query <群名>`;只有唯一候选才把 `openConversationId` 传给下一步。查询结果需要资源时在读取命令上加 `--download-resources`,不要让 Agent 手工遍历资源引用。按姓名读取单聊同理使用 `+chat-messages --user-query <姓名>`。 + +## 发送消息 + +- 姓名 + 简单文本:`+dm`。 +- 群名 + 简单文本:`+send-to-group`。 +- 已知 ID、文件、Bot、Webhook、复杂 @ 或幂等:`+messages-send`。 +- 姓名 + 文件/高级控制:`+messages-send --as user --user-query <姓名> --file <相对路径>`。 +- 群名 + 文件/高级控制:`+messages-send --as user --chat-query <群名> --file <相对路径>`。 + +`--user-query` 和 `--chat-query` 会在 CLI 内运行真实只读解析;零命中或多候选时在上传或发送前停止。Bot/Webhook 不接受这两个自然目标参数。 + +文件直接交给 `+messages-send --file`。不要恢复“独立上传 → 提取 mediaId → 发送”的旧默认链路。 + +## 创建群聊 + +`+chat-create` 同时接受 `--users` 稳定 ID 和 `--member-query` 姓名/花名。自然成员解析、候选消歧、稳定 ID 去重和创建前预检都由 CLI 完成: + +```text +传入全部姓名 +→ 对零命中和多候选统一消歧 +→ 按稳定 ID 去重 +→ 全部成功后执行一次 +chat-create +``` + +任一成员未唯一解析时不会读取当前用户或创建群;`--dry-run` 也走同一解析链。不要用群名预搜索伪装幂等,因为业务上允许同名群。 + +## 机器人消息 + +已知 `robotCode` 时使用 `+messages-send --as bot`。未知机器人、机器人入群、批量群发或撤回读取 [chat-bot.md](chat/chat-bot.md)。Bot 不继承 user 的文件能力;只使用 leaf Schema 明确发布的文本/Markdown 能力。 + +## 引用与转发 + +- 引用回复:`+messages-reply`;优先继续使用结果中的 `messageId`、`conversationId`、`deliveryStatus` 和 `referencedMessage`,未知投递状态不得写成成功送达。 +- 单条转发:`+messages-forward`。 +- 合并转发:`+messages-combine-forward`。 +- 话题转发:`+messages-forward-topic`。 + +先用 `+chat-messages`、`+search-msg` 或 `+messages-mget` 取得真实 `messageId` 和 conversation/thread 上下文。引用或合并消息中的子消息优先使用自己的 `messageId`,不要拿父消息 ID 代替。 + +## 上下文传递表 + +| 上一步 | 真实返回 | 下一步用途 | +|---|---|---| +| `+chat-search` | `openConversationId` | 高级发送、读取、群管理 | +| `dingtalk-contact` 唯一用户解析 | `userId` / `openDingTalkId` | 单聊、建群、@、按发送者搜索 | +| `+messages-send` | `openTaskId` / 投递结果 | 查询投递状态;不是回复/撤回消息 ID | +| `+chat-messages` / `+search-msg` / `+messages-mget` | `messageId`、conversation/thread、`resourceRefs` | 回复、转发、撤回、资源下载 | +| `+chat-create` | `openConversationId` | 新群后续消息与群管理 | +| 分页查询 | `hasMore` / `nextCursor` / `complete` | 继续翻页和完整性判断 | + +## 完成判断 + +- 写操作检查任务级结果或可查询状态,不只看退出码。 +- 读取检查 `complete`、`hasMore` 和 `failures`。 +- 下载检查每项 ledger;单项失败不抹掉已取得消息。 +- 投递状态未知时报告 unknown 并保留幂等键,不自动换目标重发。 diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index 09cc372f..a1c683fe 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -66,7 +66,7 @@ dws chat bot --help | [chat-group](./chat/chat-group.md) | 建群、搜群、成员、机器人进群、群设置、群主、入群审批、群身份、群公告、群邀请分享 | `group *`、`group-role *`、`search`、`search-common` | | [chat-bot](./chat/chat-bot.md) | 查机器人、机器人发消息/撤回、Webhook、机器人单聊、机器人进群 | `bot *`、`message send-by-bot`、`recall-by-bot`、`send-by-webhook` | | [chat-conversation](./chat/chat-conversation.md) | 会话列表、置顶、免打扰、隐藏、红点、已读未读、清空、会话分组、智能分组 | `conversation-info`、`category *`、`mute`、`set-top`、`mark-read` | -| [chat-workflows](./chat/chat-workflows.md) | 高频复合流程、上下文传递表、openDingTalkId 获取方式、自动化脚本 | 工作流与字段传递 | +| [消息任务级流程](./01-messaging.md) | Golden Route 无法单步覆盖的复合流程与上下文传递 | 工作流与字段传递 | ## ID 获取速查 @@ -318,12 +318,12 @@ dws chat bot --help | 场景 | 入口 | |------|------| -| 搜索群并拉消息 | [chat-workflows §群聊消息](./chat/chat-workflows.md#群聊消息) | -| 个人身份发送群聊/单聊/文件 | [chat-workflows §发送消息](./chat/chat-workflows.md#发送消息) | -| 机器人发消息后撤回 | [chat-workflows §机器人消息](./chat/chat-workflows.md#机器人消息) | -| 机器人不在群内时先邀请再发送 | [chat-workflows §机器人消息](./chat/chat-workflows.md#机器人消息) | -| 给机器人发单聊 | [chat-workflows §机器人消息](./chat/chat-workflows.md#机器人消息) | -| 发送图片 / 文件 / 音频 / 视频 | [chat-workflows §发送消息](./chat/chat-workflows.md#发送消息) | +| 搜索群并拉消息 | [消息任务级流程 §群聊消息](./01-messaging.md#群聊消息) | +| 个人身份发送群聊/单聊/文件 | [消息任务级流程 §发送消息](./01-messaging.md#发送消息) | +| 机器人发消息后撤回 | [消息任务级流程 §机器人消息](./01-messaging.md#机器人消息) | +| 机器人不在群内时先邀请再发送 | [消息任务级流程 §机器人消息](./01-messaging.md#机器人消息) | +| 给机器人发单聊 | [消息任务级流程 §机器人消息](./01-messaging.md#机器人消息) | +| 发送图片 / 文件 / 音频 / 视频 | [消息任务级流程 §发送消息](./01-messaging.md#发送消息) | | 流式卡片创建与更新 | [chat-message §话题与卡片](./chat/chat-message.md#话题与卡片) | | 群公告发布与修改 | [chat-group §群公告](./chat/chat-group.md#群公告) | | 消息转待办 | 先用 [chat-message](./chat/chat-message.md#拉取消息) 取消息内容,再读取 `dingtalk-todo` 创建待办 | @@ -349,7 +349,7 @@ dws chat bot --help | `chat group-role list` | `openRoleId` | `group-role update/remove/set-user/remove-user` | | `chat message send-card` | `bizId` | `message update-card --biz-id` | -完整字段传递表见 [chat-workflows](./chat/chat-workflows.md#上下文传递表)。 +完整字段传递表见 [消息任务级流程](./01-messaging.md#上下文传递表)。 ## 相关产品 diff --git a/skills/multi/dingtalk-chat/references/chat/chat-workflows.md b/skills/multi/dingtalk-chat/references/chat/chat-workflows.md deleted file mode 100644 index 889c5e13..00000000 --- a/skills/multi/dingtalk-chat/references/chat/chat-workflows.md +++ /dev/null @@ -1,179 +0,0 @@ -# chat-workflows:高频流程与上下文传递 - -> 返回入口:[chat.md](../chat.md) - -## 适用场景 - -用于复合任务编排、字段传递、openDingTalkId 获取、自动化脚本和跨产品衔接。 - -## 必读约束 - -- 已知上游字段时直接使用,不要重复搜索。 -- 群聊 `openConversationId` 通常来自 `chat search`;单聊 `openConversationId` 来自 `conversation-info`。 -- `userId` 与 `openDingTalkId` 不是同一种标识,按命令要求传递。 - -## openDingTalkId 获取方式 - -| 场景 | 命令 | -|------|------| -| 搜人员 openDingTalkId | `dws aisearch person --keyword "姓名" --dimension name --format json` | -| 给机器人发单聊 | `dws chat bot find --query "机器人名" --format json`,不要用 `bot search` | -| 已有 userId 需要单聊会话 ID | `dws chat conversation-info --user --format json` | -| 已有 openDingTalkId 需要单聊会话 ID | `dws chat conversation-info --open-dingtalk-id --format json` | - -## 高频工作流 - -### 群聊消息 - -```bash -# 1. 搜索群,提取 openConversationId -dws chat search --query "项目冲刺" --format json - -# 2. 拉取群消息 -dws chat message list --group --time "2025-03-01 00:00:00" --direction older --format json - -# 3. 拉未读会话 -dws chat message list-unread-conversations --count 20 --format json -``` - -### 发送消息 - -```bash -# 个人身份发群消息 -dws chat message send --group --title "周报提醒" --text "请大家本周五前提交周报" --uuid --format json - -# 个人身份发单聊 -dws chat message send --user --text "你好" --uuid --format json -dws chat message send --open-dingtalk-id --text "你好" --uuid --format json - -# 发送图片/文件/音频/视频 -dws chat message send --group --msg-type file --file-path ./screenshot.png --format json -dws chat message send --open-dingtalk-id --msg-type file --file-path ./report.pdf --format json -dws chat message send --group --msg-type audio --file-path ./voice.mp3 --format json -dws chat message send --group --msg-type video --file-path ./demo.mp4 --format json - -# 文件后补文字说明 -dws chat message send --open-dingtalk-id --msg-type file --file-path ./screenshot.png --format json -dws chat message send --open-dingtalk-id --text "这是本周数据汇总" --format json -``` - -### 机器人消息 - -```bash -# 查我的机器人,提取 robotCode -dws chat bot search --name "日报" --format json - -# 机器人发群消息,提取 processQueryKey;Markdown 稳定换行用 \n\n,不要只用 \n -dws chat message send-by-bot --robot-code --group --title "日报" --text "## 今日完成\n\n- 事项 A\n\n- 事项 B" --format json - -# 撤回机器人消息 -dws chat message recall-by-bot --robot-code --group --keys --format json - -# 机器人不在群内时先邀请 -dws chat group members add-bot --id --robot-code --format json -dws chat message send-by-bot --robot-code --group --title "通知" --text "内容" --format json - -# 给机器人发单聊,必须用 find 拿 openDingTalkId -dws chat bot find --query "玉澜" --format json -dws chat message send --open-dingtalk-id --text "你好" --format json -``` - -### 机器人 @ 指定人 - -```bash -dws aisearch person --keyword "张三" --dimension name --format json - -dws chat message send-by-bot --robot-code --group \ - --at-user-ids userId1,userId2 \ - --title "提醒" --text "@userId1 @userId2 请查收本周报告" --format json - -dws chat message send-by-bot --robot-code --group \ - --at-open-dingtalk-ids openDingtalkId1,openDingtalkId2 \ - --title "提醒" --text "@openDingtalkId1 @openDingtalkId2 请查收本周报告" --format json - -dws chat message send-by-bot --robot-code --group \ - --at-all --title "通知" --text "请所有人注意" --format json -``` - -### Webhook 告警 - -```bash -dws chat message send-by-webhook --token --title "告警" --text "CPU 超 90% @10" --at-all --format json -``` - -### 话题完整读取与回复 - -```bash -dws chat message list --group --time "2026-03-10 00:00:00" --direction older --format json -dws chat message list-topic-replies --group --topic-id --format json -dws chat message send --group --text "回复话题内容" --format json -``` - -### 群公告与邀请分享 - -```bash -# 分享 A 群邀请链接到 B 群 -dws chat group share-invite --source --target --format json - -# 发布群公告,Markdown 段落换行用空行 -dws chat group notice create --group --content "# 重要通知\n\n请大家查收" --send-ding --format json - -# 修改公告前先列表查询 notice-id/dataId -dws chat group notice list --group --format json -dws chat group notice edit --group --notice-id --content "完整的新公告内容" --format json -``` - -### 智能分组与文本翻译 - -```bash -dws aisearch person --keyword "张三" --dimension name --format json -dws chat category create-smart --name "重点群" --keywords "重点,项目" --members openDingTalkId1 --format json -dws chat text translate --query "你好世界" --to en_US --format json -``` - -## 上下文传递表 - -| 操作 | 从返回中提取 | 用于 | -|------|-------------|------| -| `chat search` | `openConversationId` | `message send/list`、`group members`、`set-top`、`group-mute` | -| `chat group create` | `openConversationId` | 新群后续发消息、成员管理、群设置 | -| `chat group get-by-group-id` | `openConversationId` | 将数字群号转为可用会话 ID | -| `chat conversation-info` | `openConversationId` | 单聊消息搜索、转发、会话状态操作 | -| `aisearch person` | `userId` | `message send --user`、`send-by-bot --users`、`--at-user-ids`、`list-by-sender --sender-user-id` | -| `aisearch person` | `openDingTalkId` | `message send --open-dingtalk-id`、`--at-open-dingtalk-ids`、`send-by-bot --open-dingtalk-ids` | -| `aisearch person` | `userId` | 可替代人员搜索结果,继续用于 userId 参数 | -| `chat bot search` | `robotCode` | `send-by-bot`、`recall-by-bot` | -| `chat bot find` | `openDingTalkId` | 给机器人发单聊 | -| `chat group bots` | `openBotId` | `group members remove-bot --bot-id` | -| `chat message send-by-bot` | `processQueryKey` | `recall-by-bot --keys` | -| `chat message send` | `openTaskId` | `query-send-status --open-task-id` | -| `chat message list` | `openMessageId` | `message recall --msg-id`、`reply --ref-msg-id`、`forward --msg-id` | -| `chat message list` | `openMsgId` | `read-status --message-id`、表情回应命令 | -| `chat message list` | `openConvThreadId` | `list-topic-replies --topic-id`、话题回复 `message send --group` | -| `chat message search` | `nextCursor` | 下次 `message search --cursor` | -| `chat message search-advanced` | `nextCursor` | 下次 `search-advanced --cursor` | -| `chat message list-all` | `nextCursor` | 下次 `list-all --cursor` | -| `chat search-common` | `openConversationId` | 后续消息发送/拉取 | -| `chat group notice list/get/create` | `dataId` / `notice-id` | `group notice get/edit --notice-id` | -| `chat group-role list` | `openRoleId` | `group-role update/remove/set-user/remove-user --role-id` | -| `chat message create-text-emotion` | `emotionId` | `add-text-emotion --emotion-id` | -| `chat message list/search-advanced` | `openMessageId` / `openMsgId` | `list-emotion-replies --msg-ids` | -| `chat category list` | `categoryId` | `category list-conversations/add-conv/remove-conv --category-id(s)` | -| `chat category create-smart` | 智能分组 ID/规则结果 | 后续查看或调整智能分组 | -| `chat message send-card` | `bizId` | `update-card --biz-id` | - -## 自动化脚本 - -| 脚本 | 场景 | 用法 | -|------|------|------| -| [chat_export_messages.py](../../scripts/chat_export_messages.py) | 导出群聊消息到 JSON 文件 | `python chat_export_messages.py --query "项目冲刺" --time "2026-03-10 00:00:00"` | -| [chat_history_with_user.py](../../scripts/chat_history_with_user.py) | 查询与某人的单聊聊天记录 | `python chat_history_with_user.py --name "张三" --time "2026-03-10 00:00:00"` | -| [extract_media_id.py](../../scripts/extract_media_id.py) | 旧链路:从 dt_media_upload URL 提取 mediaId | 新场景不要用,文件/音视频直接 `--msg-type file|audio|video --file-path` | - -## 常见错误与回退 - -- 字段已由上游返回:直接传递,不要再次搜索。 -- `chat bot search` 没有 openDingTalkId:改用 `chat bot find`。 -- 群号是纯数字:先 `chat group get-by-group-id`。 -- 翻页不要自造 cursor:使用响应中的 `nextCursor` 原值。 -- 文件/音视频发送不要走旧上传链路:优先 `message send --msg-type file|audio|video --file-path`;`audio/video` 底层按 `file` 发送。 diff --git a/skills/multi/dingtalk-chat/references/intent-guide.md b/skills/multi/dingtalk-chat/references/intent-guide.md index 7fa79ebf..6f64a323 100644 --- a/skills/multi/dingtalk-chat/references/intent-guide.md +++ b/skills/multi/dingtalk-chat/references/intent-guide.md @@ -1,88 +1,51 @@ -# chat 局部意图消歧 +# Chat 低频意图消歧 -本文件从单 Skill `intent-guide.md` 拆分而来,仅保留与本产品相关的跨产品消歧规则。 +只在根 Skill 的 Golden Route 无法区分相邻能力时读取。命令 flags 和安全语义以精确 leaf Schema/Runtime 为准。 -| 用户说... | 真实意图 | 应该用 | 不要用 | 理由 | -|---|---|---|---|---| -| "通知群里的人都来开会" | 个人身份群发 | `chat message send` | `chat message send-by-bot` | 以个人身份向群发消息 | -| "让机器人每天推送日报" | 机器人定时推送 | `chat message send-by-bot` | `chat message send` | 需要机器人身份定期发送 | -| "CPU 超过 90% 自动告警" | Webhook 告警 | `chat message send-by-webhook` | `chat message send-by-bot` | 系统告警场景,需自定义 Webhook | -| "拉取一下上周项目群的聊天记录" | 拉取会话消息 | `chat message list` | — | 拉取指定群聊的消息列表 | -| "看看张三发给我的消息" | 按发送者查询消息 | `chat message list-by-sender` | `chat message list --user` | 用户未明确说"单聊"时优先用 list-by-sender(跨单聊/群聊) | -| "拉取和张三的单聊记录" | 拉取单聊消息 | `chat message list --user` | `chat message list-by-sender` | 用户明确说"单聊"时用 list --user | -| "谁@了我/查看提及我的消息" | 查询@我的消息 | `chat message list-mentions` | `chat message list-all` | 都是跨会话时间范围查询,但 list-mentions 只返回@我的消息 | -| "查看我今天的所有消息" | 全量会话消息 | `chat message list-all` | `chat message list` | 用户未指定具体会话时用 list-all(跨所有会话),指定了具体群或人时用 list | -| "搜一下消息里的changefree链接" | 消息搜索 | `chat message search-advanced`(首选) | `chat search` | 推荐首选 search-advanced,它是 search 的严格超集(keyword 可选、支持多群、可叠加发送者/at 维度) | -| "按发送者搜索/指定多个群搜索/多维度搜消息" | 多维度搜索消息 | `chat message search-advanced`(首选) | `chat message search` | 推荐首选,支持关键词、发送者、@我、多个会话等维度组合 | -| "消息发没发成功/查询消息发送状态" | 查询消息发送状态 | `chat message query-send-status` | — | 需要 send 返回的 openTaskId | -| "撤回我发的消息/撤回消息/群主撤回消息/管理员撤回他人消息" | 撤回消息 | `chat message recall` | `chat message recall-by-bot` | recall 撤回个人消息或群主/管理员撤回他人消息,recall-by-bot 撤回机器人消息 | -| "未读消息会话/未读会话列表/我的未读会话" | 未读会话列表 | `chat message list-unread-conversations` | `chat message read-status` | list-unread-conversations 查哪些会话有未读;read-status 查具体消息的已读状态 | -| "谁看了这条消息/消息已读未读/查读状态" | 查询消息已读状态 | `chat message read-status` | `chat message list-unread-conversations` | read-status 查具体消息的已读人员;list-unread-conversations 查未读会话列表 | -| "查看消息的表情回复/拉取消息回复/消息的文字回复" | 批量拉取消息表情回复和文字回复 | `chat message list-emotion-replies` | — | 根据消息 ID 批量查询表情回复和文字回复 | -| "我和风雷的共同群/我们都在哪些群" | 搜索共同群 | `chat search-common` | `chat search` | search-common 按人员搜共同群,chat search 按群名搜索 | -| "查看会话分组/自定义分组" | 获取会话分组 | `chat category list` | — | 获取用户自定义的会话分组列表 | -| "某个分组下有哪些会话" | 分组下会话列表 | `chat category list-conversations` | — | 需先通过 category list 获取分组 ID | -| "创建会话分组/新建分组" | 创建自定义分组 | `chat category create` | — | 需指定 --title 分组名称 | -| "删除会话分组/移除分组" | 删除自定义分组 | `chat category delete` | — | 需指定 --category-id | -| "重命名分组/修改分组名称" | 更新分组名称 | `chat category rename` | — | 需指定 --category-id 和 --title | -| "把会话加到分组/将群移入分组" | 会话加入分组 | `chat category add-conv` | `chat category remove-conv` | add-conv 加入;remove-conv 移出 | -| "把会话从分组移出/从分组中删除" | 会话移出分组 | `chat category remove-conv` | `chat category add-conv` | remove-conv 移出;add-conv 加入 | -| "创建智能分组/新建智能会话分组/按关键词分组" | 创建智能会话分组 | `chat category create-smart` | `chat category create` | create-smart 按关键词/成员智能匹配;create 手动创建普通分组 | -| "分享群链接/把群分享给某人/群链接发到某群" | 分享群聊链接到会话 | `chat group share-invite` | `chat group invite-url` | share-invite 直接分享到目标会话/用户;invite-url 只获取链接不发送 | -| "根据群号查群信息/群号转openConversationId" | 群号查群聊信息 | `chat group get-by-group-id` | `chat search` | 已知数字群号时直接查;用户发消息只给了群号时,先用此工具将群号转为 openConversationId | -| "我创建的群/我管理的群/我是群主的群/我当管理员的群" | 拉取我创建/管理的群 | `chat group list-my-groups` | `chat search` | list-my-groups 按角色(群主/管理员)过滤当前用户的群;chat search 按关键词搜全部群 | -| "引用消息回复/回复那条消息" | 引用回复消息 | `chat message reply` | `chat message send` | reply 引用指定消息回复;send 普通发消息不引用 | -| "转发消息/把消息转到另一个群" | 转发单条消息 | `chat message forward` | `chat message send` | forward 转发已有消息到目标会话;send 是发新消息 | -| "置顶会话/取消置顶" | 设置/取消会话置顶 | `chat set-top` | `chat list-top-conversations` | set-top 设置或取消置顶;list-top-conversations 查看置顶列表 | -| "全员禁言/群禁言" | 全员禁言或解除 | `chat group-mute` | `chat group-mute-member` | group-mute 全员禁言或解除;group-mute-member 指定成员禁言 | -| "禁言某人/指定成员禁言" | 指定成员禁言 | `chat group-mute-member` | `chat group-mute` | group-mute-member 指定成员禁言/解禁;group-mute 全员禁言 | -| "查看群禁言配置/群里谁被禁言/禁言名单" | 查询群用户禁言配置 | `chat group get-mute-config` | `chat group-mute-member` | get-mute-config 是只读查询,返回黑白名单及时间配置;group-mute-member 会修改禁言状态 | -| "设管理员/取消管理员" | 设置群管理员 | `chat group set-admin` | `chat group invite` | set-admin 设置或取消管理员角色;invite 是邀请入群 | -| "退群/退出群聊/离开群" | 退出群聊 | `chat group quit` | `chat group members remove` | quit 是当前用户自己退群;members remove 是管理员踢别人 | -| "改群头像/更新群头像" | 更新群头像 | `chat group update-icon` | `chat group rename` | update-icon 改群头像;rename 改群名称 | -| "改群设置/群设置开关/群权限/入群许可/禁止私聊" | 更新管理员级别群功能开关 | `chat group update-settings` | `chat group set-admin` | update-settings 改管理员级别的群功能开关;set-admin 是管理员角色操作 | -| "改群昵称/设置群昵称/我在群里的名字" | 设置个人群昵称 | `chat group update-nick` | `chat group rename` | update-nick 改自己的群昵称;rename 改群名称 | -| "群备注/给群加备注/修改群备注" | 设置群备注 | `chat group update-alias` | `chat group rename` | update-alias 设置仅自己可见的备注;rename 改群名称全员可见 | -| "我的群置顶/取消我的群置顶/置顶这个群" | 设置当前用户群会话置顶 | `chat group user-settings set` | `chat group update-settings` | user-settings set 是个人会话置顶;update-settings 是管理员级别的群功能开关 | -| "我的群免打扰/关闭我的群免打扰/这个群别提醒我" | 设置当前用户群免打扰 | `chat group user-settings set` | `chat group update-settings` | user-settings set 是个人通知偏好;update-settings 是管理员级别的群功能开关 | -| "隐藏会话/隐藏群聊/隐藏对话" | 隐藏会话 | `chat hide` | `chat mute` | hide 隐藏会话不显示;mute 是免打扰但仍显示 | -| "关闭@所有人通知/屏蔽@all/不接收@所有人" | 关闭 @所有人提醒 | `chat mute-at-all` | `chat mute` | mute-at-all 仅屏蔽 @所有人;mute 是整个会话免打扰 | -| "关闭红包通知/屏蔽红包/不接收红包提醒" | 关闭红包提醒 | `chat mute-red-envelope` | `chat mute` | mute-red-envelope 仅屏蔽红包;mute 是整个会话免打扰 | -| "解散群/解散群聊" | 解散群聊 | `chat group dismiss` | `chat group quit` | dismiss 是群主解散整个群(不可逆);quit 是当前用户自己退群 | -| "新成员看历史/历史消息可见范围" | 设置新成员可见历史消息 | `chat group set-history` | `chat group update-settings` | set-history 控制新成员入群后可见历史消息范围;update-settings 是管理员级别的其他群功能开关 | -| "群里有哪些机器人/查看群机器人/列出群机器人" | 查看群内机器人列表 | `chat group bots` | `chat group members` | bots 只列机器人;members 列普通群成员 | -| "从群里移除机器人/踢机器人" | 移除群内机器人 | `chat group members remove-bot` | `chat group members add-bot` | remove-bot 通过 openBotId 移除;add-bot 通过 robotCode 添加 | -| "批量查群成员详情/按ID查群成员/查看指定成员信息" | 批量查询群成员详情 | `chat group members list-by-ids` | `chat group members` | list-by-ids 根据 openDingTalkId 列表查询指定成员详情;members 分页查询全部成员列表 | -| "标记未读/设为未读/会话标未读" | 标记会话为未读 | `chat mark-unread` | `chat clear-red-point` | mark-unread 标记为未读;clear-red-point 清除红点(相反操作) | -| "清除红点/已读/取消未读" | 清除会话红点 | `chat clear-red-point` | `chat mark-unread` | clear-red-point 清除单个会话红点;mark-unread 标记未读(相反操作) | -| "全部已读/一键清除所有未读/红点清零" | 清除所有会话红点 | `chat clear-all-red-point` | `chat clear-red-point` | clear-all-red-point 清除所有会话红点;clear-red-point 只清单个会话 | -| "所有会话/全部会话列表/我的会话" | 分页获取全部会话 | `chat list-all-conversations` | `chat list-top-conversations` | list-all-conversations 返回所有会话;list-top-conversations 仅返回置顶会话 | -| "清空聊天记录/删除会话消息/清空消息" | 清空会话聊天记录 | `chat clear-messages` | `chat clear-red-point` | clear-messages 清空聊天记录;clear-red-point 仅清除未读红点 | -| "标记已读/消息已读/读了" | 标记消息已读 | `chat mark-read` | `chat clear-red-point` | mark-read 标记指定消息及之前的消息为已读;clear-red-point 仅清除红点不标记消息 | -| "我加入的所有群/拉取全部群列表/我的群" | 分页拉取所有群 | `chat group list-all` | `chat group list-my-groups` | list-all 返回用户加入的所有群;list-my-groups 仅返回用户作为群主/管理员的群 | -| "入群申请记录/谁申请入群/群申请列表/查看入群审批" | 拉取入群验证记录 | `chat group list-join-validations` | `chat group list-all` | list-join-validations 拉取入群验证/审批记录;list-all 拉取群列表 | -| "通过入群申请/拒绝入群/审批入群/同意加群" | 审批入群验证 | `chat group audit-join-validation` | `chat group list-join-validations` | audit-join-validation 执行审批操作;list-join-validations 仅查询记录 | -| "发群公告/在群里发公告/群公告置顶" | 发布群公告 | `chat group notice create` | `blackboard create` | notice create 是群维度公告(发到指定群聊);blackboard 是企业公告(面向全员,不可撤回) | -| "改群公告/修改群公告/更新群公告" | 修改群公告 | `chat group notice edit` | `chat group notice create` | edit 整体替换已有公告内容(需 dataId);create 发布新公告 | -| "查群公告/看群公告/群公告列表/群里有什么公告" | 查看群公告列表 | `chat group notice list` | `blackboard list` | notice list 查指定群的公告;blackboard list 查企业公告 | -| "群公告详情/公告已读人数/谁读了公告" | 查看群公告详情 | `chat group notice get` | `chat group notice list` | get 返回单条公告详情(含已读人数、点赞/评论数);list 分页拉公告列表 | -| "搜索机器人/找机器人/查机器人/帮我找XXX机器人" | 搜索全部可用机器人 | `chat bot find` | `chat bot search` | find 返回全部可用机器人(含他人/官方),额外返回 openDingTalkId(可用于给机器人发单聊消息);search 仅返回我创建的 | -| "给机器人发单聊/给机器人发消息/跟机器人聊天" | 给机器人发单聊消息 | `chat bot find` → `chat message send --open-dingtalk-id` | `chat bot search` | 必须先用 find 拿 openDingTalkId(search 没有此字段),再用 send --open-dingtalk-id 发单聊 | -| "我创建的机器人/我的机器人/我自己的机器人/查看我的机器人" | 搜索我创建的机器人 | `chat bot search` | `chat bot find` | search 仅返回当前用户自己创建的机器人(返回 robotCode + robotName,无 openDingTalkId);find 返回全部可用机器人 | -| "合并转发/批量转发/合并转发多条消息" | 合并转发多条消息 | `chat message combine-forward` | `chat message forward` | combine-forward 合并多条为一条转发;forward 转发单条消息 | -| "转发话题/转发话题消息/话题转发到另一个群" | 转发话题消息 | `chat message forward-topic` | `chat message forward` | forward-topic 专用于转发话题消息(需要话题ID);forward 转发普通单条消息 | -| "发卡片消息/推送流式卡片" | 创建并推送流式卡片 | `chat message send-card` | `chat message send` | send-card 发流式卡片;send 发普通文本/Markdown 消息 | -| "更新卡片/流式更新卡片" | 流式更新卡片内容 | `chat message update-card` | `chat message send-card` | update-card 更新已有卡片;send-card 创建新卡片 | -| "钉住消息/Pin消息/置顶消息到会话" | 钉住消息 | `chat message set-pin-msg` | `chat set-top` | set-pin-msg 钉住单条消息(Pin);set-top 置顶整个会话 | -| "取消钉住/Unpin消息" | 取消钉住消息 | `chat message unset-pin-msg` | `chat message set-pin-msg` | unset-pin-msg 取消钉住;set-pin-msg 设置钉住 | -| "查看钉住的消息/Pin列表/钉住消息列表" | 拉取钉住消息列表 | `chat message list-pin-msg` | `chat message list` | list-pin-msg 只返回被钉住的消息;list 拉取全部消息 | -| "置顶某条消息/把这条消息置顶" | 置顶消息 | `chat message set-top-msg` | `chat set-top` | set-top-msg 置顶会话内某条消息;set-top 置顶整个会话在列表中 | -| "取消置顶消息/取消消息置顶" | 取消置顶消息 | `chat message unset-top-msg` | `chat message set-top-msg` | unset-top-msg 取消置顶;set-top-msg 设置置顶 | -| "DING消息/查DING/DING历史" | 查询 DING 消息列表 | `ding message list` | `chat message list` | ding 是独立顶层命令;ding message list 查 DING 消息;chat message list 查普通聊天消息 | -| "DING接收状态/谁收到了DING" | DING 接收状态 | `ding message receiver-status` | `chat message read-status` | ding 是独立顶层命令;receiver-status 查 DING 接收;chat message read-status 查普通消息已读 | -| "发DING/DING通知" | 发送 DING 消息 | `ding message send` | `chat message send` | DING 是钉钉的强提醒(应用内/短信/电话),独立顶层命令;普通群消息用 chat | -| "撤回DING" | 撤回 DING 消息 | `ding message recall` | `chat message recall` | DING 撤回独立命令;chat recall 是撤回普通聊天消息 | -| "搜一下智能化方案/最近 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 仅支持纯文本,需先提取文档内容 | +## 消息选择 + +| 用户终点 | 选择 | 不要混用 | +|---|---|---| +| 给姓名发简单文本 | `+dm` | 先查人再原子发送 | +| 给群名发简单文本 | `+send-to-group` | 先搜群再原子发送 | +| 文件、Bot、Webhook、复杂 @、幂等 | `+messages-send` | 为不同身份各走一套原子入口 | +| 浏览指定会话 | `+chat-messages` | 跨会话搜索命令 | +| 跨会话或多维过滤 | `+search-msg` | 单会话浏览命令 | +| 已知消息 IDs 取详情 | `+messages-mget` | 重新搜索关键词 | +| @我的消息 | `+at-me` | 全量消息后本地猜测 @ | +| 已知 thread/topic ID 的回复 | `+thread-replies` | 普通消息列表 | +| 引用回复一条消息 | `+messages-reply` | 普通发送 | +| 单条/合并/话题转发 | `+messages-forward` / `+messages-combine-forward` / `+messages-forward-topic` | 复制正文重新发送 | +| 流式卡片创建或更新 | `+messages-send-card` / `+messages-update-card` | 普通 text/Markdown 发送 | + +## 对象层级 + +| 用户终点 | 对象 | Reference | +|---|---|---| +| 收藏或取消收藏 | 当前用户的 Favorite | [chat-message.md](chat/chat-message.md) | +| Pin/Unpin 一条消息 | 消息 Pin | [chat-message.md](chat/chat-message.md) | +| 置顶/取消置顶一条消息 | 消息 Top | [chat-message.md](chat/chat-message.md) | +| 置顶/取消置顶整个会话 | 会话 Top | [chat-conversation.md](chat/chat-conversation.md) | +| 查看置顶会话 | 会话列表 | `+conversation-list-top` | +| 标记消息已读 | 消息读取状态 | [chat-message.md](chat/chat-message.md) | +| 清红点、标记会话未读 | 会话状态 | [chat-conversation.md](chat/chat-conversation.md) | + +Favorite、消息 Pin、消息 Top 和会话 Top 不能互换,即使用户都说“收藏/钉住/置顶”。 + +## 群与机器人 + +| 用户终点 | 选择 | +|---|---| +| 已有成员 IDs 创建群 | `+chat-create` | +| 加人、踢人、管理员、群公告、群设置 | [chat-group.md](chat/chat-group.md) | +| 找可用机器人并取得单聊 ID | `chat bot find`,不是只查自己创建机器人的 `bot search` | +| 已知 robotCode 发送 | `+messages-send --as bot` | +| 机器人入群、移除、批量群发或撤回 | [chat-bot.md](chat/chat-bot.md) | + +## 跨产品边界 + +- 紧急 DING、短信或电话:切 `dingtalk-misc`,不要当普通 Chat 消息。 +- 邮件:切 `dingtalk-mail`。 +- 只查询人员资料或把姓名解析成 ID:切 `dingtalk-contact`;若终点只是简单发消息,直接留在 Chat 使用 `+dm`。 +- 企业知识跨文档、消息、邮件搜索:切 `dingtalk-aisearch`;明确只搜聊天消息时使用 `+search-msg`。 +- 文档翻译先由文档产品读取正文;`chat text translate` 只处理纯文本。 diff --git a/skills/multi/dingtalk-chat/references/lite-recipes.md b/skills/multi/dingtalk-chat/references/lite-recipes.md index 09f1c2ae..99b76138 100644 --- a/skills/multi/dingtalk-chat/references/lite-recipes.md +++ b/skills/multi/dingtalk-chat/references/lite-recipes.md @@ -1,7 +1,5 @@ -# chat Lite Recipe +# Chat Lite Recipe 导航 -本文件从单 Skill `lite-recipes.md` 拆分而来,仅保留与本产品相关的轻量流程。 +简单发送、读取和搜索直接使用根 Skill 的 Golden Route,不需要加载 recipe。 -## #1 消息沟通 - -所有消息沟通相关的命令详情、参数说明、意图路由和复合工作流,请查阅 [chat.md](./chat.md)。 +只有任务需要多步传递真实 ID、消息上下文或资源结果时,读取 [01-messaging.md](01-messaging.md)。群管理、Bot、消息对象和会话状态分别读取根 Skill 指向的精确 reference。 diff --git a/skills/multi/dingtalk-event/SKILL.md b/skills/multi/dingtalk-event/SKILL.md index d621f82c..8f080737 100644 --- a/skills/multi/dingtalk-event/SKILL.md +++ b/skills/multi/dingtalk-event/SKILL.md @@ -1,254 +1,100 @@ --- name: dingtalk-event -description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖消息接收、全部单聊/群消息、指定发送人、已读、撤回、表情回应和群生命周期,输出 NDJSON 到 stdout。Use when 用户提到 监听个人消息事件、监听所有单聊或群消息、被@消息、监听单聊或群消息、监听某人发送的消息、监听消息已读、监听消息撤回、监听消息贴表情或表情回应、监听群成员加入、监听群成员退出、监听群改名或群解散、实时接收钉钉事件、用事件驱动 Agent。命令前缀:dws event。 +description: 钉钉个人 IM 事件长连接监听、订阅与消费,覆盖 @我、指定发送人、指定群、全部单聊/群消息,以及消息已读、撤回、表情回应和群生命周期。Use when 用户提到实时监听消息、被@消息、监听某人或某群、监听已读/撤回/表情回应、群成员加入、群成员退出、群状态变化、事件驱动 Agent。命令前缀:dws event。 --- # 钉钉个人 IM 事件 -只使用 `dws event consume` 建立个人消息事件长连接。用户要求实时监听、订阅、自动回复或驱动 Agent 时,不要写轮询脚本,不要用消息历史查询模拟事件。 +实时监听必须使用事件长连接,不写轮询脚本,不用历史消息查询模拟事件。高频 IM 意图优先交给 `dws event +listen-im`;它在 CLI 内解析自然目标、选择 EventKey,并复用现有订阅与 bus 生命周期。 -## 运行方式 +## Golden Route -- bus 后台进程持有对钉钉的个人 Stream 长连;consume 从 bus 读事件、按 NDJSON 打到 stdout。consume 只读,不发消息(回复用 `dws chat message send`)。 -- 没有 bus 时 consume 自动拉起;通常只跑 consume。 -- 一个组织一个 bus,可同时跑;同组织内多个 consume 共享一个 bus。 -- 非默认组织加全局 `--profile `;漏传会退回默认 profile 而失败。 - -## Core commands - -| Command | Purpose | +| 用户意图 | 唯一推荐入口 | |---|---| -| `dws event list` | 查看当前个人事件目录;不要把它当能力菜单主动展示 | -| `dws event schema --flatten` | 查看 Agent 使用的顶层业务字段 schema,默认 JSON | -| `dws event consume [event_key...] --flatten [flags]` | 阻塞消费一个或多个兼容事件;事件写到 stdout,推荐 `-f ndjson` | -| `dws event status --event ` | 查看个人订阅、personal bus 和本地 consume | -| `dws event stop --dry-run` / `--yes` | 先预览,再确认取消个人订阅并停止对应本地消费 | -| `dws event stop --all --dry-run` / `--yes` | 先预览,再确认清理当前身份下本地记录的全部个人订阅 | +| 监听 @我的消息 | `dws event +listen-im --kind at-me` | +| 监听某人发来的消息 | `dws event +listen-im --kind sender --user-query <姓名>` | +| 监听指定群消息 | `dws event +listen-im --kind group --chat-query <群名>` | +| 同一人/群的消息、表情、已读或撤回 | `dws event +listen-im --kind --events message,reaction,read,recall ...` | +| 监听全部单聊或全部群消息 | `dws event +listen-im --kind `;只有用户明确要求“全部”时使用 | +| 群改名、成员进退、群解散 | 读取 [references/event-im.md](references/event-im.md),使用精确 `event consume` EventKey | +| 已知 EventKey 或需要底层订阅控制 | `dws event consume`;参数与约束以 leaf Schema 为准 | +| 查看状态 / 停止 | `dws event status` / `dws event stop --dry-run`,确认后再 `--yes` | -区分两个 schema:`dws event schema ` 查事件输出字段;`dws schema "event consume"` 查 consume 命令入参(统一内嵌 ToolSpec,含 parameters + 位置参数)。`source` 是 reviewed command identity 的 provenance;`event list/schema` 是 `interface_mode=local`,`event consume/status/stop` 因同时编排远端订阅控制面与本地 bus 而是 `interface_mode=composite`,不要把 identity 与实现机制混为一谈。 +默认 `--events message`。可选事件为 `message`、`reaction`、`read`、`recall`: -## Event catalog +- `at-me`、`all-direct`、`all-group` 只支持 `message`,且不接受目标。 +- `sender` 必须且只能传 `--user`、`--open-dingtalk-id` 或 `--user-query` 之一。 +- `group` 必须且只能传 `--chat-id` 或 `--chat-query` 之一。 +- `--query` 只用于纯 `message` 监听;混入 reaction/read/recall 时不得使用。 -| 事件码 | 场景 | 必填参数 | -|---|---|---| -| `user_im_message_receive_at` | 当前用户被 @ 的消息 | 无 | -| `user_im_message_receive_o2o` | 当前用户与指定用户的单聊消息 | `--user` 或 `--open-dingtalk-id` | -| `user_im_message_receive_group` | 当前用户所在指定群聊/会话的消息 | `--group` | -| `user_im_message_receive_user` | 当前用户收到的指定用户发送的消息(单聊和群聊) | `--user` 或 `--open-dingtalk-id` | -| `user_im_message_receive_o2o_all` | 当前用户收到的所有单聊消息 | 无 | -| `user_im_message_receive_group_all` | 当前用户收到的所有群聊消息 | 无 | -| `user_im_message_read_o2o` | 指定单聊中当前用户发送的消息被已读 | `--user` 或 `--open-dingtalk-id` | -| `user_im_message_read_group` | 指定群聊中当前用户发送的消息被已读 | `--group` | -| `user_im_message_recall_o2o` | 指定单聊中的消息被撤回 | `--user` 或 `--open-dingtalk-id` | -| `user_im_message_recall_group` | 指定群聊中的消息被撤回 | `--group` | -| `user_im_message_reaction_o2o` | 指定单聊中的消息收到表情回应 | `--user` 或 `--open-dingtalk-id` | -| `user_im_message_reaction_group` | 指定群聊中的消息收到表情回应 | `--group` | -| `user_im_group_updated` | 指定群聊的标题发生变更 | `--group` | -| `user_im_group_member_added` | 指定群聊有成员加入 | `--group` | -| `user_im_group_member_exited` | 指定群聊有成员退出 | `--group` | -| `user_im_group_disbanded` | 指定群聊被解散 | `--group` | +自然姓名和群名由 CLI 内部唯一解析:零命中或多候选返回结构化失败,在创建任何订阅前停止。`--dry-run` 走同一解析链。解析、监听、状态和停止必须使用同一个 `--profile`,不得跨组织搬运 ID。 -只承认上表 16 个事件码。其它身份模式、应用凭证模式、非个人 IM 事件不在本 skill 范围内。 +### 兼容 EventKey 索引 -## Command rules +`+listen-im` 覆盖高频路径;只有需要精确底层控制时才直接使用以下 16 个 EventKey: -- 默认身份就是当前用户,不要额外加身份切换 flag。 -- 使用当前用户 OAuth 登录态;未登录或 token 失效时,引导用户执行 `dws auth login`。 -- 不主动运行 `dws event list` 作为能力菜单;按用户意图直接选择上表事件。 -- 缺少必填 ID 时先解析或追问,不要猜测 ID。 -- 用户只给单聊对端人名时,先运行 `dws aisearch person --keyword "" --dimension name --format json` 解析 userId;多候选必须让用户确认。 -- 企业内部 userId 使用 `--user`;用户明确提供 openDingtalkId,或目标是外部联系人、机器人、跨组织身份时,使用 `--open-dingtalk-id`。 -- `--user` 与 `--open-dingtalk-id` 严格二选一。不要把 openDingtalkId 填入 `--user`,不要自动猜测或转换身份类型;缺少外部目标的 openDingtalkId 时先追问。 -- “监听我和某人的单聊”使用 `user_im_message_receive_o2o`;“监听某人发给我的消息/监听某人发送的消息”使用 `user_im_message_receive_user`,后者覆盖该发送人的单聊和群聊消息。 -- 只有用户明确要求“所有单聊消息”或“所有群消息”时才使用 `user_im_message_receive_o2o_all` / `user_im_message_receive_group_all`;指定人或指定群仍使用范围更小的事件。 -- 用户只给群名时,先运行 `dws chat search --query "" --format json` 解析 openConversationId;多候选必须让用户确认。 -- “监听群改名/群标题变更”使用 `user_im_group_updated`;“监听有人进群”使用 `user_im_group_member_added`;“监听有人退群”使用 `user_im_group_member_exited`;“监听群解散”使用 `user_im_group_disbanded`。群解散自测只能使用明确的测试群,并在执行解散操作前再次提示其不可逆影响。 -- 用户要求执行“撤回消息”时使用 `dws chat`;只有“监听/订阅消息撤回”才使用 `dws event consume user_im_message_recall_*`。 -- 用户说“贴标签”且语义是给消息贴表情时,按消息表情回应事件处理,event key 使用 `reaction`。 -- 正常 Agent 消费统一显式使用 `--flatten -f ndjson`。抓一条样本可用 `--flatten --max-events 1 -f json`。`--format` 只控制 JSON 序列化,`--flatten` 才控制数据结构。 -- 同一目标、同一过滤条件的兼容事件优先放在一个 `consume` 命令中:用户类事件共享一个 `--user` 或 `--open-dingtalk-id`,群类事件共享一个 `--group`,无目标事件可加入任一类组合。 -- 用户类与群类事件不能放进同一命令;不同用户、不同群或不同过滤条件必须启动多个 consume 进程。多事件命令不使用 `--subscribe-id`、`--rule`、`--event-types`、`--filter`、`--foreground`、`--force` 或 `--debug-raw-events`。 -- 多事件共享 `--query` / `--filter-json` 时,所选事件必须全部是消息接收事件;已读、撤回、表情回应或群生命周期事件混入后不能使用消息过滤参数。 -- 监听非默认组织时带 `--profile `;漏传会退回默认 profile 而失败。 -- 自己发的消息不作为事件回来(`isSelfLoop` 过滤):边监听边 `dws chat message send` 回复不成环;测试投递用别人 / 机器人发(自发会看到 0 事件)。 -- `--debug-raw-events` 只用于联调确认服务端推送是否到达本地连接;正常任务不要使用。它和 `--flatten` 互斥,`-f raw` 也不能与 `--flatten` 同时使用。 -- 排查:consume 报 bus 启动失败 → 报错已带真实原因,先查 `dws --profile auth status`(非默认组织带对 `--profile`);本地日志见 `~/.dws/events//personal_stream//bus.log`(`hash` 见 `dws event status` 的 Workdir);有残留先用 `dws event stop --all --dry-run` 预览,确认后加 `--yes` 清理。看着"挂住"无输出多是误加了 `--foreground`(那是跑 bus、不打印事件),去掉即可。 - -## 订阅创建失败与重试预算 - -以下约束适用于上表全部 16 个事件以及多事件命令中的每一项,只治理 `[event] ready` 之前的订阅创建;ready 之后的 Stream 断线由长连接重连机制处理。 - -- `0/2/1` 是 **Agent/host 编排约束**,不是 CLI 持久化硬总次数上限。每次 `dws event consume` 调用对每个逻辑订阅最多发送一次订阅创建 HTTP 请求,进程内不会自动重试。CLI 本地状态只持久化 `in_flight`、`cooldown`、`terminal_hold` 三种保护状态,不持久化或计算跨调用的 Agent/host 尝试次数。 -- 解析人名或群名、执行 `event consume` 以及后续 `event status/stop` 必须使用同一个 `--profile`。不得把其它 profile 下解析出的 userId、openDingtalkId 或 openConversationId 直接带入当前 profile 的订阅。 -- 同一逻辑订阅由当前 profile / 身份、event key、rule type、目标和过滤条件共同确定。`subscribe_id`、`trace_id` 以及重新启动进程都只是诊断或执行信息,不会生成新的逻辑操作;Agent/host 必须自行延续原编排预算。 -- Agent/host 收到 `retryable=false`:`max_additional_attempts=0`,立即停止,不得自动重跑。 -- Agent/host 收到 `retryable=true`:`max_additional_attempts=2`,初次失败后最多再尝试 2 次;错误若给出 `retry_after_seconds` 或 `next_retry_at`,不得提前重试。 -- 未返回 `retryable`(即 `retryable=unknown`):Agent/host 使用 `max_additional_attempts=1`,最多补偿尝试 1 次;仍无法确认时停止并上报错误与 trace。 -- `in_flight` 表示同一逻辑订阅已有请求执行中;`cooldown` 或 `terminal_hold` 表示当前被退避或终态保护。遇到这些状态不得递归调用 `event consume`、并行启动相同订阅或通过新 subId / trace 绕过;等待原请求或保护时间结束,同时由 Agent/host 继续维护自己的编排次数。 -- 多事件命令必须作为同一次原始操作治理。不得把失败事件拆成新的单事件命令、调整顺序或反复重启来重置预算;启动中任一项失败时,由 CLI 回滚本次已经创建的订阅。 - -### 本地保护状态运维 - -- open 版默认状态文件为 `~/.dws/events/open/personal_stream//personal_subscription_attempts.json`。设置 `DWS_CONFIG_DIR` 后配置根目录随之变化;其它 edition 使用对应 edition 目录,不固定为 `open`。 -- identity 目录权限为 `0700`;`personal_subscription_attempts.json` 与 `personal_subscription_attempts.lock` 权限均为 `0600`。 -- 连续 24h 没有失败后失败计数重置;`terminal_hold` 持续 1h。正常处理优先等待错误中的 `next_retry_at`,不要把删状态文件当成常规重试手段。 -- 紧急恢复时,先确认该 identity 没有正在创建订阅的进程;只删除 `personal_subscription_attempts.json`,不要删除 lock 文件。删除 JSON 会清空该 identity 的全部保护记录,不只影响一个事件。 - -## Call flow - -1. 从用户意图选择事件码;人名或群名先解析成必填 ID。 -2. 需要了解字段时运行 `dws event schema --flatten`,读取 `schema.properties`;此模式的 `jq_root_path` 为 `.`。 -3. 启动 `dws event consume [event_key...] ... --flatten -f ndjson`。单事件等待 `[event] ready event_key= bus_pid= subscribe_id=`;多事件先记录每条 `[event] subscription event_key= subscribe_id=`,再等待 `[event] ready event_count= bus_pid=`。不要用 `sleep` 猜测。 -4. stdout 每行是一个扁平事件 JSON;消息、动作及群成员加入/退出事件直接读取顶层业务字段。群标题变更和群解散只读取公共字段与 `payload` 中实际存在的字段。 -5. 需要确认监听状态时运行 `dws event status --event `,查看 `Subscriptions` 和 `Consumers`。 -6. 任务完成后优雅结束 consume;本次新建的订阅会自动取消。复用已有订阅或需要从外部主动取消时,先用 `dws event stop --dry-run` 预览,向用户确认后再加 `--yes`;临时测试可用 `--max-events` 或 `--duration` 自动退出。 - -## Subprocess contract - -- `event consume` 阻塞式长连接。stdout 只出事件;stderr 只出状态 / debug / 错误。 -- 就绪:单事件使用 `[event] ready event_key= bus_pid= subscribe_id=`;多事件在全部逻辑 consumer 就绪后使用 `[event] ready event_count= bus_pid=`,其前每个事件各有一条 `[event] subscription ...`。父进程等对应 ready 行再处理 stdout;不要 `--quiet`。 -- 退出:末行 `[event] exited — received N event(s) in Xs (reason: limit|timeout|signal|bus_shutdown)`;受控退出码 0,失败非 0 无 exited 行。 -- stdin 关闭 = 停机:仅当 stdin 是管道且未设 `--max-events/--duration` 时生效;交互终端和 `< /dev/null` 不触发。用管道 stdin 又要常驻就喂 `< <(tail -f /dev/null)`。 -- 正常事件处理持续读取 stdout 管道,不要改写为 `--output-dir` watcher。 -- 无界监听需外部进程管理;有界自测用 `--max-events N` 或 `--duration 10m`。 -- 订阅清理:本次新建的订阅任意干净退出即自动退订;`--subscribe-id` 复用的保留,`--ephemeral` 强制退订。优雅停用 SIGTERM、关 stdin,或外部先用 `dws event stop --dry-run` 预览、确认后加 `--yes`。不要 `kill -9`(跳过退订、泄漏服务端订阅)。 -- 批量清理先用 `dws event stop --all --dry-run` 预览,确认后加 `--yes`。 -- 一个 consume 可监听多个兼容事件,并为每个事件建立独立订阅和逻辑 consumer;它们共享本机 bus、远程连接、输出和生命周期,仍按 `event_type + subscribe_id` 隔离。`dws event stop ` 只移除对应事件,最后一个被移除后进程退出。 - -## Examples - -```bash -# 当前用户被 @ 的消息 -dws event consume user_im_message_receive_at --flatten -f ndjson - -# 当前用户与指定用户的单聊消息 -dws event consume user_im_message_receive_o2o \ - --user test-user-001 \ - --flatten \ - -f ndjson - -# 使用 openDingtalkId 监听外部联系人、机器人或跨组织身份的单聊消息 -dws event consume user_im_message_receive_o2o \ - --open-dingtalk-id open-user-1 \ - --flatten \ - -f ndjson - -# 指定群聊/会话消息 -dws event consume user_im_message_receive_group \ - --group cidxxxxxxxx \ - --flatten \ - -f ndjson - -# 指定发送人的消息(单聊和群聊) -dws event consume user_im_message_receive_user \ - --user test-user-001 \ - --flatten \ - -f ndjson - -# 当前用户收到的所有单聊消息(仅在用户明确要求“所有”时使用) -dws event consume user_im_message_receive_o2o_all --flatten -f ndjson - -# 当前用户收到的所有群聊消息(仅在用户明确要求“所有”时使用) -dws event consume user_im_message_receive_group_all --flatten -f ndjson - -# 使用 openDingtalkId 监听指定发送人的消息 -dws event consume user_im_message_receive_user \ - --open-dingtalk-id open-user-1 \ - --flatten \ - -f ndjson - -# 指定单聊消息已读 -dws event consume user_im_message_read_o2o \ - --user test-user-001 \ - --flatten \ - -f ndjson - -# 指定群聊消息撤回 -dws event consume user_im_message_recall_group \ - --group cidxxxxxxxx \ - --flatten \ - -f ndjson - -# 指定单聊消息收到表情回应 -dws event consume user_im_message_reaction_o2o \ - --user test-user-001 \ - --flatten \ - -f ndjson - -# 指定群标题变更 -dws event consume user_im_group_updated \ - --group cidxxxxxxxx \ - --flatten \ - -f ndjson - -# 指定群有成员加入 -dws event consume user_im_group_member_added \ - --group cidxxxxxxxx \ - --flatten \ - -f ndjson - -# 指定群有成员退出 -dws event consume user_im_group_member_exited \ - --group cidxxxxxxxx \ - --flatten \ - -f ndjson - -# 指定群解散 -dws event consume user_im_group_disbanded \ - --group cidxxxxxxxx \ - --flatten \ - -f ndjson - -# 同一用户的单聊消息、已读和撤回(一个进程) -dws event consume \ - user_im_message_receive_o2o \ - user_im_message_read_o2o \ - user_im_message_recall_o2o \ - --user test-user-001 \ - --flatten \ - -f ndjson - -# 同一群的消息和生命周期事件(一个进程) -dws event consume \ - user_im_message_receive_group \ - user_im_group_updated \ - user_im_group_disbanded \ - --group cidxxxxxxxx \ - --flatten \ - -f ndjson - -# 有界自测 -dws event consume user_im_message_receive_at \ - --duration 10m \ - --flatten \ - -f ndjson - -# 抓一条样本 -dws event consume user_im_message_receive_o2o \ - --user test-user-001 \ - --max-events 1 \ - --flatten \ - -f json +```text +user_im_message_receive_at +user_im_message_receive_o2o user_im_message_receive_user +user_im_message_receive_group user_im_message_receive_o2o_all +user_im_message_receive_group_all user_im_message_read_o2o +user_im_message_read_group user_im_message_recall_o2o +user_im_message_recall_group user_im_message_reaction_o2o +user_im_message_reaction_group user_im_group_updated +user_im_group_member_added user_im_group_member_exited +user_im_group_disbanded ``` -所有 `*_o2o` 命令和 `user_im_message_receive_user` 都可将 `--user ` 替换为 `--open-dingtalk-id `,但两个参数不能同时使用。 +用户类事件传 `--user` 或 `--open-dingtalk-id`,群类事件传 `--group`。群生命周期输出可含 `operator_open_dingtalk_id` 和 `members`;成员项使用 `open_dingtalk_id`。精确组合、兼容性和 Filter 规则见 reference。 -## 输出处理 +## 公开层与内部统一边界 -- `dws event schema --flatten` 是 Agent 写解析逻辑的依据。 -- `--flatten` 模式的顶层 `jq_root_path` 为 `.`;不传时为兼容存量脚本的 transport envelope,业务 payload 在 `.data | fromjson`。 -- `schema.properties` 是业务字段列表,例如 `content`、`sender`、`conversation_id`、`message_id`、`event_time`。 -- Agent 命令已显式传 `--flatten`,消息接收、已读、撤回和表情回应事件直接读取顶层业务字段;不要对该模式再生成 `fromjson` 或内部 transport 路径。 -- 引用回复读取可选的 `quoted_message`;合并转发读取可选的 `forward_messages` 数组。两者保留内部消息的 `message_id/conversation_id/sender/sender_open_dingtalk_id/content/create_time`;不要通过“聊天记录”等本地化外层文案识别或拆分合并转发。 -- 群成员加入/退出事件读取顶层 `conversation_id`、`operator`、`operator_open_dingtalk_id`、`members`、`event_time`。`operator` 是执行操作的人,`members` 是本次加入或退出的成员数组;成员项读取 `nick` 和 `open_dingtalk_id`。系统操作或成员自行退出时,操作人字段可能为空。 -- 群标题变更和群解散当前只承诺顶层 `type/event_id/timestamp/subscribe_id/payload`。读取 `payload` 时以实际键为准,不猜测群标题、操作者等尚未确认的字段;完整原始协议用 `-f raw` 或 `--debug-raw-events` 排查。 -- 群自动回复使用事件顶层 `conversation_id`;单聊自动回复使用顶层 `sender_open_dingtalk_id`。 -- 已读事件读取顶层 `reader`、`reader_open_dingtalk_id`、`read_time`;撤回事件读取 `recaller`、`recaller_open_dingtalk_id`、`recall_time`。 -- 表情回应事件读取顶层 `operator`、`operator_open_dingtalk_id`、`reaction_name`、`reaction_text`、`operation_type`、`operation_time`。 -- 图片、文件等媒体消息的 `content` 可能是可读描述;合并转发媒体的下载定位信息位于对应 `forward_messages[].content`。需要实际媒体文件时调用 `dws chat message download-media`。 +`+listen-im` 是意图编译层,不是第二套事件系统。它只负责: -## Topic index +```text +kind + events + target +→ typed resolver +→ 确定 EventKey 集合 +→ 一次 event consume 生命周期 +``` -| Topic | Reference | Coverage | +订阅创建/复用、单 bus、多 consumer、ready marker、扁平 NDJSON、超时/取消、部分失败回滚和退出清理全部复用现有 Runtime。低频 EventKey、群生命周期、Filter DSL、原始 envelope、复用 subscribe_id 等仍由 `event consume` 承担。 + +## 运行与结果契约 + +- 正常消费固定使用当前用户 OAuth 身份、`--flatten` 和 NDJSON;stdout 只输出事件,stderr 输出订阅、ready、退出和错误状态。 +- 单事件 ready:`[event] ready event_key= bus_pid= subscribe_id=`。 +- 多事件先逐条输出 subscription,全部就绪后输出 `[event] ready event_count= bus_pid=`。必须等待 ready,不用 `sleep` 猜测。 +- 有界任务使用 `--max-events N` 或 `--duration 10m`;无界任务需要宿主管理进程并持续读取 stdout。 +- 干净退出会取消本次新建的订阅;使用 SIGTERM、关闭符合条件的管道 stdin,或 Runtime 的 bounded exit。不要 `kill -9`。 +- 当前用户自己发送的消息会被 self-loop 过滤;自测事件应由另一用户或机器人发送。 +- 事件只负责监听;需要回复时把真实 `conversation_id` 或 `sender_open_dingtalk_id` 交给 `dws chat +messages-send`,不要从显示名猜 ID。 +- 扁平消息/动作字段按事件类型读取:已读为 `reader_open_dingtalk_id`,撤回为 `recaller_open_dingtalk_id`,回应为 `reaction_name`、`operation_type`。媒体优先通过聊天读取命令加 `--download-resources`;已知消息 ID 的底层降级入口是 `dws chat message download-media`。 + +## 安全与失败处理 + +- `event stop` 会取消订阅并影响本地 consumer:先 `--dry-run`,用户确认后再加 `--yes`。 +- 多事件属于一次原始操作;任一订阅启动失败时 Runtime 回滚本次已创建项,不拆成新命令绕过重试预算。 +- 这套 `0/2/1` 是 **Agent/host** 编排预算,适用于 16 个 EventKey:`retryable=false` 对应 `max_additional_attempts=0`;`retryable=true` 对应 `max_additional_attempts=2`;`retryable=unknown` 对应 `max_additional_attempts=1`。它不是 CLI 持久化硬总次数上限;每次调用最多创建一次,进程内不会自动重试,CLI 也不持久化或计算跨调用的 Agent/host 尝试次数。 +- 重试必须遵守 `retry_after_seconds` / `next_retry_at`。遇到 `in_flight`、`cooldown`、`terminal_hold` 不并发或递归重启同一逻辑订阅,也不换 `subscribe_id` / `trace_id` 绕过保护。 +- 认证、profile、订阅保护状态和 bus 排障按失败类型读取 [references/event-im.md](references/event-im.md),不要在正常路径预加载完整运维手册。 + +### 本地订阅保护契约 + +- open 版状态路径为 `~/.dws/events/open/personal_stream//personal_subscription_attempts.json`;设置 `DWS_CONFIG_DIR` 后根目录随之变化。 +- identity 目录权限为 `0700`;`personal_subscription_attempts.json` 与 `personal_subscription_attempts.lock` 权限为 `0600`。 +- 连续 `24h` 无失败后重置计数;`terminal_hold` 持续 `1h`。优先等待 `next_retry_at`,不要把删状态当常规重试。 +- 仅在确认该 identity 没有订阅创建进程的紧急恢复场景,只删除 `personal_subscription_attempts.json`,不要删除 lock 文件。该操作会清空该 identity 的全部保护记录,而非单个事件。 + +## 何时查询 Schema + +- 已知 Golden Route 时直接执行,不先跑 `event list`。 +- 只有解析业务字段时才用 `dws event schema --flatten`。 +- 只有参数或安全不确定时才用 `dws schema --cli-path "event +listen-im"` 或对应 leaf。 +- `event schema` 描述事件 payload;顶层 `dws schema` 描述 CLI 命令,两者不要混用。 + +## Reference + +| Topic | Reference | 何时读取 | |---|---|---| -| IM | [references/event-im.md](references/event-im.md) | 十六类个人 IM 事件命令、参数、生命周期、输出解析、自测和排障 | +| 低频 EventKey、群生命周期、原始输出、订阅运维与排障 | [references/event-im.md](references/event-im.md) | Golden Route 不覆盖、需要字段细节,或 Runtime 返回明确失败时 | diff --git a/skills/multi/dws-shared/SKILL.md b/skills/multi/dws-shared/SKILL.md index 7358231a..f6dcf32e 100644 --- a/skills/multi/dws-shared/SKILL.md +++ b/skills/multi/dws-shared/SKILL.md @@ -11,33 +11,23 @@ metadata: # DWS 共享执行契约 -把本文件作为所有 DWS 操作的轻量公共前置条件。明确单产品请求直接使用对应 -`dingtalk-*` skill;只有泛称 DWS、跨产品流程、URL 预检或意图不清时,才把本 -skill 作为入口进行二级路由。 +本文件只在泛称 DWS、跨产品流程、URL 预检或意图不清时作为入口。明确单产品请求直接使用对应 `dingtalk-*` skill;产品根 Skill 已从同一来源内嵌最小执行契约,不需要先完整读取本文件。 -## 所有 DWS 操作必须遵守 + +## 最小 DWS 执行契约 -- 只通过 `dws` CLI 操作钉钉产品;不要改用 curl、私有 HTTP API 或浏览器绕过。 -- 所有结构化机器读取命令使用 `--format json`,按真实返回字段判断结果。 -- 返回中的秒/毫秒时间戳必须转换为带时区的可读日期时间后再面向用户展示;默认使用 - 当前会话时区(本环境为 `Asia/Shanghai`),必要时同时保留原始值,禁止只把裸时间戳 - 交给 Agent 或用户判断具体日期。 -- 不要猜测命令、flag、字段名、UUID、nodeId、userId、邮箱、手机号或 URL。 -- 用户已明确产品内容意图时,意图优先于 URL 形态。`.md` 等本地格式文件按普通 - 文件走 `drive`:先 `dws drive download` 下载到本地处理,再用 `dws drive upload` - 回传。 -- 命令或参数不确定时,先运行最接近层级的 `dws --help`;禁止把自然语言 - 同义词直接拼成命令。 -- 后续步骤使用的 ID 必须来自前一步真实返回;多人同名、多目标或类型不明时先消歧。 -- 多账号场景只传组织时,使用该组织唯一 `isOrgCurrent=true` 的默认账号;多账号组织 - 没有默认账号时先让用户指定账号,禁止选择第一项、最近登录或最近使用账号。账号 - 选择与跨组织规则详见 `../dingtalk-profile/SKILL.md`。 -- 不输出、转述或记录 token、refresh token、appSecret 等凭据。宿主已注入认证时 - 不要求用户重新提供凭据。 -- 写入操作必须符合用户明确意图。删除、审批决策及其他不可逆/高影响操作,先展示 - 对象、动作和影响,获得明确同意后才追加 `--yes`;不得静默确认。 -- 创建、更新、发送等写操作后,按产品 skill 的要求回读或查询状态,不得仅凭退出码 - 宣称成功。 +- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。 +- 已知命令直接执行。只有 leaf 参数或安全语义不确定时读取精确 Schema,只有 Cobra flag 不确定时读取精确 leaf Help;不要加载产品级 Catalog 代替选路。 +- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。 +- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。 +- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。 +- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;需要确认时先说明对象、动作与影响,再追加 `--yes`。 +- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。 +- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。 +- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dws-shared` 中对应 reference;不要连续猜测替代命令。 + + +产品或跨产品规则在最小契约之上增量加载。用户已明确产品内容意图时,意图优先于 URL 形态;多账号选择与跨组织规则读取 `../dingtalk-profile/SKILL.md`。本地文件、产品边界和跨产品传递规则只在对应任务中加载,避免把全局手册放入每个单产品请求。 ## 渐进加载 diff --git a/skills/multi/dws-shared/references/runtime-contract.md b/skills/multi/dws-shared/references/runtime-contract.md new file mode 100644 index 00000000..28759c4f --- /dev/null +++ b/skills/multi/dws-shared/references/runtime-contract.md @@ -0,0 +1,11 @@ +## 最小 DWS 执行契约 + +- 只通过 `dws` CLI 操作钉钉;结构化读取使用 `--format json`,按真实返回判断结果。 +- 已知命令直接执行。只有 leaf 参数或安全语义不确定时读取精确 Schema,只有 Cobra flag 不确定时读取精确 leaf Help;不要加载产品级 Catalog 代替选路。 +- 不猜命令、flag、字段、ID、账号或时间。后续 ID 必须来自真实返回;零命中、多候选或类型不明时停止并消歧。 +- 解析目标、读取上下文和最终执行必须使用同一 profile;不得跨组织复用 userId、openDingTalkId 或 openConversationId。多账号组织只使用明确的 `isOrgCurrent=true` 默认账号;没有默认账号时要求用户指定,禁止选择第一项、最近登录或最近使用账号。 +- 不输出或记录 token、refresh token、appSecret、webhook token 等凭据;宿主已注入认证时不要索要凭据。 +- 写操作必须符合用户明确意图。是否需要确认以最终 Runtime gate 和 Schema 为准;需要确认时先说明对象、动作与影响,再追加 `--yes`。 +- 写后按任务结果契约验证;不能仅凭退出码宣称成功。部分结果、未知投递状态和失败项必须如实保留。 +- 时间戳面向用户展示时转换为带时区的可读时间;默认使用当前会话时区,必要时同时保留原值。 +- 遇到认证、权限、profile、confirmation 或未知错误时,只加载 `dws-shared` 中对应 reference;不要连续猜测替代命令。 diff --git a/test/mock_mcp/mock_mcp_smoke_test.go b/test/mock_mcp/mock_mcp_smoke_test.go index a97cb649..34648eca 100644 --- a/test/mock_mcp/mock_mcp_smoke_test.go +++ b/test/mock_mcp/mock_mcp_smoke_test.go @@ -3,8 +3,17 @@ package mock_mcp_test import ( "bytes" "context" + "crypto/rand" + "crypto/rsa" + "crypto/tls" + "crypto/x509" + "crypto/x509/pkix" "encoding/json" + "encoding/pem" "fmt" + "io" + "math/big" + "net" "net/http" "net/http/httptest" "os" @@ -18,9 +27,13 @@ import ( "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/app" + authpkg "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/auth" ) -const mockMCPSmokeHelperEnv = "DWS_MOCK_MCP_SMOKE_HELPER" +const ( + mockMCPSmokeHelperEnv = "DWS_MOCK_MCP_SMOKE_HELPER" + mockMCPSmokeCAEnv = "DWS_MOCK_MCP_SMOKE_CA_FILE" +) type recordedToolCall struct { path string @@ -39,6 +52,20 @@ func TestCLIHelperProcess(t *testing.T) { if os.Getenv(mockMCPSmokeHelperEnv) != "1" { return } + if caFile := strings.TrimSpace(os.Getenv(mockMCPSmokeCAEnv)); caFile != "" { + certificatePEM, err := os.ReadFile(caFile) + if err != nil { + fmt.Fprintf(os.Stderr, "Mock MCP smoke helper: read CA: %v\n", err) + os.Exit(2) + } + pool := x509.NewCertPool() + if !pool.AppendCertsFromPEM(certificatePEM) { + fmt.Fprintln(os.Stderr, "Mock MCP smoke helper: invalid CA file") + os.Exit(2) + } + _ = os.Setenv("GODEBUG", "x509usefallbackroots=1") + x509.SetFallbackRoots(pool) + } marker := -1 for i, arg := range os.Args { @@ -163,6 +190,519 @@ func TestMockMCPSmoke_CLIRoutesSerializedArgumentsAndPrintsJSON(t *testing.T) { } } +func TestMultiIME2E_NaturalTargetsCompletenessAndWriteBoundaries(t *testing.T) { + var requestsMu sync.Mutex + var requests []recordedToolCall + var uploadedBody []byte + download := newTrustedDownloadFixture(t, []byte("downloaded-resource-body")) + authConfigDir := t.TempDir() + authKeychainDir := filepath.Join(t.TempDir(), "keychain") + t.Setenv("DWS_CONFIG_DIR", authConfigDir) + t.Setenv("DWS_KEYCHAIN_DIR", authKeychainDir) + t.Setenv("DWS_DISABLE_KEYCHAIN", "1") + if err := authpkg.SaveTokenData(authConfigDir, &authpkg.TokenData{ + AccessToken: "ci-smoke-token", + RefreshToken: "ci-refresh-token", + ExpiresAt: time.Now().Add(time.Hour), + RefreshExpAt: time.Now().Add(2 * time.Hour), + ClientID: "ci-client", + CorpID: "ci-corp", + UserID: "ci-user", + }); err != nil { + t.Fatalf("save isolated event auth: %v", err) + } + var server *httptest.Server + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodPut && r.URL.Path == "/upload/conversation" { + body, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + requestsMu.Lock() + uploadedBody = append([]byte(nil), body...) + requestsMu.Unlock() + w.WriteHeader(http.StatusOK) + return + } + var envelope struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params struct { + Name string `json:"name"` + Arguments map[string]any `json:"arguments"` + } `json:"params"` + } + decodeErr := json.NewDecoder(r.Body).Decode(&envelope) + call := recordedToolCall{ + path: r.URL.Path, + method: r.Method, + authorization: r.Header.Get("Authorization"), + jsonrpc: envelope.JSONRPC, + tool: envelope.Params.Name, + arguments: envelope.Params.Arguments, + err: decodeErr, + } + requestsMu.Lock() + requests = append(requests, call) + requestsMu.Unlock() + + response := multiIMMockResponse( + envelope.Params.Name, + envelope.Params.Arguments, + server.URL, + download.URL, + ) + w.Header().Set("Content-Type", "application/json") + _ = json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": envelope.ID, + "result": map[string]any{ + "content": []map[string]any{{"type": "text", "text": response}}, + }, + }) + })) + defer server.Close() + + env := isolatedCLIEnv(t, map[string]string{ + "DINGTALK_CONTACT_MCP_URL": server.URL + "/mcp/contact", + "DINGTALK_CHAT_MCP_URL": server.URL + "/mcp/chat", + "DINGTALK_IM_MCP_URL": server.URL + "/mcp/im", + "HTTPS_PROXY": download.ProxyURL, + "https_proxy": download.ProxyURL, + "SSL_CERT_FILE": download.CAFile, + mockMCPSmokeCAEnv: download.CAFile, + "DWS_CONFIG_DIR": authConfigDir, + "DWS_KEYCHAIN_DIR": authKeychainDir, + }) + reset := func() { + requestsMu.Lock() + requests = nil + uploadedBody = nil + requestsMu.Unlock() + } + snapshot := func() []recordedToolCall { + requestsMu.Lock() + defer requestsMu.Unlock() + return append([]recordedToolCall(nil), requests...) + } + + t.Run("natural user advanced send resolves then writes once", func(t *testing.T) { + reset() + stdout, stderr, err := runCLI(t, env, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+messages-send", "--as", "user", + "--user-query", "张三", "--text", "你好", "--yes", + ) + if err != nil { + t.Fatalf("natural send failed: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + if len(calls) != 2 || calls[0].tool != "search_contact_by_key_word" || calls[1].tool != "send_personal_message" { + t.Fatalf("calls = %#v, want resolve + one send", calls) + } + if calls[1].arguments["receiverOpenDingTalkId"] != "D-user-1" { + t.Fatalf("send target = %#v", calls[1].arguments) + } + assertMultiIMCallsAreLocalAndAuthorized(t, calls) + }) + + t.Run("natural group file send uploads commits and writes once", func(t *testing.T) { + reset() + workdir := t.TempDir() + fileBody := []byte("multi-im-file-upload") + if err := os.WriteFile(filepath.Join(workdir, "report.txt"), fileBody, 0o600); err != nil { + t.Fatal(err) + } + stdout, stderr, err := runCLIInDir(t, env, workdir, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+messages-send", "--as", "user", + "--chat-query", "项目群", "--file", "./report.txt", + "--idempotency-key", "file-key", "--yes", + ) + if err != nil { + t.Fatalf("file send failed: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + wantTools := []string{ + "search_groups", + "init_conversation_file_upload", + "commit_conversation_file_upload", + "send_personal_message", + } + if got := recordedToolNames(calls); !reflect.DeepEqual(got, wantTools) { + t.Fatalf("file send tools = %#v, want %#v", got, wantTools) + } + requestsMu.Lock() + gotUpload := append([]byte(nil), uploadedBody...) + requestsMu.Unlock() + if !bytes.Equal(gotUpload, fileBody) { + t.Fatalf("uploaded body = %q, want %q", gotUpload, fileBody) + } + var payload map[string]any + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { + t.Fatalf("file send output is not JSON: %v\n%s", err, stdout) + } + if payload["ok"] != true || payload["effectiveMessageType"] != "file" { + t.Fatalf("file send result = %#v", payload) + } + }) + + t.Run("ambiguous user has zero write side effects", func(t *testing.T) { + reset() + _, _, err := runCLI(t, env, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+messages-send", "--as", "user", + "--user-query", "同名用户", "--text", "不能发", "--yes", + ) + if err == nil { + t.Fatal("ambiguous user unexpectedly succeeded") + } + calls := snapshot() + if len(calls) != 1 || calls[0].tool != "search_contact_by_key_word" { + t.Fatalf("ambiguous calls = %#v, want resolver only", calls) + } + }) + + t.Run("natural group resource read downloads atomically", func(t *testing.T) { + reset() + workdir := t.TempDir() + stdout, stderr, err := runCLIInDir(t, env, workdir, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+chat-messages", "--chat-query", "资源群", + "--download-resources", "--output-dir", "./downloads", + ) + if err != nil { + t.Fatalf("resource read failed: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + wantTools := []string{"search_groups", "list_conversation_message_v2", "get_resource_download_url"} + if got := recordedToolNames(calls); !reflect.DeepEqual(got, wantTools) { + t.Fatalf("resource tools = %#v, want %#v", got, wantTools) + } + downloaded, err := os.ReadFile(filepath.Join(workdir, "downloads", "artifact.bin")) + if err != nil { + t.Fatalf("read downloaded resource: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + if string(downloaded) != "downloaded-resource-body" { + t.Fatalf("downloaded resource = %q", downloaded) + } + var payload map[string]any + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { + t.Fatalf("resource output is not JSON: %v\n%s", err, stdout) + } + ledger, _ := payload["resourceDownloads"].(map[string]any) + if payload["complete"] != true || ledger["downloadedCount"] != float64(1) || ledger["failedCount"] != float64(0) { + t.Fatalf("resource completion = %#v", payload) + } + }) + + t.Run("search later page failure preserves results and marks incomplete", func(t *testing.T) { + reset() + stdout, stderr, err := runCLI(t, env, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+search-msg", "--query", "分页失败", + "--page-all", "--no-enrich", + ) + if err != nil { + t.Fatalf("partial search failed as a command: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + if got := recordedToolNames(calls); !reflect.DeepEqual(got, []string{"search_messages", "search_messages"}) { + t.Fatalf("search tools = %#v", got) + } + var payload map[string]any + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { + t.Fatalf("search output is not JSON: %v\n%s", err, stdout) + } + if payload["complete"] != false || payload["count"] != float64(1) || + payload["pagesFetched"] != float64(1) || payload["failedCount"] != float64(1) { + t.Fatalf("partial search contract = %#v", payload) + } + failures, _ := payload["failures"].([]any) + if len(failures) != 1 || failures[0].(map[string]any)["stage"] != "search-page" { + t.Fatalf("partial search failures = %#v", failures) + } + }) + + t.Run("event listen im process dry-run resolves and creates no subscription", func(t *testing.T) { + reset() + stdout, stderr, err := runCLI(t, env, + "--token", "ci-smoke-token", "--client-id", "ci-client", + "event", "+listen-im", "--kind", "group", + "--events", "message,reaction", "--chat-query", "项目群", + "--dry-run", + ) + if err != nil { + t.Fatalf("event dry-run failed: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + if got := recordedToolNames(calls); !reflect.DeepEqual(got, []string{"search_groups"}) { + t.Fatalf("event dry-run tools = %#v", got) + } + if !strings.Contains(stderr, "subscription[0]") || !strings.Contains(stderr, "subscription[1]") || + !strings.Contains(stderr, "user_im_message_receive_group") || + !strings.Contains(stderr, "user_im_message_reaction_group") { + t.Fatalf("event dry-run plan missing:\nstdout=%s\nstderr=%s", stdout, stderr) + } + }) + + t.Run("natural group read returns complete shared message contract", func(t *testing.T) { + reset() + stdout, stderr, err := runCLI(t, env, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+chat-messages", "--chat-query", "项目群", + ) + if err != nil { + t.Fatalf("natural read failed: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + if len(calls) != 2 || calls[0].tool != "search_groups" || calls[1].tool != "list_conversation_message_v2" { + t.Fatalf("read calls = %#v", calls) + } + var payload map[string]any + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { + t.Fatalf("read output is not JSON: %v\n%s", err, stdout) + } + if payload["contractVersion"] != "im.message-list.v1" || payload["complete"] != true || payload["failedCount"] != float64(0) { + t.Fatalf("message contract = %#v", payload) + } + messages, _ := payload["messages"].([]any) + if len(messages) != 1 { + t.Fatalf("messages = %#v", payload["messages"]) + } + message, _ := messages[0].(map[string]any) + if message["messageId"] != "msg-1" || message["senderId"] != "D-sender" || message["senderType"] != "user" { + t.Fatalf("projected message = %#v", message) + } + }) + + t.Run("natural member create dry-run resolves but never creates", func(t *testing.T) { + reset() + stdout, stderr, err := runCLI(t, env, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+chat-create", "--name", "新群", + "--member-query", "张三", "--dry-run", "--yes", + ) + if err != nil { + t.Fatalf("chat-create dry-run failed: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + if len(calls) != 2 || calls[0].tool != "search_contact_by_key_word" || calls[1].tool != "get_current_user_profile" { + t.Fatalf("dry-run calls = %#v, want resolver/profile and no create", calls) + } + for _, call := range calls { + if call.tool == "create_group_conversation" { + t.Fatalf("dry-run created a group: %#v", calls) + } + } + }) + + t.Run("reply returns continuation context", func(t *testing.T) { + reset() + stdout, stderr, err := runCLI(t, env, + "--token", "ci-smoke-token", "--format", "json", + "chat", "+messages-reply", + "--conversation-id", "cid-1", "--message-id", "msg-1", + "--ref-sender", "D-sender", "--text", "收到", + "--idempotency-key", "reply-key", "--yes", + ) + if err != nil { + t.Fatalf("reply failed: %v\nstdout=%s\nstderr=%s", err, stdout, stderr) + } + calls := snapshot() + if len(calls) != 1 || calls[0].tool != "send_personal_message" { + t.Fatalf("reply calls = %#v", calls) + } + var payload map[string]any + if err := json.Unmarshal([]byte(stdout), &payload); err != nil { + t.Fatalf("reply output is not JSON: %v\n%s", err, stdout) + } + if payload["contractVersion"] != "im.message-reply.v1" || payload["messageId"] != "sent-msg-1" || + payload["conversationId"] != "cid-1" || payload["idempotencyKey"] != "reply-key" { + t.Fatalf("reply continuation = %#v", payload) + } + }) +} + +func multiIMMockResponse(tool string, arguments map[string]any, mcpBaseURL, resourceURL string) string { + switch tool { + case "search_contact_by_key_word": + if arguments["keyword"] == "同名用户" { + return `{"result":[{"name":"同名用户","userId":"u1","openDingTalkId":"D1"},{"name":"同名用户","userId":"u2","openDingTalkId":"D2"}]}` + } + return `{"result":[{"name":"张三","userId":"user-1","openDingTalkId":"D-user-1"}]}` + case "search_groups": + if arguments["keyword"] == "资源群" { + return `{"result":[{"title":"资源群","openConversationId":"cid-resources"}]}` + } + return `{"result":[{"title":"项目群","openConversationId":"cid-1"}]}` + case "list_conversation_message_v2": + if arguments["openconversation_id"] == "cid-resources" { + return `{"result":{"hasMore":false,"messages":[{"openMessageId":"msg-resource","openConversationId":"cid-resources","sender":{"name":"张三","openDingTalkId":"D-sender","senderType":"user"},"msgType":"file","content":"{\"mediaId\":\"@media-1\"}","createTime":"2026-08-03 10:00:00"}]}}` + } + return `{"result":{"hasMore":false,"messages":[{"openMessageId":"msg-1","openConversationId":"cid-1","sender":{"name":"张三","openDingTalkId":"D-sender","senderType":"user"},"msgType":"text","content":"进度正常","createTime":"2026-08-03 10:00:00"}]}}` + case "get_resource_download_url": + body, _ := json.Marshal(map[string]any{"result": map[string]any{ + "resourceUrl": resourceURL, + "fileName": "artifact.bin", + }}) + return string(body) + case "init_conversation_file_upload": + body, _ := json.Marshal(map[string]any{ + "resourceUrl": mcpBaseURL + "/upload/conversation", + "uploadKey": "upload-key-1", + }) + return string(body) + case "commit_conversation_file_upload": + return `{"result":{"dentryId":101,"spaceId":202}}` + case "search_messages": + if arguments["keyword"] == "分页失败" && fmt.Sprint(arguments["cursor"]) == "c2" { + return `{"success":false,"code":"MOCK_PAGE_FAILURE","message":"simulated second page failure"}` + } + return `{"result":{"messages":[{"openMessageId":"search-msg-1","openConversationId":"cid-1","senderOpenDingTalkId":"D-sender","content":"first page"}],"hasMore":true,"nextCursor":"c2"}}` + case "get_current_user_profile": + return `{"result":[{"orgEmployeeModel":{"userId":"self-user"}}]}` + case "create_group_conversation": + return `{"result":{"openCid":"created-cid"}}` + case "send_personal_message": + return `{"result":{"openMessageId":"sent-msg-1","sendStatus":"accepted"}}` + default: + return `{"success":true}` + } +} + +func recordedToolNames(calls []recordedToolCall) []string { + names := make([]string, 0, len(calls)) + for _, call := range calls { + names = append(names, call.tool) + } + return names +} + +type trustedDownloadFixture struct { + URL string + ProxyURL string + CAFile string +} + +func newTrustedDownloadFixture(t *testing.T, body []byte) trustedDownloadFixture { + t.Helper() + caKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + now := time.Now() + caTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(1), + Subject: pkix.Name{CommonName: "DWS Multi IM Test CA"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign, + BasicConstraintsValid: true, + IsCA: true, + } + caDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + caCertificate, err := x509.ParseCertificate(caDER) + if err != nil { + t.Fatal(err) + } + leafKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatal(err) + } + leafTemplate := &x509.Certificate{ + SerialNumber: big.NewInt(2), + Subject: pkix.Name{CommonName: "download.dingtalk.com"}, + DNSNames: []string{"download.dingtalk.com"}, + NotBefore: now.Add(-time.Hour), + NotAfter: now.Add(time.Hour), + KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment, + ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, + } + leafDER, err := x509.CreateCertificate(rand.Reader, leafTemplate, caCertificate, &leafKey.PublicKey, caKey) + if err != nil { + t.Fatal(err) + } + leafPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: leafDER}) + leafPEM = append(leafPEM, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER})...) + privateKeyPEM := pem.EncodeToMemory(&pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(leafKey)}) + certificate, err := tls.X509KeyPair(leafPEM, privateKeyPEM) + if err != nil { + t.Fatal(err) + } + + downloadServer := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + w.Header().Set("Content-Type", "application/octet-stream") + _, _ = w.Write(body) + })) + downloadServer.TLS = &tls.Config{Certificates: []tls.Certificate{certificate}, MinVersion: tls.VersionTLS12} + downloadServer.StartTLS() + t.Cleanup(downloadServer.Close) + + proxyServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodConnect { + http.Error(w, "CONNECT required", http.StatusMethodNotAllowed) + return + } + upstream, err := net.DialTimeout("tcp", downloadServer.Listener.Addr().String(), 2*time.Second) + if err != nil { + http.Error(w, err.Error(), http.StatusBadGateway) + return + } + hijacker, ok := w.(http.Hijacker) + if !ok { + _ = upstream.Close() + http.Error(w, "hijacking unsupported", http.StatusInternalServerError) + return + } + client, buffered, err := hijacker.Hijack() + if err != nil { + _ = upstream.Close() + return + } + _, _ = fmt.Fprint(buffered, "HTTP/1.1 200 Connection Established\r\n\r\n") + _ = buffered.Flush() + go func() { + _, _ = io.Copy(upstream, client) + _ = upstream.Close() + }() + _, _ = io.Copy(client, upstream) + _ = client.Close() + })) + t.Cleanup(proxyServer.Close) + + caFile := filepath.Join(t.TempDir(), "download-ca.pem") + caPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: caDER}) + if err := os.WriteFile(caFile, caPEM, 0o600); err != nil { + t.Fatal(err) + } + return trustedDownloadFixture{ + URL: "https://download.dingtalk.com/artifact.bin", + ProxyURL: proxyServer.URL, + CAFile: caFile, + } +} + +func assertMultiIMCallsAreLocalAndAuthorized(t *testing.T, calls []recordedToolCall) { + t.Helper() + for _, call := range calls { + if call.err != nil { + t.Fatal(call.err) + } + if call.method != http.MethodPost || call.jsonrpc != "2.0" || call.authorization != "Bearer ci-smoke-token" { + t.Fatalf("invalid local JSON-RPC call = %#v", call) + } + if !strings.HasPrefix(call.path, "/mcp/") { + t.Fatalf("unexpected MCP path = %q", call.path) + } + } +} + func isolatedCLIEnv(t *testing.T, extra map[string]string) []string { t.Helper() @@ -201,6 +741,10 @@ func isolatedCLIEnv(t *testing.T, extra map[string]string) []string { } func runCLI(t *testing.T, env []string, args ...string) (string, string, error) { + return runCLIInDir(t, env, "", args...) +} + +func runCLIInDir(t *testing.T, env []string, dir string, args ...string) (string, string, error) { t.Helper() ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second) @@ -208,6 +752,7 @@ func runCLI(t *testing.T, env []string, args ...string) (string, string, error) processArgs := append([]string{"-test.run=^TestCLIHelperProcess$", "--"}, args...) cmd := exec.CommandContext(ctx, os.Args[0], processArgs...) cmd.Env = env + cmd.Dir = dir var stdout, stderr bytes.Buffer cmd.Stdout = &stdout cmd.Stderr = &stderr