From 5e3827d177cd73753333e4f5191683f172631768 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Wed, 3 Jun 2026 11:47:27 +0800 Subject: [PATCH 01/10] docs(chat-skill): add OpenClaw connector bot-provisioning recipe MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dws chat bot can only search/add-to-group bots, not create one. When a user asks "give me a bot / 接入 OpenClaw", the agent had no guided path. Add a "创建新机器人" recipe to the chat product reference (mono + multi) and wire trigger words + an intent row into the dingtalk-chat skill so dispatch routes the request. The recipe orchestrates the official @dingtalk-real-ai/dingtalk-connector (preflight gate -> npx install -> QR scan -> cross-verify via `dws chat bot search`), and is explicit that the flow depends on non-dws prerequisites (OpenClaw runtime + npm + QR) so the agent never fakes a successful creation. Docs-only: no Go / command-surface change. --- skills/mono/references/products/chat.md | 27 +++++++++++++++++++ skills/multi/dingtalk-chat/SKILL.md | 5 +++- skills/multi/dingtalk-chat/references/chat.md | 27 +++++++++++++++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index f925b042..ad228207 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -1099,6 +1099,33 @@ search 与 find 选择指南: | 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | +#### 创建【新】机器人 — 通过 OpenClaw 连接器(dws 无原生创建命令) + +范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。 +要"给我建一个新机器人/给 agent 接入钉钉/接入 OpenClaw",走官方连接器 `@dingtalk-real-ai/dingtalk-connector`(Hermes / OpenClaw 原生方式),它会免控制台一键建号并以 Stream 模式接入 agent(无需公网 IP / Webhook)。 +典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw""provision a bot"。 + +固定路线(前置闸门 → 创建 → 交叉验证,缺一步不算闭环): + +``` +# 1. 前置闸门:确认 OpenClaw 已安装且版本达标(< 2026.4.9 直接告知用户先升级,不要硬跑 npx) +openclaw -v # 需 OpenClaw CLI ≥ 2026.4.9 + +# 2. 创建:连接器在终端渲染钉钉二维码;手机钉钉扫码 → 点「一键创建新机器人」→ 自动建号 + 授权 + Stream 接入 +npx -y @dingtalk-real-ai/dingtalk-connector install + +# 3. 验证 channel 已 running +openclaw channels status --deep | grep DingTalk + +# 4. 交叉验证:连接器新建的机器人,dws 应当能搜到(两个独立工具看到同一个 robotCode 才算真创建成功) +dws chat bot search --format json +``` + +注意: + - 这条 recipe 依赖 dws 之外的前置(OpenClaw 运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 + - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 + ### category (会话分组管理) #### 获取用户自定义会话分组 diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index 24ebf51a..7ff5f47e 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -1,6 +1,6 @@ --- name: dingtalk-chat -description: 钉钉群聊与消息。Use when 用户提到 发消息/单聊/群聊/建群/拉人进群/改群名/搜索群/群成员管理/@消息/撤回消息/机器人群发/Webhook通知/发图片或文件到群。Distinct from dingtalk-ding(紧急DING消息/短信/电话)、dingtalk-mail(邮件)、dingtalk-edu-group(班级群)。命令前缀:dws chat。 +description: 钉钉群聊与消息。Use when 用户提到 发消息/单聊/群聊/建群/拉人进群/改群名/搜索群/群成员管理/@消息/撤回消息/机器人群发/Webhook通知/发图片或文件到群/搜机器人/查我的机器人/创建机器人/给agent接入钉钉机器人/接入OpenClaw。Distinct from dingtalk-ding(紧急DING消息/短信/电话)、dingtalk-mail(邮件)、dingtalk-edu-group(班级群)。命令前缀:dws chat。 cli_version: ">=0.2.14" metadata: category: product @@ -35,6 +35,8 @@ metadata: | "用机器人发消息" | `dws chat message send-by-bot --robot-code --group --title "<标题>" --text "<内容>"` | | "Webhook 推一条" | `dws chat message send-by-webhook --token --title "<标题>" --text "<内容>"` | | "撤回机器人消息" | `dws chat message recall-by-bot --robot-code --group --keys `(只能撤回机器人发的;撤回普通用户消息开源 dws v1.0.30 暂不支持)| +| "搜机器人" / "查我创建的机器人" | `dws chat bot find --query "<关键词>"`(全部可用,带 openDingTalkId)/ `dws chat bot search`(仅我创建的)| +| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw" | dws 无原生创建命令,走 OpenClaw 连接器:`npx -y @dingtalk-real-ai/dingtalk-connector install` → 扫码一键建号 → `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人」节 | > **注**:v1.0.30 起 `chat message send / send-by-bot / send-by-webhook` 全部强制 `--title` 必填(单聊群聊都要)。 @@ -44,3 +46,4 @@ metadata: - 要发图片/文件 → 先 `dt_media_upload` 上传 → `python scripts/extract_media_id.py ""` 提取 mediaId → 再用 `--media-id` - 紧急升级(应用内/短信/电话)→ 切到 `dingtalk-ding` - 发邮件 → 切到 `dingtalk-mail` +- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建,交给官方连接器 `@dingtalk-real-ai/dingtalk-connector`(见 [chat.md](references/chat.md) 「创建【新】机器人」节),建完用 `dws chat bot search` 交叉验证 diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index 5640f510..5d400bf6 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -1099,6 +1099,33 @@ search 与 find 选择指南: | 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | +#### 创建【新】机器人 — 通过 OpenClaw 连接器(dws 无原生创建命令) + +范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。 +要"给我建一个新机器人/给 agent 接入钉钉/接入 OpenClaw",走官方连接器 `@dingtalk-real-ai/dingtalk-connector`(Hermes / OpenClaw 原生方式),它会免控制台一键建号并以 Stream 模式接入 agent(无需公网 IP / Webhook)。 +典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw""provision a bot"。 + +固定路线(前置闸门 → 创建 → 交叉验证,缺一步不算闭环): + +``` +# 1. 前置闸门:确认 OpenClaw 已安装且版本达标(< 2026.4.9 直接告知用户先升级,不要硬跑 npx) +openclaw -v # 需 OpenClaw CLI ≥ 2026.4.9 + +# 2. 创建:连接器在终端渲染钉钉二维码;手机钉钉扫码 → 点「一键创建新机器人」→ 自动建号 + 授权 + Stream 接入 +npx -y @dingtalk-real-ai/dingtalk-connector install + +# 3. 验证 channel 已 running +openclaw channels status --deep | grep DingTalk + +# 4. 交叉验证:连接器新建的机器人,dws 应当能搜到(两个独立工具看到同一个 robotCode 才算真创建成功) +dws chat bot search --format json +``` + +注意: + - 这条 recipe 依赖 dws 之外的前置(OpenClaw 运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 + - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 + ### category (会话分组管理) #### 获取用户自定义会话分组 From 63fcde6be54223d057c0a72221efdcaa8e9d681d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Wed, 3 Jun 2026 14:05:42 +0800 Subject: [PATCH 02/10] docs(chat-skill): make bot-provisioning recipe host-agnostic MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first version gated the recipe on `openclaw -v`, which wrongly implied OpenClaw-only. The connector is host-agnostic by design (`peerDependencies.openclaw` is optional; the `bin install` device-auth flow mints the bot via DingTalk OpenAPI with zero OpenClaw dependency). Reframe the recipe into two layers: - 建号 (provisioning): host-agnostic — npx install works in any Node env. - 收→回 (runtime): OpenClaw and its forks (e.g. Hermes) work out of the box via the openclaw plugin-sdk contract; other mainstream agents (Claude / Cursor / Codex / ...) connect via the host's OpenAI Chat Completions endpoint. Preflight gate now accepts ANY supported host instead of hard-requiring openclaw. Cross-verify via `dws chat bot search` stays (host-agnostic). --- skills/mono/references/products/chat.md | 31 ++++++++++++------- skills/multi/dingtalk-chat/SKILL.md | 6 ++-- skills/multi/dingtalk-chat/references/chat.md | 31 ++++++++++++------- 3 files changed, 43 insertions(+), 25 deletions(-) diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index ad228207..01bbd18c 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -1099,30 +1099,39 @@ search 与 find 选择指南: | 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | -#### 创建【新】机器人 — 通过 OpenClaw 连接器(dws 无原生创建命令) +#### 创建【新】机器人 — 通过钉钉官方 Connector(dws 无原生创建命令,宿主无关) 范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。 -要"给我建一个新机器人/给 agent 接入钉钉/接入 OpenClaw",走官方连接器 `@dingtalk-real-ai/dingtalk-connector`(Hermes / OpenClaw 原生方式),它会免控制台一键建号并以 Stream 模式接入 agent(无需公网 IP / Webhook)。 -典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw""provision a bot"。 +要"给我建一个新机器人 / 给 agent 接入钉钉",走官方 Connector `@dingtalk-real-ai/dingtalk-connector`。它**分两层**——建号宿主无关,收发按宿主接入,不要把它当成 OpenClaw 专属: -固定路线(前置闸门 → 创建 → 交叉验证,缺一步不算闭环): +- **① 建号(host-agnostic)**: `npx -y @dingtalk-real-ai/dingtalk-connector install` 走钉钉 OpenAPI 设备授权(扫码一键建号),**与具体 agent 宿主无关**,任何能跑 Node 的环境都能建号。 +- **② 收→回(按宿主接入)**: 让机器人真正"收消息 → 回消息"需要一个宿主运行时。 + - **OpenClaw 及其 fork(如 Hermes)**: 开箱即用,Connector 以 Stream 模式接入(无公网 IP / Webhook)。 + - **其它主流 agent(Claude / Cursor / Codex / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 接入(Connector 把钉钉消息桥接到该端点)。 + +典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 + +固定路线(前置闸门 → 建号 → 按宿主接入 → 交叉验证,缺一步不算闭环): ``` -# 1. 前置闸门:确认 OpenClaw 已安装且版本达标(< 2026.4.9 直接告知用户先升级,不要硬跑 npx) -openclaw -v # 需 OpenClaw CLI ≥ 2026.4.9 +# 1. 前置闸门:确认存在【任一】受支持的宿主运行时,而不是硬卡 openclaw +# OpenClaw / Hermes 系(Stream 宿主,≥ 2026.4.9):openclaw -v +# 其它 agent:确认有一个 OpenAI Chat Completions endpoint 可桥接 +openclaw -v 2>/dev/null || echo "无 OpenClaw/Hermes 宿主 → 改用 OpenAI-compatible endpoint 接入(见上层②)" -# 2. 创建:连接器在终端渲染钉钉二维码;手机钉钉扫码 → 点「一键创建新机器人」→ 自动建号 + 授权 + Stream 接入 +# 2. 建号(宿主无关):终端渲染钉钉二维码 → 手机钉钉扫码 → 一键创建新机器人 → 自动建号 + 授权 npx -y @dingtalk-real-ai/dingtalk-connector install -# 3. 验证 channel 已 running -openclaw channels status --deep | grep DingTalk +# 3. 按宿主验证接入:OpenClaw / Hermes 系 → channel 应 running + connected +openclaw channels status --deep | grep -i dingtalk -# 4. 交叉验证:连接器新建的机器人,dws 应当能搜到(两个独立工具看到同一个 robotCode 才算真创建成功) +# 4. 交叉验证(宿主无关):新建机器人 dws 应当能搜到(两个独立工具看到同一 robotCode 才算真成功) dws chat bot search --format json ``` 注意: - - 这条 recipe 依赖 dws 之外的前置(OpenClaw 运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **分层理解**:建号是钉钉 OpenAPI 能力,宿主无关;收→回依赖宿主运行时——今天 OpenClaw 及其 fork(Hermes)开箱即用,其它主流 agent 经 OpenAI-compatible endpoint 接入。**不要因为包名含 openclaw 就以为只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 + - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index 7ff5f47e..c66ad19e 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -1,6 +1,6 @@ --- name: dingtalk-chat -description: 钉钉群聊与消息。Use when 用户提到 发消息/单聊/群聊/建群/拉人进群/改群名/搜索群/群成员管理/@消息/撤回消息/机器人群发/Webhook通知/发图片或文件到群/搜机器人/查我的机器人/创建机器人/给agent接入钉钉机器人/接入OpenClaw。Distinct from dingtalk-ding(紧急DING消息/短信/电话)、dingtalk-mail(邮件)、dingtalk-edu-group(班级群)。命令前缀:dws chat。 +description: 钉钉群聊与消息。Use when 用户提到 发消息/单聊/群聊/建群/拉人进群/改群名/搜索群/群成员管理/@消息/撤回消息/机器人群发/Webhook通知/发图片或文件到群/搜机器人/查我的机器人/创建机器人/给agent接入钉钉机器人/接入OpenClaw或Hermes/provision bot。Distinct from dingtalk-ding(紧急DING消息/短信/电话)、dingtalk-mail(邮件)、dingtalk-edu-group(班级群)。命令前缀:dws chat。 cli_version: ">=0.2.14" metadata: category: product @@ -36,7 +36,7 @@ metadata: | "Webhook 推一条" | `dws chat message send-by-webhook --token --title "<标题>" --text "<内容>"` | | "撤回机器人消息" | `dws chat message recall-by-bot --robot-code --group --keys `(只能撤回机器人发的;撤回普通用户消息开源 dws v1.0.30 暂不支持)| | "搜机器人" / "查我创建的机器人" | `dws chat bot find --query "<关键词>"`(全部可用,带 openDingTalkId)/ `dws chat bot search`(仅我创建的)| -| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw" | dws 无原生创建命令,走 OpenClaw 连接器:`npx -y @dingtalk-real-ai/dingtalk-connector install` → 扫码一键建号 → `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人」节 | +| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw/Hermes" | dws 无原生创建命令,走官方 Connector(**宿主无关**):`npx -y @dingtalk-real-ai/dingtalk-connector install` 扫码一键建号(host-agnostic)→ 收发按宿主接入(OpenClaw/Hermes 开箱即用,其它 agent 经 OpenAI-compatible endpoint)→ `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人」节 | > **注**:v1.0.30 起 `chat message send / send-by-bot / send-by-webhook` 全部强制 `--title` 必填(单聊群聊都要)。 @@ -46,4 +46,4 @@ metadata: - 要发图片/文件 → 先 `dt_media_upload` 上传 → `python scripts/extract_media_id.py ""` 提取 mediaId → 再用 `--media-id` - 紧急升级(应用内/短信/电话)→ 切到 `dingtalk-ding` - 发邮件 → 切到 `dingtalk-mail` -- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建,交给官方连接器 `@dingtalk-real-ai/dingtalk-connector`(见 [chat.md](references/chat.md) 「创建【新】机器人」节),建完用 `dws chat bot search` 交叉验证 +- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建,交给官方 Connector `@dingtalk-real-ai/dingtalk-connector`(**宿主无关**:建号通用,收→回 OpenClaw/Hermes 开箱、其它主流 agent 经 OpenAI-compatible endpoint 接入;见 [chat.md](references/chat.md) 「创建【新】机器人」节),建完用 `dws chat bot search` 交叉验证 diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index 5d400bf6..b690273b 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -1099,30 +1099,39 @@ search 与 find 选择指南: | 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | -#### 创建【新】机器人 — 通过 OpenClaw 连接器(dws 无原生创建命令) +#### 创建【新】机器人 — 通过钉钉官方 Connector(dws 无原生创建命令,宿主无关) 范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。 -要"给我建一个新机器人/给 agent 接入钉钉/接入 OpenClaw",走官方连接器 `@dingtalk-real-ai/dingtalk-connector`(Hermes / OpenClaw 原生方式),它会免控制台一键建号并以 Stream 模式接入 agent(无需公网 IP / Webhook)。 -典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw""provision a bot"。 +要"给我建一个新机器人 / 给 agent 接入钉钉",走官方 Connector `@dingtalk-real-ai/dingtalk-connector`。它**分两层**——建号宿主无关,收发按宿主接入,不要把它当成 OpenClaw 专属: -固定路线(前置闸门 → 创建 → 交叉验证,缺一步不算闭环): +- **① 建号(host-agnostic)**: `npx -y @dingtalk-real-ai/dingtalk-connector install` 走钉钉 OpenAPI 设备授权(扫码一键建号),**与具体 agent 宿主无关**,任何能跑 Node 的环境都能建号。 +- **② 收→回(按宿主接入)**: 让机器人真正"收消息 → 回消息"需要一个宿主运行时。 + - **OpenClaw 及其 fork(如 Hermes)**: 开箱即用,Connector 以 Stream 模式接入(无公网 IP / Webhook)。 + - **其它主流 agent(Claude / Cursor / Codex / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 接入(Connector 把钉钉消息桥接到该端点)。 + +典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 + +固定路线(前置闸门 → 建号 → 按宿主接入 → 交叉验证,缺一步不算闭环): ``` -# 1. 前置闸门:确认 OpenClaw 已安装且版本达标(< 2026.4.9 直接告知用户先升级,不要硬跑 npx) -openclaw -v # 需 OpenClaw CLI ≥ 2026.4.9 +# 1. 前置闸门:确认存在【任一】受支持的宿主运行时,而不是硬卡 openclaw +# OpenClaw / Hermes 系(Stream 宿主,≥ 2026.4.9):openclaw -v +# 其它 agent:确认有一个 OpenAI Chat Completions endpoint 可桥接 +openclaw -v 2>/dev/null || echo "无 OpenClaw/Hermes 宿主 → 改用 OpenAI-compatible endpoint 接入(见上层②)" -# 2. 创建:连接器在终端渲染钉钉二维码;手机钉钉扫码 → 点「一键创建新机器人」→ 自动建号 + 授权 + Stream 接入 +# 2. 建号(宿主无关):终端渲染钉钉二维码 → 手机钉钉扫码 → 一键创建新机器人 → 自动建号 + 授权 npx -y @dingtalk-real-ai/dingtalk-connector install -# 3. 验证 channel 已 running -openclaw channels status --deep | grep DingTalk +# 3. 按宿主验证接入:OpenClaw / Hermes 系 → channel 应 running + connected +openclaw channels status --deep | grep -i dingtalk -# 4. 交叉验证:连接器新建的机器人,dws 应当能搜到(两个独立工具看到同一个 robotCode 才算真创建成功) +# 4. 交叉验证(宿主无关):新建机器人 dws 应当能搜到(两个独立工具看到同一 robotCode 才算真成功) dws chat bot search --format json ``` 注意: - - 这条 recipe 依赖 dws 之外的前置(OpenClaw 运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **分层理解**:建号是钉钉 OpenAPI 能力,宿主无关;收→回依赖宿主运行时——今天 OpenClaw 及其 fork(Hermes)开箱即用,其它主流 agent 经 OpenAI-compatible endpoint 接入。**不要因为包名含 openclaw 就以为只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 + - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 From 55a2352466134c580a05589cc52dee4a3ece8b31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:21:11 +0800 Subject: [PATCH 03/10] docs(chat-skill): list all dws-supported agents, not just OpenClaw/Hermes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The recipe implied only OpenClaw/Hermes were "first-class". In fact the dws skill (incl. this recipe) installs into every mainstream agent home dws supports — Claude Code, Cursor, Codex, Qoder, opencode, Gemini, Windsurf, Cline, Kiro, Trae, Hermes, OpenClaw (~14 agent homes; see internal/app/skill_command.go + internal/upgrade/paths.go). Reframe the runtime as two equal bridges (no "only OpenClaw"): - openclaw/plugin-sdk channel contract (OpenClaw + forks like Hermes) - the host's OpenAI Chat Completions endpoint (every other agent) --- skills/mono/references/products/chat.md | 8 ++++---- skills/multi/dingtalk-chat/SKILL.md | 4 ++-- skills/multi/dingtalk-chat/references/chat.md | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index 01bbd18c..ec9b271a 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -1105,9 +1105,9 @@ search 与 find 选择指南: 要"给我建一个新机器人 / 给 agent 接入钉钉",走官方 Connector `@dingtalk-real-ai/dingtalk-connector`。它**分两层**——建号宿主无关,收发按宿主接入,不要把它当成 OpenClaw 专属: - **① 建号(host-agnostic)**: `npx -y @dingtalk-real-ai/dingtalk-connector install` 走钉钉 OpenAPI 设备授权(扫码一键建号),**与具体 agent 宿主无关**,任何能跑 Node 的环境都能建号。 -- **② 收→回(按宿主接入)**: 让机器人真正"收消息 → 回消息"需要一个宿主运行时。 - - **OpenClaw 及其 fork(如 Hermes)**: 开箱即用,Connector 以 Stream 模式接入(无公网 IP / Webhook)。 - - **其它主流 agent(Claude / Cursor / Codex / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 接入(Connector 把钉钉消息桥接到该端点)。 +- **② 收→回(运行时)**: 让机器人真正"收消息 → 回消息"需要一个 agent 运行时。**这条 recipe(建号指南)随 dws skill 安装到 dws 支持的全部主流 agent**——Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等(约 14 个 agent home,见 `dws skill setup` 目标),它们都能驱动上面的建号流程。运行时桥接有两条**对等**路径: + - **OpenClaw 及其 fork(如 Hermes)**: 走 Connector 的 `openclaw/plugin-sdk` channel 契约,Stream 模式直连(无公网 IP / Webhook)。 + - **其它 agent(Claude Code / Cursor / Codex / Qoder / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 桥接(Connector 把钉钉消息转给该端点,agent 回流);与 DEAP 架构同源。 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 @@ -1130,7 +1130,7 @@ dws chat bot search --format json ``` 注意: - - **分层理解**:建号是钉钉 OpenAPI 能力,宿主无关;收→回依赖宿主运行时——今天 OpenClaw 及其 fork(Hermes)开箱即用,其它主流 agent 经 OpenAI-compatible endpoint 接入。**不要因为包名含 openclaw 就以为只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 + - **分层理解**:① 建号是钉钉 OpenAPI 能力,宿主无关;② 这条 skill 指南覆盖 dws 支持的全部主流 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等,约 14 个 agent home);③ 收→回运行时有两条**对等**路径(`openclaw/plugin-sdk` 契约 / OpenAI-compatible endpoint)。**不是只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index c66ad19e..284ee19a 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -36,7 +36,7 @@ metadata: | "Webhook 推一条" | `dws chat message send-by-webhook --token --title "<标题>" --text "<内容>"` | | "撤回机器人消息" | `dws chat message recall-by-bot --robot-code --group --keys `(只能撤回机器人发的;撤回普通用户消息开源 dws v1.0.30 暂不支持)| | "搜机器人" / "查我创建的机器人" | `dws chat bot find --query "<关键词>"`(全部可用,带 openDingTalkId)/ `dws chat bot search`(仅我创建的)| -| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw/Hermes" | dws 无原生创建命令,走官方 Connector(**宿主无关**):`npx -y @dingtalk-real-ai/dingtalk-connector install` 扫码一键建号(host-agnostic)→ 收发按宿主接入(OpenClaw/Hermes 开箱即用,其它 agent 经 OpenAI-compatible endpoint)→ `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人」节 | +| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw/Hermes/Qoder/Claude Code" | dws 无原生创建命令,走官方 Connector(**宿主无关**):`npx -y @dingtalk-real-ai/dingtalk-connector install` 扫码一键建号 → 该指南覆盖 dws 支持的全部主流 agent(Claude Code/Cursor/Codex/Qoder/opencode/Gemini/Windsurf/Cline/Kiro/Trae/Hermes/OpenClaw 等);收发运行时两条对等路径(plugin-sdk 契约 / OpenAI-compatible endpoint)→ `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人」节 | > **注**:v1.0.30 起 `chat message send / send-by-bot / send-by-webhook` 全部强制 `--title` 必填(单聊群聊都要)。 @@ -46,4 +46,4 @@ metadata: - 要发图片/文件 → 先 `dt_media_upload` 上传 → `python scripts/extract_media_id.py ""` 提取 mediaId → 再用 `--media-id` - 紧急升级(应用内/短信/电话)→ 切到 `dingtalk-ding` - 发邮件 → 切到 `dingtalk-mail` -- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建,交给官方 Connector `@dingtalk-real-ai/dingtalk-connector`(**宿主无关**:建号通用,收→回 OpenClaw/Hermes 开箱、其它主流 agent 经 OpenAI-compatible endpoint 接入;见 [chat.md](references/chat.md) 「创建【新】机器人」节),建完用 `dws chat bot search` 交叉验证 +- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建,交给官方 Connector `@dingtalk-real-ai/dingtalk-connector`(**宿主无关**:建号通用,指南覆盖 dws 支持的全部主流 agent——Claude Code/Cursor/Codex/Qoder/opencode/Gemini/Windsurf/Cline/Kiro/Trae/Hermes/OpenClaw 等;收→回两条对等路径 plugin-sdk 契约 / OpenAI-compatible endpoint;见 [chat.md](references/chat.md) 「创建【新】机器人」节),建完用 `dws chat bot search` 交叉验证 diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index b690273b..c11969cb 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -1105,9 +1105,9 @@ search 与 find 选择指南: 要"给我建一个新机器人 / 给 agent 接入钉钉",走官方 Connector `@dingtalk-real-ai/dingtalk-connector`。它**分两层**——建号宿主无关,收发按宿主接入,不要把它当成 OpenClaw 专属: - **① 建号(host-agnostic)**: `npx -y @dingtalk-real-ai/dingtalk-connector install` 走钉钉 OpenAPI 设备授权(扫码一键建号),**与具体 agent 宿主无关**,任何能跑 Node 的环境都能建号。 -- **② 收→回(按宿主接入)**: 让机器人真正"收消息 → 回消息"需要一个宿主运行时。 - - **OpenClaw 及其 fork(如 Hermes)**: 开箱即用,Connector 以 Stream 模式接入(无公网 IP / Webhook)。 - - **其它主流 agent(Claude / Cursor / Codex / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 接入(Connector 把钉钉消息桥接到该端点)。 +- **② 收→回(运行时)**: 让机器人真正"收消息 → 回消息"需要一个 agent 运行时。**这条 recipe(建号指南)随 dws skill 安装到 dws 支持的全部主流 agent**——Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等(约 14 个 agent home,见 `dws skill setup` 目标),它们都能驱动上面的建号流程。运行时桥接有两条**对等**路径: + - **OpenClaw 及其 fork(如 Hermes)**: 走 Connector 的 `openclaw/plugin-sdk` channel 契约,Stream 模式直连(无公网 IP / Webhook)。 + - **其它 agent(Claude Code / Cursor / Codex / Qoder / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 桥接(Connector 把钉钉消息转给该端点,agent 回流);与 DEAP 架构同源。 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 @@ -1130,7 +1130,7 @@ dws chat bot search --format json ``` 注意: - - **分层理解**:建号是钉钉 OpenAPI 能力,宿主无关;收→回依赖宿主运行时——今天 OpenClaw 及其 fork(Hermes)开箱即用,其它主流 agent 经 OpenAI-compatible endpoint 接入。**不要因为包名含 openclaw 就以为只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 + - **分层理解**:① 建号是钉钉 OpenAPI 能力,宿主无关;② 这条 skill 指南覆盖 dws 支持的全部主流 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等,约 14 个 agent home);③ 收→回运行时有两条**对等**路径(`openclaw/plugin-sdk` 契约 / OpenAI-compatible endpoint)。**不是只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 From 1d66ffc50fb367162e56a500fe6e3f56dc1b5e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Wed, 3 Jun 2026 15:43:58 +0800 Subject: [PATCH 04/10] =?UTF-8?q?docs(chat-skill):=20add=20the=20robot-sco?= =?UTF-8?q?pe=20approval=20gate=20(=E5=BB=BA=E5=8F=B7=E2=89=A0=E5=8F=AF?= =?UTF-8?q?=E7=94=A8)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Hard-won lesson: device-auth SUCCESS and a successful accessToken only prove the APP credential is live — the robot still needs the qyapi_robot_sendmsg scope approved before it can actually send/receive. `openclaw channels status: connected` is just the stream WS, NOT a DingTalk capability receipt, and must not be treated as "connected = done". Add step 5 (approval-gate probe: get token -> robot/groupMessages/send; AccessDenied on qyapi_robot_sendmsg => pending, apply via the open-dev appscope link DingTalk returns) and a prominent caveat bullet. --- skills/mono/references/products/chat.md | 23 +++++++++++++++---- skills/multi/dingtalk-chat/references/chat.md | 23 +++++++++++++++---- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index ec9b271a..cee2f794 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -1111,7 +1111,7 @@ search 与 find 选择指南: 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 -固定路线(前置闸门 → 建号 → 按宿主接入 → 交叉验证,缺一步不算闭环): +固定路线(前置闸门 → 建号 → 按宿主接入 → **审批闸门** → 交叉验证,缺一步不算闭环): ``` # 1. 前置闸门:确认存在【任一】受支持的宿主运行时,而不是硬卡 openclaw @@ -1122,16 +1122,29 @@ openclaw -v 2>/dev/null || echo "无 OpenClaw/Hermes 宿主 → 改用 OpenAI-co # 2. 建号(宿主无关):终端渲染钉钉二维码 → 手机钉钉扫码 → 一键创建新机器人 → 自动建号 + 授权 npx -y @dingtalk-real-ai/dingtalk-connector install -# 3. 按宿主验证接入:OpenClaw / Hermes 系 → channel 应 running + connected +# 3. 接入状态:OpenClaw / Hermes 系 → channel 显示 running +# ⚠️ 注意:connected 只是 stream WS 连上,≠ 机器人能收发(真实判定见第 5 步) openclaw channels status --deep | grep -i dingtalk -# 4. 交叉验证(宿主无关):新建机器人 dws 应当能搜到(两个独立工具看到同一 robotCode 才算真成功) -dws chat bot search --format json +# 4. 身份交叉验证:用新应用凭据查它名下机器人,拿 robotName(appKey ≠ robotCode) +dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json + +# 5. 审批闸门【关键:建号 ≠ 可用】:robot 发消息能力需开通 scope qyapi_robot_sendmsg +# device-auth SUCCESS / 能换 token 只代表【应用凭据】有效,不代表【机器人能收发】,必须探测真实能力: +TOKEN=$(curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \ + -H 'Content-Type: application/json' -d '{"appKey":"<新clientId>","appSecret":"<新secret>"}' | jq -r .accessToken) +curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ + -H "x-acs-dingtalk-access-token: $TOKEN" -H 'Content-Type: application/json' \ + -d '{"robotCode":"<新clientId>","openConversationId":"__probe__","msgKey":"sampleText","msgParam":"{\"content\":\"probe\"}"}' +# → Forbidden.AccessDenied ...qyapi_robot_sendmsg = 审批未过:照报错里给的 +# open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg 开通后再用 +# → 参数类错误(非 AccessDenied) = scope 已通,机器人可收发 ``` 注意: - **分层理解**:① 建号是钉钉 OpenAPI 能力,宿主无关;② 这条 skill 指南覆盖 dws 支持的全部主流 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等,约 14 个 agent home);③ 收→回运行时有两条**对等**路径(`openclaw/plugin-sdk` 契约 / OpenAI-compatible endpoint)。**不是只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 - - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **建号 ≠ 可用(血泪闸门,最易踩)**:device-auth 返回 `SUCCESS` / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` 只是 stream WS 连上,**不是** DingTalk 的能力回执——**绝不能拿它当"建联成功"**。唯一可靠判定 = 第 5 步真实能力探测;开通入口就在钉钉返回的报错里:`open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg`。 + - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码 + **robot scope 审批**),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index c11969cb..9621cfe1 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -1111,7 +1111,7 @@ search 与 find 选择指南: 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 -固定路线(前置闸门 → 建号 → 按宿主接入 → 交叉验证,缺一步不算闭环): +固定路线(前置闸门 → 建号 → 按宿主接入 → **审批闸门** → 交叉验证,缺一步不算闭环): ``` # 1. 前置闸门:确认存在【任一】受支持的宿主运行时,而不是硬卡 openclaw @@ -1122,16 +1122,29 @@ openclaw -v 2>/dev/null || echo "无 OpenClaw/Hermes 宿主 → 改用 OpenAI-co # 2. 建号(宿主无关):终端渲染钉钉二维码 → 手机钉钉扫码 → 一键创建新机器人 → 自动建号 + 授权 npx -y @dingtalk-real-ai/dingtalk-connector install -# 3. 按宿主验证接入:OpenClaw / Hermes 系 → channel 应 running + connected +# 3. 接入状态:OpenClaw / Hermes 系 → channel 显示 running +# ⚠️ 注意:connected 只是 stream WS 连上,≠ 机器人能收发(真实判定见第 5 步) openclaw channels status --deep | grep -i dingtalk -# 4. 交叉验证(宿主无关):新建机器人 dws 应当能搜到(两个独立工具看到同一 robotCode 才算真成功) -dws chat bot search --format json +# 4. 身份交叉验证:用新应用凭据查它名下机器人,拿 robotName(appKey ≠ robotCode) +dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json + +# 5. 审批闸门【关键:建号 ≠ 可用】:robot 发消息能力需开通 scope qyapi_robot_sendmsg +# device-auth SUCCESS / 能换 token 只代表【应用凭据】有效,不代表【机器人能收发】,必须探测真实能力: +TOKEN=$(curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \ + -H 'Content-Type: application/json' -d '{"appKey":"<新clientId>","appSecret":"<新secret>"}' | jq -r .accessToken) +curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ + -H "x-acs-dingtalk-access-token: $TOKEN" -H 'Content-Type: application/json' \ + -d '{"robotCode":"<新clientId>","openConversationId":"__probe__","msgKey":"sampleText","msgParam":"{\"content\":\"probe\"}"}' +# → Forbidden.AccessDenied ...qyapi_robot_sendmsg = 审批未过:照报错里给的 +# open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg 开通后再用 +# → 参数类错误(非 AccessDenied) = scope 已通,机器人可收发 ``` 注意: - **分层理解**:① 建号是钉钉 OpenAPI 能力,宿主无关;② 这条 skill 指南覆盖 dws 支持的全部主流 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等,约 14 个 agent home);③ 收→回运行时有两条**对等**路径(`openclaw/plugin-sdk` 契约 / OpenAI-compatible endpoint)。**不是只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 - - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **建号 ≠ 可用(血泪闸门,最易踩)**:device-auth 返回 `SUCCESS` / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` 只是 stream WS 连上,**不是** DingTalk 的能力回执——**绝不能拿它当"建联成功"**。唯一可靠判定 = 第 5 步真实能力探测;开通入口就在钉钉返回的报错里:`open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg`。 + - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码 + **robot scope 审批**),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 From e818bfeeb7e94dfc041f7ff76c922bfbdfb99496 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 4 Jun 2026 10:47:47 +0800 Subject: [PATCH 05/10] =?UTF-8?q?docs(chat-skill):=20=E5=BB=BA=E5=8F=B7?= =?UTF-8?q?=E6=94=B9=E7=94=A8=E6=9C=8D=E5=8A=A1=E7=AB=AF=20API=20+=20Strea?= =?UTF-8?q?m=20=E5=BB=BA=E8=81=94=EF=BC=8C=E4=B8=8B=E7=BA=BF=E6=89=AB?= =?UTF-8?q?=E7=A0=81=20device-auth?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 机器人 provisioning 从「扫码 device-auth(npx @dingtalk-real-ai/dingtalk-connector install)」 改为服务端 API + DWClient Stream 建联,并删除所有扫码/device-auth 表述: - 建号:POST /v1.0/microApp/agent/create 一次拿 robotCode/clientId/clientSecret, 无需扫码、可无人值守。标注为目标态(依赖上游 MCP 工具上线,封成 mcp-gw 工具后 dws 自动生成命令),并给出临时 dws api 裸调验证方式(需自带创建者凭证)。 - 建联:拿 clientId/secret 起 DWClient 订阅 TOPIC_ROBOT,agent 收→回;运行时两条 对等路径(openclaw/plugin-sdk 契约 / OpenAI-compatible endpoint)保持不变。 - 审批闸门「建号≠可用」(qyapi_robot_sendmsg) + 真实发消息探测 + 交叉验证保留。 - best_practices 新增 provision-bot recipe 行。 注意:新 API/MCP 建号路径尚未上线,本 PR 合并后到工具上线前,skill 内建号步骤 不可直接闭环(过渡真空期,已在文档中明确标注,不写幽灵命令)。 涉及:chat.md(mono+multi)、dingtalk-chat/SKILL.md、01-messaging.md(mono+multi) --- .../references/best_practices/01-messaging.md | 1 + skills/mono/references/products/chat.md | 73 ++++++++++++------- skills/multi/dingtalk-chat/SKILL.md | 4 +- .../dingtalk-chat/references/01-messaging.md | 1 + skills/multi/dingtalk-chat/references/chat.md | 73 ++++++++++++------- 5 files changed, 100 insertions(+), 52 deletions(-) diff --git a/skills/mono/references/best_practices/01-messaging.md b/skills/mono/references/best_practices/01-messaging.md index fb95e817..0b50b3fb 100644 --- a/skills/mono/references/best_practices/01-messaging.md +++ b/skills/mono/references/best_practices/01-messaging.md @@ -4,6 +4,7 @@ | Recipe | 行动指南(固定路线) | |--------|-------------------| +| provision-bot | 建号→建联→审批三段(详见 [chat.md](../products/chat.md) 「创建【新】机器人 + 建联」节):
① **建号**:服务端 API `microApp/agent/create` 一次拿 robotCode/clientId/clientSecret(**目标态,依赖上游 MCP 工具上线**;临时可 `dws api POST /v1.0/microApp/agent/create --token <创建者token> --data '{...}'` 自带凭据验证)
② **Stream 建联**:拿 clientId/secret 起 DWClient 订阅 TOPIC_ROBOT,agent 收→回(运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 dingtalk-openclaw-connector)
③ **审批+验证**:`qyapi_robot_sendmsg` scope 审批(建号≠可用,connected 不是能力回执,真实发消息探测才算数)+ `chat bot search` 交叉验证 | | query-group-chat | **优先**:`chat_export_messages.py`(开源版未引入;可手动用 `dws chat message list` 翻页后写入文件)(自动搜群+翻页+导出)
备选: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 | **优先**:`chat_history_with_user.py`(开源版未引入;可手动用 `dws chat search` + `dws chat message list` 组合)(自动搜人+翻页+导出)
备选: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 "<内容>"`(必填项见 [ding.md](../products/ding.md))
2. `chat message send --group --text "<内容>"` 群里提醒(可选 `--title` / `@` 见 [chat.md](../products/chat.md))
3. `todo task create --title "<标题>" --executors --priority 40` 建紧急待办
前置:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`;`chat search --query "<群名>"` → 取 `openConversationId` | diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index cee2f794..5e83481b 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -1099,38 +1099,59 @@ search 与 find 选择指南: | 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | -#### 创建【新】机器人 — 通过钉钉官方 Connector(dws 无原生创建命令,宿主无关) +#### 创建【新】机器人 + 建联(建号 → Stream 建联 → 审批开通) -范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。 -要"给我建一个新机器人 / 给 agent 接入钉钉",走官方 Connector `@dingtalk-real-ai/dingtalk-connector`。它**分两层**——建号宿主无关,收发按宿主接入,不要把它当成 OpenClaw 专属: - -- **① 建号(host-agnostic)**: `npx -y @dingtalk-real-ai/dingtalk-connector install` 走钉钉 OpenAPI 设备授权(扫码一键建号),**与具体 agent 宿主无关**,任何能跑 Node 的环境都能建号。 -- **② 收→回(运行时)**: 让机器人真正"收消息 → 回消息"需要一个 agent 运行时。**这条 recipe(建号指南)随 dws skill 安装到 dws 支持的全部主流 agent**——Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等(约 14 个 agent home,见 `dws skill setup` 目标),它们都能驱动上面的建号流程。运行时桥接有两条**对等**路径: - - **OpenClaw 及其 fork(如 Hermes)**: 走 Connector 的 `openclaw/plugin-sdk` channel 契约,Stream 模式直连(无公网 IP / Webhook)。 - - **其它 agent(Claude Code / Cursor / Codex / Qoder / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 桥接(Connector 把钉钉消息转给该端点,agent 回流);与 DEAP 架构同源。 +范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。要"建一个新机器人 / 给 agent 接入钉钉",整条链路分三段:**① 建号(拿凭据)→ ② Stream 建联(用凭据起连接)→ ③ 审批开通**,缺一段都不算可用。 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 -固定路线(前置闸门 → 建号 → 按宿主接入 → **审批闸门** → 交叉验证,缺一步不算闭环): +##### ① 建号:服务端 API 一次拿到 robotCode + clientId + clientSecret + +钉钉服务端接口 `POST /v1.0/microApp/agent/create` 一次调用直接建号,返回 `robotCode` / `clientId` / `clientSecret`(**clientSecret 仅返回一次**),无需扫码、可无人值守。 + +> ⚠️ **状态:目标态,依赖上游 MCP 工具上线(规划中)。** +> 落地方式是把这个接口封成**服务端 MCP 工具**挂到 mcp-gw:客户端(dws)用普通会话凭证即可调用,**本地无需持有 AppKey/AppSecret**;建号所需的创建者凭证与建号 scope 由 MCP 服务端持有,并由服务端校验「调用方有权为该企业 / 该 userId 建号」(同 org、管理员或本人)。工具上线进入服务发现后,dws 会自动生成对应命令。 +> **它上线前,不要凭空写出 discovery 里不存在的命令**(幽灵命令);建号这步当前在 dws 内不可直接闭环。 +当前临时验证(需自带带建号 scope 的创建者凭证 access_token,凭据由调用者自备): + +``` +dws api POST /v1.0/microApp/agent/create \ + --token <创建者ACCESS_TOKEN> \ + --data '{ + "userId": "<归属人 staffId>", + "appName": "<2~20,企业内唯一>", + "robotName": "<2~20>", + "desc": "<≤200>", + "robotMediaId": "", + "previewMediaId": "" + }' +# 返回 robotCode / clientId / clientSecret(secret 仅此一次,立即安全保存) +# 接口契约仍是草稿:域名(api/oapi)、字段(userId/userid)、返回(agentId/appId)以真实返回为准 ``` -# 1. 前置闸门:确认存在【任一】受支持的宿主运行时,而不是硬卡 openclaw -# OpenClaw / Hermes 系(Stream 宿主,≥ 2026.4.9):openclaw -v -# 其它 agent:确认有一个 OpenAI Chat Completions endpoint 可桥接 -openclaw -v 2>/dev/null || echo "无 OpenClaw/Hermes 宿主 → 改用 OpenAI-compatible endpoint 接入(见上层②)" -# 2. 建号(宿主无关):终端渲染钉钉二维码 → 手机钉钉扫码 → 一键创建新机器人 → 自动建号 + 授权 -npx -y @dingtalk-real-ai/dingtalk-connector install +##### ② Stream 建联:让机器人收→回(无公网 IP / Webhook) + +建号本质只是拿到 `clientId` / `clientSecret`。**拿到这对凭据后,由一个 agent 运行时起 Stream 连接即可驱动收发,与凭据从哪来无关**(API 建号 / 手动从开放平台后台抄,建联这段都一样)。机制(参考实现:[dingtalk-openclaw-connector](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector)): -# 3. 接入状态:OpenClaw / Hermes 系 → channel 显示 running -# ⚠️ 注意:connected 只是 stream WS 连上,≠ 机器人能收发(真实判定见第 5 步) -openclaw channels status --deep | grep -i dingtalk +1. `new DWClient({ clientId, clientSecret })`(`dingtalk-stream` SDK) +2. `client.registerCallbackListener(TOPIC_ROBOT, handler)` 订阅机器人消息 +3. `client.connect()` 起 WebSocket 直连钉钉网关(**无需公网 IP / Webhook**) +4. 回调里解析 `res.data`,拿 `sessionWebhook` / `conversationId` / `text.content` / `senderStaffId`,转给本地 agent +5. agent 产出回复 → 回 `data.sessionWebhook` -# 4. 身份交叉验证:用新应用凭据查它名下机器人,拿 robotName(appKey ≠ robotCode) -dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json +运行时桥接两条**对等**路径,按宿主选(**不是只支持 OpenClaw**,`peerDependencies.openclaw` 标记为 optional): +- **OpenClaw 及其 fork(如 Hermes)**: 走 Connector 的 `openclaw/plugin-sdk` channel 契约,Stream 直连(上面 1–5 即其内部实现)。把 `clientId` / `clientSecret` 配进账号文件后,`openclaw channels status --deep | grep -i dingtalk` 看 running。 +- **其它 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae 等约 14 个 agent home)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 桥接,Connector 把钉钉消息转给该端点再回流;与 DEAP 架构同源。 -# 5. 审批闸门【关键:建号 ≠ 可用】:robot 发消息能力需开通 scope qyapi_robot_sendmsg -# device-auth SUCCESS / 能换 token 只代表【应用凭据】有效,不代表【机器人能收发】,必须探测真实能力: +> 账号文件字段、启动、多机器人 `chatbotUserId` 协作等具体配置,以 connector 仓 README / `docs/DINGTALK_MANUAL_SETUP.md` 为准,本 skill 不复制其实现细节。 + +##### ③ 审批开通 + 交叉验证(建号 ≠ 可用,最易踩) + +建号成功 / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` / Stream WS 连上**都不是** DingTalk 的能力回执,**不能当作"建联成功"**。唯一可靠判定是真实发消息探测: + +``` +# 用新应用凭据探测真实发消息能力(appKey/appSecret 即 clientId/clientSecret) TOKEN=$(curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \ -H 'Content-Type: application/json' -d '{"appKey":"<新clientId>","appSecret":"<新secret>"}' | jq -r .accessToken) curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ @@ -1141,10 +1162,12 @@ curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ # → 参数类错误(非 AccessDenied) = scope 已通,机器人可收发 ``` +身份交叉验证(appKey ≠ robotCode): `dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json` 查它名下机器人,拿 robotName / robotCode。 + 注意: - - **分层理解**:① 建号是钉钉 OpenAPI 能力,宿主无关;② 这条 skill 指南覆盖 dws 支持的全部主流 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等,约 14 个 agent home);③ 收→回运行时有两条**对等**路径(`openclaw/plugin-sdk` 契约 / OpenAI-compatible endpoint)。**不是只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 - - **建号 ≠ 可用(血泪闸门,最易踩)**:device-auth 返回 `SUCCESS` / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` 只是 stream WS 连上,**不是** DingTalk 的能力回执——**绝不能拿它当"建联成功"**。唯一可靠判定 = 第 5 步真实能力探测;开通入口就在钉钉返回的报错里:`open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg`。 - - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码 + **robot scope 审批**),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **整条链路依赖 dws 之外的前置**(建号=上游 MCP 工具上线,或自带创建者凭证;建联=agent 运行时;两段都要 robot scope 审批),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **建号 ≠ 可用(血泪闸门,最易踩)**:能换 `access_token` 只代表【应用凭据】生效,机器人收发还需 `qyapi_robot_sendmsg` 审批;`connected` 不是能力回执。唯一可靠判定 = 第 ③ 段真实能力探测。 + - **mediaId**:`robotMediaId` / `previewMediaId` 需先经 `media.upload` 上传获得;留空走服务端默认图标。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index 284ee19a..231943db 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -36,7 +36,7 @@ metadata: | "Webhook 推一条" | `dws chat message send-by-webhook --token --title "<标题>" --text "<内容>"` | | "撤回机器人消息" | `dws chat message recall-by-bot --robot-code --group --keys `(只能撤回机器人发的;撤回普通用户消息开源 dws v1.0.30 暂不支持)| | "搜机器人" / "查我创建的机器人" | `dws chat bot find --query "<关键词>"`(全部可用,带 openDingTalkId)/ `dws chat bot search`(仅我创建的)| -| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw/Hermes/Qoder/Claude Code" | dws 无原生创建命令,走官方 Connector(**宿主无关**):`npx -y @dingtalk-real-ai/dingtalk-connector install` 扫码一键建号 → 该指南覆盖 dws 支持的全部主流 agent(Claude Code/Cursor/Codex/Qoder/opencode/Gemini/Windsurf/Cline/Kiro/Trae/Hermes/OpenClaw 等);收发运行时两条对等路径(plugin-sdk 契约 / OpenAI-compatible endpoint)→ `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人」节 | +| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw/Hermes/Qoder/Claude Code" | 三段链路:① 建号(服务端 API `microApp/agent/create` 一次拿 robotCode/clientId/clientSecret,**目标态依赖上游 MCP 工具上线**)→ ② Stream 建联(拿 clientId/secret 起 DWClient,运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint)→ ③ `qyapi_robot_sendmsg` 审批 + `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人 + 建联」节 | > **注**:v1.0.30 起 `chat message send / send-by-bot / send-by-webhook` 全部强制 `--title` 必填(单聊群聊都要)。 @@ -46,4 +46,4 @@ metadata: - 要发图片/文件 → 先 `dt_media_upload` 上传 → `python scripts/extract_media_id.py ""` 提取 mediaId → 再用 `--media-id` - 紧急升级(应用内/短信/电话)→ 切到 `dingtalk-ding` - 发邮件 → 切到 `dingtalk-mail` -- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建,交给官方 Connector `@dingtalk-real-ai/dingtalk-connector`(**宿主无关**:建号通用,指南覆盖 dws 支持的全部主流 agent——Claude Code/Cursor/Codex/Qoder/opencode/Gemini/Windsurf/Cline/Kiro/Trae/Hermes/OpenClaw 等;收→回两条对等路径 plugin-sdk 契约 / OpenAI-compatible endpoint;见 [chat.md](references/chat.md) 「创建【新】机器人」节),建完用 `dws chat bot search` 交叉验证 +- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建。建号走服务端 API `microApp/agent/create`(**目标态依赖上游 MCP 工具上线**,封成 mcp-gw 工具后 dws 自动生成命令)拿 clientId/clientSecret,再由 agent 运行时起 Stream 建联(两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 `@dingtalk-real-ai/dingtalk-connector`);见 [chat.md](references/chat.md) 「创建【新】机器人 + 建联」节,建完过 `qyapi_robot_sendmsg` 审批 + `dws chat bot search` 交叉验证 diff --git a/skills/multi/dingtalk-chat/references/01-messaging.md b/skills/multi/dingtalk-chat/references/01-messaging.md index 7932c5a8..d68cae15 100644 --- a/skills/multi/dingtalk-chat/references/01-messaging.md +++ b/skills/multi/dingtalk-chat/references/01-messaging.md @@ -6,6 +6,7 @@ | Recipe | 行动指南(固定路线) | |--------|-------------------| +| provision-bot | 建号→建联→审批三段(详见 [chat.md](./chat.md) 「创建【新】机器人 + 建联」节):
① **建号**:服务端 API `microApp/agent/create` 一次拿 robotCode/clientId/clientSecret(**目标态,依赖上游 MCP 工具上线**;临时可 `dws api POST /v1.0/microApp/agent/create --token <创建者token> --data '{...}'` 自带凭据验证)
② **Stream 建联**:拿 clientId/secret 起 DWClient 订阅 TOPIC_ROBOT,agent 收→回(运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 dingtalk-openclaw-connector)
③ **审批+验证**:`qyapi_robot_sendmsg` scope 审批(建号≠可用,connected 不是能力回执,真实发消息探测才算数)+ `chat bot search` 交叉验证 | | 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-direct --user --time ""` → 取消息列表
3. **翻页**:同 query-group-chat
4. 合并全部消息后总结 | | escalate-ding | 三级升级:
1. `ding message send --robot-code --type app --users --content "<内容>"`(必填项见 `dingtalk-ding/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` | diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index 9621cfe1..15d7f9f0 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -1099,38 +1099,59 @@ search 与 find 选择指南: | 额外返回 openDingTalkId | 无 | 有(可用于给机器人发单聊消息) | | 触发词 | "我创建的""我的""我自己的" | "搜索机器人""找机器人""查机器人" | -#### 创建【新】机器人 — 通过钉钉官方 Connector(dws 无原生创建命令,宿主无关) +#### 创建【新】机器人 + 建联(建号 → Stream 建联 → 审批开通) -范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。 -要"给我建一个新机器人 / 给 agent 接入钉钉",走官方 Connector `@dingtalk-real-ai/dingtalk-connector`。它**分两层**——建号宿主无关,收发按宿主接入,不要把它当成 OpenClaw 专属: - -- **① 建号(host-agnostic)**: `npx -y @dingtalk-real-ai/dingtalk-connector install` 走钉钉 OpenAPI 设备授权(扫码一键建号),**与具体 agent 宿主无关**,任何能跑 Node 的环境都能建号。 -- **② 收→回(运行时)**: 让机器人真正"收消息 → 回消息"需要一个 agent 运行时。**这条 recipe(建号指南)随 dws skill 安装到 dws 支持的全部主流 agent**——Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等(约 14 个 agent home,见 `dws skill setup` 目标),它们都能驱动上面的建号流程。运行时桥接有两条**对等**路径: - - **OpenClaw 及其 fork(如 Hermes)**: 走 Connector 的 `openclaw/plugin-sdk` channel 契约,Stream 模式直连(无公网 IP / Webhook)。 - - **其它 agent(Claude Code / Cursor / Codex / Qoder / …)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 桥接(Connector 把钉钉消息转给该端点,agent 回流);与 DEAP 架构同源。 +范围: `dws chat bot` 只能**搜索**(search / find)和**拉机器人进群**(group members add-bot),**不能创建**机器人。要"建一个新机器人 / 给 agent 接入钉钉",整条链路分三段:**① 建号(拿凭据)→ ② Stream 建联(用凭据起连接)→ ③ 审批开通**,缺一段都不算可用。 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 -固定路线(前置闸门 → 建号 → 按宿主接入 → **审批闸门** → 交叉验证,缺一步不算闭环): +##### ① 建号:服务端 API 一次拿到 robotCode + clientId + clientSecret + +钉钉服务端接口 `POST /v1.0/microApp/agent/create` 一次调用直接建号,返回 `robotCode` / `clientId` / `clientSecret`(**clientSecret 仅返回一次**),无需扫码、可无人值守。 + +> ⚠️ **状态:目标态,依赖上游 MCP 工具上线(规划中)。** +> 落地方式是把这个接口封成**服务端 MCP 工具**挂到 mcp-gw:客户端(dws)用普通会话凭证即可调用,**本地无需持有 AppKey/AppSecret**;建号所需的创建者凭证与建号 scope 由 MCP 服务端持有,并由服务端校验「调用方有权为该企业 / 该 userId 建号」(同 org、管理员或本人)。工具上线进入服务发现后,dws 会自动生成对应命令。 +> **它上线前,不要凭空写出 discovery 里不存在的命令**(幽灵命令);建号这步当前在 dws 内不可直接闭环。 +当前临时验证(需自带带建号 scope 的创建者凭证 access_token,凭据由调用者自备): + +``` +dws api POST /v1.0/microApp/agent/create \ + --token <创建者ACCESS_TOKEN> \ + --data '{ + "userId": "<归属人 staffId>", + "appName": "<2~20,企业内唯一>", + "robotName": "<2~20>", + "desc": "<≤200>", + "robotMediaId": "", + "previewMediaId": "" + }' +# 返回 robotCode / clientId / clientSecret(secret 仅此一次,立即安全保存) +# 接口契约仍是草稿:域名(api/oapi)、字段(userId/userid)、返回(agentId/appId)以真实返回为准 ``` -# 1. 前置闸门:确认存在【任一】受支持的宿主运行时,而不是硬卡 openclaw -# OpenClaw / Hermes 系(Stream 宿主,≥ 2026.4.9):openclaw -v -# 其它 agent:确认有一个 OpenAI Chat Completions endpoint 可桥接 -openclaw -v 2>/dev/null || echo "无 OpenClaw/Hermes 宿主 → 改用 OpenAI-compatible endpoint 接入(见上层②)" -# 2. 建号(宿主无关):终端渲染钉钉二维码 → 手机钉钉扫码 → 一键创建新机器人 → 自动建号 + 授权 -npx -y @dingtalk-real-ai/dingtalk-connector install +##### ② Stream 建联:让机器人收→回(无公网 IP / Webhook) + +建号本质只是拿到 `clientId` / `clientSecret`。**拿到这对凭据后,由一个 agent 运行时起 Stream 连接即可驱动收发,与凭据从哪来无关**(API 建号 / 手动从开放平台后台抄,建联这段都一样)。机制(参考实现:[dingtalk-openclaw-connector](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector)): -# 3. 接入状态:OpenClaw / Hermes 系 → channel 显示 running -# ⚠️ 注意:connected 只是 stream WS 连上,≠ 机器人能收发(真实判定见第 5 步) -openclaw channels status --deep | grep -i dingtalk +1. `new DWClient({ clientId, clientSecret })`(`dingtalk-stream` SDK) +2. `client.registerCallbackListener(TOPIC_ROBOT, handler)` 订阅机器人消息 +3. `client.connect()` 起 WebSocket 直连钉钉网关(**无需公网 IP / Webhook**) +4. 回调里解析 `res.data`,拿 `sessionWebhook` / `conversationId` / `text.content` / `senderStaffId`,转给本地 agent +5. agent 产出回复 → 回 `data.sessionWebhook` -# 4. 身份交叉验证:用新应用凭据查它名下机器人,拿 robotName(appKey ≠ robotCode) -dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json +运行时桥接两条**对等**路径,按宿主选(**不是只支持 OpenClaw**,`peerDependencies.openclaw` 标记为 optional): +- **OpenClaw 及其 fork(如 Hermes)**: 走 Connector 的 `openclaw/plugin-sdk` channel 契约,Stream 直连(上面 1–5 即其内部实现)。把 `clientId` / `clientSecret` 配进账号文件后,`openclaw channels status --deep | grep -i dingtalk` 看 running。 +- **其它 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae 等约 14 个 agent home)**: 通过宿主暴露的 **OpenAI Chat Completions endpoint** 桥接,Connector 把钉钉消息转给该端点再回流;与 DEAP 架构同源。 -# 5. 审批闸门【关键:建号 ≠ 可用】:robot 发消息能力需开通 scope qyapi_robot_sendmsg -# device-auth SUCCESS / 能换 token 只代表【应用凭据】有效,不代表【机器人能收发】,必须探测真实能力: +> 账号文件字段、启动、多机器人 `chatbotUserId` 协作等具体配置,以 connector 仓 README / `docs/DINGTALK_MANUAL_SETUP.md` 为准,本 skill 不复制其实现细节。 + +##### ③ 审批开通 + 交叉验证(建号 ≠ 可用,最易踩) + +建号成功 / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` / Stream WS 连上**都不是** DingTalk 的能力回执,**不能当作"建联成功"**。唯一可靠判定是真实发消息探测: + +``` +# 用新应用凭据探测真实发消息能力(appKey/appSecret 即 clientId/clientSecret) TOKEN=$(curl -s -X POST 'https://api.dingtalk.com/v1.0/oauth2/accessToken' \ -H 'Content-Type: application/json' -d '{"appKey":"<新clientId>","appSecret":"<新secret>"}' | jq -r .accessToken) curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ @@ -1141,10 +1162,12 @@ curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ # → 参数类错误(非 AccessDenied) = scope 已通,机器人可收发 ``` +身份交叉验证(appKey ≠ robotCode): `dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json` 查它名下机器人,拿 robotName / robotCode。 + 注意: - - **分层理解**:① 建号是钉钉 OpenAPI 能力,宿主无关;② 这条 skill 指南覆盖 dws 支持的全部主流 agent(Claude Code / Cursor / Codex / Qoder / opencode / Gemini / Windsurf / Cline / Kiro / Trae / Hermes / OpenClaw 等,约 14 个 agent home);③ 收→回运行时有两条**对等**路径(`openclaw/plugin-sdk` 契约 / OpenAI-compatible endpoint)。**不是只支持 OpenClaw**(`peerDependencies.openclaw` 标记为 optional)。 - - **建号 ≠ 可用(血泪闸门,最易踩)**:device-auth 返回 `SUCCESS` / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` 只是 stream WS 连上,**不是** DingTalk 的能力回执——**绝不能拿它当"建联成功"**。唯一可靠判定 = 第 5 步真实能力探测;开通入口就在钉钉返回的报错里:`open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg`。 - - 这条 recipe 依赖 dws 之外的前置(宿主运行时 + npm 网络 + 手机扫码 + **robot scope 审批**),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **整条链路依赖 dws 之外的前置**(建号=上游 MCP 工具上线,或自带创建者凭证;建联=agent 运行时;两段都要 robot scope 审批),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **建号 ≠ 可用(血泪闸门,最易踩)**:能换 `access_token` 只代表【应用凭据】生效,机器人收发还需 `qyapi_robot_sendmsg` 审批;`connected` 不是能力回执。唯一可靠判定 = 第 ③ 段真实能力探测。 + - **mediaId**:`robotMediaId` / `previewMediaId` 需先经 `media.upload` 上传获得;留空走服务端默认图标。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 - 安全: 机器人在你的授权范围内以你的身份行事,按个人助理对待,勿用于无人值守的生产部署。 From 134cf00bb186a5b54e2318237a2645c8e8663d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 4 Jun 2026 11:59:03 +0800 Subject: [PATCH 06/10] =?UTF-8?q?feat(chat):=20=E6=96=B0=E5=A2=9E=20dws=20?= =?UTF-8?q?chat=20bot=20create=EF=BC=8C=E7=BB=8F=20MCP=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E5=BB=BA=E9=92=89=E9=92=89=E6=9C=BA=E5=99=A8=E4=BA=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增原生命令 `dws chat bot create`,调用「钉钉开放平台应用管理」MCP 的 create_dingtalk_robot 工具,一次性建号(企业自建 Agent 应用 + 承载机器人), 返回 agentId / robotCode / clientId / clientSecret(clientSecret 仅返回一次)。 corpId / userid 由 MCP 服务端按当前登录身份注入。 - internal/helpers/chat.go:新增 create 子命令 + robotCreateInvoke, 路由 CanonicalProduct "opendev",必填 --app-name/--robot-name/--desc, 可选 --robot-media-id/--preview-media-id - internal/app/direct_runtime.go:硬编码 opendev 的 MCP 端点(不走服务发现), 仿 PAT built-in;env-var DINGTALK_OPENDEV_MCP_URL 仍可覆盖 - internal/transport/client.go:支持 MCP 广场自包含 ?key= URL——可信域名上带 key 的端点保留 query 并跳过会话 bearer(key 即凭据),否则维持原有 strip query + bearer 行为(防参数注入)。含单元测试,普通端点无回归 - skills:chat.md / SKILL.md / best_practices 三处同步为真实命令, 替换原「目标态/依赖上游 MCP 工具上线」占位措辞 预发已端到端验证:dws chat bot create 成功建号并返回完整凭据。 --- internal/app/direct_runtime.go | 23 +++++++ internal/helpers/chat.go | 69 +++++++++++++++++++ internal/transport/client.go | 36 ++++++++-- internal/transport/client_test.go | 40 +++++++++++ .../references/best_practices/01-messaging.md | 2 +- skills/mono/references/products/chat.md | 34 ++++----- skills/multi/dingtalk-chat/SKILL.md | 4 +- .../dingtalk-chat/references/01-messaging.md | 2 +- skills/multi/dingtalk-chat/references/chat.md | 34 ++++----- 9 files changed, 192 insertions(+), 52 deletions(-) diff --git a/internal/app/direct_runtime.go b/internal/app/direct_runtime.go index b46e0f9f..74e53354 100644 --- a/internal/app/direct_runtime.go +++ b/internal/app/direct_runtime.go @@ -47,6 +47,18 @@ const ( defaultPATServerID = "abc3c880fb90f04b52d1426aaf093766e5fc9ec38411688cbb74df42a584d374" ) +// Hardcoded built-in endpoint for the DingTalk open-platform app-management MCP +// server, which hosts the robot-provisioning tool create_dingtalk_robot. This is +// wired by source (NOT service discovery) per product decision: the helper +// command `dws chat bot create` routes CanonicalProduct "opendev" here. The full +// URL — host, server hash and access key — is pinned literally so the call always +// reaches the (currently pre-only) gateway regardless of ~/.dws/mcp_url. Update +// the URL when the production server ships. +const ( + robotCreateProductID = "opendev" + robotCreateEndpoint = "https://pre-mcp-gw.dingtalk.com/server/d0d388844aa5537c4cc8d749e01ffa13909d4fecb70bc576995beb763253eeed?key=73dd09d84e42543474d0cfac20339f17" +) + func defaultPATServerDescriptor() market.ServerDescriptor { return market.ServerDescriptor{ Key: defaultPATProductID, @@ -241,6 +253,17 @@ func directRuntimeEndpoint(productID, toolName string) (string, bool) { } } + // Hardcoded built-in: the robot-provisioning product is pinned to a fixed + // MCP server in source (NOT service discovery), per product decision. Placed + // after the env-var override but before the dynamic/discovery lookups so the + // pinned endpoint stays authoritative even if a same-named product later + // shows up in discovery. + for _, candidate := range []string{strings.TrimSpace(productID), normalized} { + if candidate == robotCreateProductID { + return robotCreateEndpoint, true + } + } + dynamicMu.RLock() de := dynamicEndpoints te := dynamicToolEndpoints diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index 292b103f..c3c404c4 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -117,6 +117,7 @@ func (chatHandler) Command(runner executor.Runner) *cobra.Command { bot.AddCommand( newChatBotFindCommand(runner), newChatBotSearchCommand(runner), + newChatBotCreateCommand(runner), ) root.AddCommand(message, group, bot) @@ -140,6 +141,74 @@ func botInvoke(runner executor.Runner, cmd *cobra.Command, tool string, params m return writeCommandPayload(cmd, result) } +// robotCreateInvoke routes robot-provisioning tools to the open-platform +// app-management MCP server via CanonicalProduct "opendev". That product's +// endpoint is hardcoded in internal/app/direct_runtime.go (NOT resolved from +// service discovery), so this command works without any discovery/overlay entry. +func robotCreateInvoke(runner executor.Runner, cmd *cobra.Command, tool string, params map[string]any) error { + invocation := executor.NewHelperInvocation( + cobracmd.LegacyCommandPath(cmd), + "opendev", + tool, + params, + ) + invocation.DryRun = commandDryRun(cmd) + result, err := runner.Run(cmd.Context(), invocation) + if err != nil { + return err + } + return writeCommandPayload(cmd, result) +} + +// newChatBotCreateCommand creates `dws chat bot create`, the robot-provisioning +// command. It calls the create_dingtalk_robot MCP tool, which creates an +// enterprise self-built Agent app plus its hosting robot in one shot and returns +// agentId / robotCode / clientId / clientSecret (clientSecret is returned only +// once). corpId and userid are injected server-side from the current login. +func newChatBotCreateCommand(runner executor.Runner) *cobra.Command { + cmd := &cobra.Command{ + Use: "create", + Short: "创建钉钉智能体机器人", + Long: "一次性创建企业自建 Agent 应用及承载机器人,返回 agentId / robotCode / clientId / clientSecret。⚠️ clientSecret 仅返回一次,请立即安全保存。corpId 和 userid 由 MCP 服务端按当前登录身份注入。", + Example: " dws chat bot create --app-name \"销售助手\" --robot-name \"销售助手机器人\" --desc \"销售线索查询与客户跟进\"", + Args: cobra.NoArgs, + DisableAutoGenTag: true, + RunE: func(cmd *cobra.Command, args []string) error { + appName, _ := cmd.Flags().GetString("app-name") + robotName, _ := cmd.Flags().GetString("robot-name") + desc, _ := cmd.Flags().GetString("desc") + if strings.TrimSpace(appName) == "" { + return apperrors.NewValidation("--app-name is required") + } + if strings.TrimSpace(robotName) == "" { + return apperrors.NewValidation("--robot-name is required") + } + if strings.TrimSpace(desc) == "" { + return apperrors.NewValidation("--desc is required") + } + params := map[string]any{ + "appName": appName, + "robotName": robotName, + "desc": desc, + } + if v, _ := cmd.Flags().GetString("robot-media-id"); strings.TrimSpace(v) != "" { + params["robotMediaId"] = v + } + if v, _ := cmd.Flags().GetString("preview-media-id"); strings.TrimSpace(v) != "" { + params["previewMediaId"] = v + } + return robotCreateInvoke(runner, cmd, "create_dingtalk_robot", params) + }, + } + preferLegacyLeaf(cmd) + cmd.Flags().String("app-name", "", "智能体应用名称,2~20 字,企业内唯一 (必填)") + cmd.Flags().String("robot-name", "", "承载机器人名称,2~20 字 (必填)") + cmd.Flags().String("desc", "", "机器人功能描述,≤200 字 (必填)") + cmd.Flags().String("robot-media-id", "", "机器人图标 mediaId(可选,留空用服务端默认图标)") + cmd.Flags().String("preview-media-id", "", "机器人预览图 mediaId(可选,留空复用 --robot-media-id)") + return cmd +} + func newChatBotFindCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "find", diff --git a/internal/transport/client.go b/internal/transport/client.go index e22b90f2..37b5bc72 100644 --- a/internal/transport/client.go +++ b/internal/transport/client.go @@ -494,8 +494,17 @@ func (c *Client) callJSONRPC(ctx context.Context, endpoint string, request reque } func (c *Client) doWithRetry(ctx context.Context, endpoint string, body []byte) (*http.Response, error) { - // Strip any query/fragment from the endpoint to prevent parameter injection. - endpoint = validate.StripQueryFragment(endpoint) + // MCP marketplace "self-contained" URLs carry a pre-authed ?key=<...> that IS + // the credential (bound at issue time to the user+org). For those, preserve + // the query verbatim and do NOT attach the session bearer token: the gateway + // authenticates by the key, and a conflicting bearer makes it resolve the MCP + // via the caller's discovery context instead, failing with "MCP不存在". For + // every other endpoint, strip the query/fragment to prevent parameter + // injection (the bearer token is the auth). + keyAuthed := endpointHasPreAuthKey(endpoint) && c.isEndpointTrusted(endpoint) + if !keyAuthed { + endpoint = validate.StripQueryFragment(endpoint) + } var lastErr error for attempt := 0; attempt <= c.MaxRetries; attempt++ { req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, bytes.NewReader(body)) @@ -518,10 +527,12 @@ func (c *Client) doWithRetry(ctx context.Context, endpoint string, body []byte) if c.ExecutionId != "" { req.Header.Set(HeaderExecutionId, c.ExecutionId) } - if token := sanitizeBearerToken(c.AuthToken); token != "" { - if c.isEndpointTrusted(endpoint) { - req.Header.Set("Authorization", "Bearer "+token) - req.Header.Set("x-user-access-token", token) + if !keyAuthed { + if token := sanitizeBearerToken(c.AuthToken); token != "" { + if c.isEndpointTrusted(endpoint) { + req.Header.Set("Authorization", "Bearer "+token) + req.Header.Set("x-user-access-token", token) + } } } for key, value := range c.ExtraHeaders { @@ -719,6 +730,19 @@ func parseRetryAfter(raw string) (time.Duration, bool) { return delay, true } +// endpointHasPreAuthKey reports whether the endpoint carries a pre-authed +// ?key=<...> query parameter, as issued by the DingTalk MCP marketplace for +// self-contained (user+org-bound) server URLs. Such keys are themselves the +// credential and must be forwarded verbatim; the session bearer token is +// omitted for them (see doWithRetry). +func endpointHasPreAuthKey(endpoint string) bool { + parsed, err := url.Parse(endpoint) + if err != nil { + return false + } + return strings.TrimSpace(parsed.Query().Get("key")) != "" +} + // isEndpointTrusted checks whether the endpoint is HTTPS and belongs to a // trusted domain. When no auth token is set this is a no-op. // Set DWS_ALLOW_HTTP_ENDPOINTS=1 for development/testing to allow HTTP, but diff --git a/internal/transport/client_test.go b/internal/transport/client_test.go index 27d78c65..154f6c48 100644 --- a/internal/transport/client_test.go +++ b/internal/transport/client_test.go @@ -330,6 +330,46 @@ func TestCallToolInjectsAuthHeaders(t *testing.T) { } } +// TestCallToolKeyURLKeepsQueryAndOmitsBearer verifies that for a self-contained +// MCP marketplace URL carrying a pre-authed ?key=<...>, the transport forwards +// the key verbatim (query preserved) and does NOT attach the session bearer +// token — the key is the credential. A conflicting bearer makes the gateway +// resolve the MCP via the caller's discovery context and fail ("MCP不存在"). +func TestCallToolKeyURLKeepsQueryAndOmitsBearer(t *testing.T) { + t.Parallel() + + server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if got := r.URL.Query().Get("key"); got != "k-secret-123" { + t.Fatalf("key query = %q, want k-secret-123 (query must be preserved)", got) + } + if got := r.Header.Get("Authorization"); got != "" { + t.Fatalf("Authorization header = %q, want empty (bearer must be omitted for key URLs)", got) + } + if got := r.Header.Get("x-user-access-token"); got != "" { + t.Fatalf("x-user-access-token = %q, want empty (omitted for key URLs)", got) + } + _ = json.NewEncoder(w).Encode(map[string]any{ + "jsonrpc": "2.0", + "id": 3, + "result": map[string]any{"content": map[string]any{"ok": true}}, + }) + })) + defer server.Close() + + client := NewClient(server.Client()) + client.AuthToken = "test-token" // present but must be skipped for key URLs + client.TrustedDomains = []string{"127.0.0.1"} + + endpoint := server.URL + "?key=k-secret-123" + result, err := client.CallTool(context.Background(), endpoint, "create_dingtalk_robot", map[string]any{"appName": "x"}) + if err != nil { + t.Fatalf("CallTool() error = %v", err) + } + if result.Content["ok"] != true { + t.Fatalf("CallTool() content = %#v, want ok=true", result.Content) + } +} + func TestCallToolAcceptsStructuredContentResults(t *testing.T) { t.Parallel() diff --git a/skills/mono/references/best_practices/01-messaging.md b/skills/mono/references/best_practices/01-messaging.md index 0b50b3fb..cb828fe6 100644 --- a/skills/mono/references/best_practices/01-messaging.md +++ b/skills/mono/references/best_practices/01-messaging.md @@ -4,7 +4,7 @@ | Recipe | 行动指南(固定路线) | |--------|-------------------| -| provision-bot | 建号→建联→审批三段(详见 [chat.md](../products/chat.md) 「创建【新】机器人 + 建联」节):
① **建号**:服务端 API `microApp/agent/create` 一次拿 robotCode/clientId/clientSecret(**目标态,依赖上游 MCP 工具上线**;临时可 `dws api POST /v1.0/microApp/agent/create --token <创建者token> --data '{...}'` 自带凭据验证)
② **Stream 建联**:拿 clientId/secret 起 DWClient 订阅 TOPIC_ROBOT,agent 收→回(运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 dingtalk-openclaw-connector)
③ **审批+验证**:`qyapi_robot_sendmsg` scope 审批(建号≠可用,connected 不是能力回执,真实发消息探测才算数)+ `chat bot search` 交叉验证 | +| provision-bot | 建号→建联→审批三段(详见 [chat.md](../products/chat.md) 「创建【新】机器人 + 建联」节):
① **建号**:`dws chat bot create --app-name <名> --robot-name <名> --desc <描述>`(调 MCP 工具 create_dingtalk_robot,端点硬编码,一次拿 agentId/robotCode/clientId/clientSecret,secret 仅返回一次)
② **Stream 建联**:拿 clientId/secret 起 DWClient 订阅 TOPIC_ROBOT,agent 收→回(运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 dingtalk-openclaw-connector)
③ **审批+验证**:`qyapi_robot_sendmsg` scope 审批(建号≠可用,connected 不是能力回执,真实发消息探测才算数)+ `chat bot search` 交叉验证 | | query-group-chat | **优先**:`chat_export_messages.py`(开源版未引入;可手动用 `dws chat message list` 翻页后写入文件)(自动搜群+翻页+导出)
备选: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 | **优先**:`chat_history_with_user.py`(开源版未引入;可手动用 `dws chat search` + `dws chat message list` 组合)(自动搜人+翻页+导出)
备选: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 "<内容>"`(必填项见 [ding.md](../products/ding.md))
2. `chat message send --group --text "<内容>"` 群里提醒(可选 `--title` / `@` 见 [chat.md](../products/chat.md))
3. `todo task create --title "<标题>" --executors --priority 40` 建紧急待办
前置:`aisearch person --keyword "<姓名>" --dimension name` → 取 `userId`;`chat search --query "<群名>"` → 取 `openConversationId` | diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index 5e83481b..0db6ce48 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -1105,31 +1105,23 @@ search 与 find 选择指南: 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 -##### ① 建号:服务端 API 一次拿到 robotCode + clientId + clientSecret +##### ① 建号:`dws chat bot create` 一次拿到 robotCode + clientId + clientSecret -钉钉服务端接口 `POST /v1.0/microApp/agent/create` 一次调用直接建号,返回 `robotCode` / `clientId` / `clientSecret`(**clientSecret 仅返回一次**),无需扫码、可无人值守。 - -> ⚠️ **状态:目标态,依赖上游 MCP 工具上线(规划中)。** -> 落地方式是把这个接口封成**服务端 MCP 工具**挂到 mcp-gw:客户端(dws)用普通会话凭证即可调用,**本地无需持有 AppKey/AppSecret**;建号所需的创建者凭证与建号 scope 由 MCP 服务端持有,并由服务端校验「调用方有权为该企业 / 该 userId 建号」(同 org、管理员或本人)。工具上线进入服务发现后,dws 会自动生成对应命令。 -> **它上线前,不要凭空写出 discovery 里不存在的命令**(幽灵命令);建号这步当前在 dws 内不可直接闭环。 - -当前临时验证(需自带带建号 scope 的创建者凭证 access_token,凭据由调用者自备): +`dws chat bot create` 调用服务端 MCP 工具 `create_dingtalk_robot`,一次性创建企业自建 Agent 应用及承载机器人,返回 `agentId` / `robotCode` / `clientId` / `clientSecret`(**clientSecret 仅返回一次,立即安全保存**)。无需扫码、可无人值守;`corpId` / `userid` 由 MCP 服务端按当前登录身份注入。 ``` -dws api POST /v1.0/microApp/agent/create \ - --token <创建者ACCESS_TOKEN> \ - --data '{ - "userId": "<归属人 staffId>", - "appName": "<2~20,企业内唯一>", - "robotName": "<2~20>", - "desc": "<≤200>", - "robotMediaId": "", - "previewMediaId": "" - }' -# 返回 robotCode / clientId / clientSecret(secret 仅此一次,立即安全保存) -# 接口契约仍是草稿:域名(api/oapi)、字段(userId/userid)、返回(agentId/appId)以真实返回为准 +dws chat bot create \ + --app-name "销售助手" \ # 智能体应用名,2~20 字,企业内唯一(必填) + --robot-name "销售助手机器人" \ # 承载机器人名,2~20 字(必填) + --desc "销售线索查询与客户跟进" \ # 功能描述,≤200 字(必填) + --robot-media-id "@..." \ # 可选:机器人图标 mediaId,留空用服务端默认图标 + --preview-media-id "@..." # 可选:预览图 mediaId,留空复用 --robot-media-id +# 返回 agentId / robotCode / clientId / clientSecret(secret 仅此一次,务必立即保存) ``` +> 实现说明:该命令的 MCP 端点在 dws 内**硬编码**(不走服务发现),当前指向「钉钉开放平台应用管理」MCP。它是 MCP 广场的自包含 `?key=` URL,dws transport 对带 key 的 URL 保留 query 并跳过会话 bearer(key 即凭据,详见 `internal/transport`、`internal/app/direct_runtime.go`)。 +> `--robot-media-id` / `--preview-media-id` 的 mediaId 需先经 `media.upload` 上传获得。 + ##### ② Stream 建联:让机器人收→回(无公网 IP / Webhook) 建号本质只是拿到 `clientId` / `clientSecret`。**拿到这对凭据后,由一个 agent 运行时起 Stream 连接即可驱动收发,与凭据从哪来无关**(API 建号 / 手动从开放平台后台抄,建联这段都一样)。机制(参考实现:[dingtalk-openclaw-connector](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector)): @@ -1165,7 +1157,7 @@ curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ 身份交叉验证(appKey ≠ robotCode): `dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json` 查它名下机器人,拿 robotName / robotCode。 注意: - - **整条链路依赖 dws 之外的前置**(建号=上游 MCP 工具上线,或自带创建者凭证;建联=agent 运行时;两段都要 robot scope 审批),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **链路前置**:建号已是原生命令 `dws chat bot create`(MCP 端点硬编码,dws 内可直接闭环);建联需一个 agent 运行时;两段都要 robot scope 审批。前置不满足时明确告知用户,不要伪造创建成功。 - **建号 ≠ 可用(血泪闸门,最易踩)**:能换 `access_token` 只代表【应用凭据】生效,机器人收发还需 `qyapi_robot_sendmsg` 审批;`connected` 不是能力回执。唯一可靠判定 = 第 ③ 段真实能力探测。 - **mediaId**:`robotMediaId` / `previewMediaId` 需先经 `media.upload` 上传获得;留空走服务端默认图标。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 diff --git a/skills/multi/dingtalk-chat/SKILL.md b/skills/multi/dingtalk-chat/SKILL.md index 231943db..689dd27f 100644 --- a/skills/multi/dingtalk-chat/SKILL.md +++ b/skills/multi/dingtalk-chat/SKILL.md @@ -36,7 +36,7 @@ metadata: | "Webhook 推一条" | `dws chat message send-by-webhook --token --title "<标题>" --text "<内容>"` | | "撤回机器人消息" | `dws chat message recall-by-bot --robot-code --group --keys `(只能撤回机器人发的;撤回普通用户消息开源 dws v1.0.30 暂不支持)| | "搜机器人" / "查我创建的机器人" | `dws chat bot find --query "<关键词>"`(全部可用,带 openDingTalkId)/ `dws chat bot search`(仅我创建的)| -| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw/Hermes/Qoder/Claude Code" | 三段链路:① 建号(服务端 API `microApp/agent/create` 一次拿 robotCode/clientId/clientSecret,**目标态依赖上游 MCP 工具上线**)→ ② Stream 建联(拿 clientId/secret 起 DWClient,运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint)→ ③ `qyapi_robot_sendmsg` 审批 + `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人 + 建联」节 | +| "给我建个机器人" / "给 agent 接入钉钉" / "接入 OpenClaw/Hermes/Qoder/Claude Code" | 三段链路:① 建号 `dws chat bot create --app-name <名> --robot-name <名> --desc <描述>`(调 MCP 工具 create_dingtalk_robot,一次拿 agentId/robotCode/clientId/clientSecret,secret 仅返回一次)→ ② Stream 建联(拿 clientId/secret 起 DWClient,运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint)→ ③ `qyapi_robot_sendmsg` 审批 + `dws chat bot search` 交叉验证。固定路线见 [chat.md](references/chat.md) 「创建【新】机器人 + 建联」节 | > **注**:v1.0.30 起 `chat message send / send-by-bot / send-by-webhook` 全部强制 `--title` 必填(单聊群聊都要)。 @@ -46,4 +46,4 @@ metadata: - 要发图片/文件 → 先 `dt_media_upload` 上传 → `python scripts/extract_media_id.py ""` 提取 mediaId → 再用 `--media-id` - 紧急升级(应用内/短信/电话)→ 切到 `dingtalk-ding` - 发邮件 → 切到 `dingtalk-mail` -- 要**新建**机器人 / 给 agent 接入钉钉 → dws 只能搜不能建。建号走服务端 API `microApp/agent/create`(**目标态依赖上游 MCP 工具上线**,封成 mcp-gw 工具后 dws 自动生成命令)拿 clientId/clientSecret,再由 agent 运行时起 Stream 建联(两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 `@dingtalk-real-ai/dingtalk-connector`);见 [chat.md](references/chat.md) 「创建【新】机器人 + 建联」节,建完过 `qyapi_robot_sendmsg` 审批 + `dws chat bot search` 交叉验证 +- 要**新建**机器人 / 给 agent 接入钉钉 → 用原生命令 `dws chat bot create`(调 MCP 工具 create_dingtalk_robot,端点在 dws 内硬编码,拿 clientId/clientSecret),再由 agent 运行时起 Stream 建联(两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 `@dingtalk-real-ai/dingtalk-connector`);见 [chat.md](references/chat.md) 「创建【新】机器人 + 建联」节,建完过 `qyapi_robot_sendmsg` 审批 + `dws chat bot search` 交叉验证 diff --git a/skills/multi/dingtalk-chat/references/01-messaging.md b/skills/multi/dingtalk-chat/references/01-messaging.md index d68cae15..4799fd0c 100644 --- a/skills/multi/dingtalk-chat/references/01-messaging.md +++ b/skills/multi/dingtalk-chat/references/01-messaging.md @@ -6,7 +6,7 @@ | Recipe | 行动指南(固定路线) | |--------|-------------------| -| provision-bot | 建号→建联→审批三段(详见 [chat.md](./chat.md) 「创建【新】机器人 + 建联」节):
① **建号**:服务端 API `microApp/agent/create` 一次拿 robotCode/clientId/clientSecret(**目标态,依赖上游 MCP 工具上线**;临时可 `dws api POST /v1.0/microApp/agent/create --token <创建者token> --data '{...}'` 自带凭据验证)
② **Stream 建联**:拿 clientId/secret 起 DWClient 订阅 TOPIC_ROBOT,agent 收→回(运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 dingtalk-openclaw-connector)
③ **审批+验证**:`qyapi_robot_sendmsg` scope 审批(建号≠可用,connected 不是能力回执,真实发消息探测才算数)+ `chat bot search` 交叉验证 | +| provision-bot | 建号→建联→审批三段(详见 [chat.md](./chat.md) 「创建【新】机器人 + 建联」节):
① **建号**:`dws chat bot create --app-name <名> --robot-name <名> --desc <描述>`(调 MCP 工具 create_dingtalk_robot,端点硬编码,一次拿 agentId/robotCode/clientId/clientSecret,secret 仅返回一次)
② **Stream 建联**:拿 clientId/secret 起 DWClient 订阅 TOPIC_ROBOT,agent 收→回(运行时两条对等路径:plugin-sdk 契约 / OpenAI-compatible endpoint,参考 dingtalk-openclaw-connector)
③ **审批+验证**:`qyapi_robot_sendmsg` scope 审批(建号≠可用,connected 不是能力回执,真实发消息探测才算数)+ `chat bot search` 交叉验证 | | 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-direct --user --time ""` → 取消息列表
3. **翻页**:同 query-group-chat
4. 合并全部消息后总结 | | escalate-ding | 三级升级:
1. `ding message send --robot-code --type app --users --content "<内容>"`(必填项见 `dingtalk-ding/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` | diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index 15d7f9f0..d9c2844c 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -1105,31 +1105,23 @@ search 与 find 选择指南: 典型触发词: "给我建个机器人""创建一个机器人""给 agent 接个钉钉机器人""接入 OpenClaw / Hermes""provision a bot"。 -##### ① 建号:服务端 API 一次拿到 robotCode + clientId + clientSecret +##### ① 建号:`dws chat bot create` 一次拿到 robotCode + clientId + clientSecret -钉钉服务端接口 `POST /v1.0/microApp/agent/create` 一次调用直接建号,返回 `robotCode` / `clientId` / `clientSecret`(**clientSecret 仅返回一次**),无需扫码、可无人值守。 - -> ⚠️ **状态:目标态,依赖上游 MCP 工具上线(规划中)。** -> 落地方式是把这个接口封成**服务端 MCP 工具**挂到 mcp-gw:客户端(dws)用普通会话凭证即可调用,**本地无需持有 AppKey/AppSecret**;建号所需的创建者凭证与建号 scope 由 MCP 服务端持有,并由服务端校验「调用方有权为该企业 / 该 userId 建号」(同 org、管理员或本人)。工具上线进入服务发现后,dws 会自动生成对应命令。 -> **它上线前,不要凭空写出 discovery 里不存在的命令**(幽灵命令);建号这步当前在 dws 内不可直接闭环。 - -当前临时验证(需自带带建号 scope 的创建者凭证 access_token,凭据由调用者自备): +`dws chat bot create` 调用服务端 MCP 工具 `create_dingtalk_robot`,一次性创建企业自建 Agent 应用及承载机器人,返回 `agentId` / `robotCode` / `clientId` / `clientSecret`(**clientSecret 仅返回一次,立即安全保存**)。无需扫码、可无人值守;`corpId` / `userid` 由 MCP 服务端按当前登录身份注入。 ``` -dws api POST /v1.0/microApp/agent/create \ - --token <创建者ACCESS_TOKEN> \ - --data '{ - "userId": "<归属人 staffId>", - "appName": "<2~20,企业内唯一>", - "robotName": "<2~20>", - "desc": "<≤200>", - "robotMediaId": "", - "previewMediaId": "" - }' -# 返回 robotCode / clientId / clientSecret(secret 仅此一次,立即安全保存) -# 接口契约仍是草稿:域名(api/oapi)、字段(userId/userid)、返回(agentId/appId)以真实返回为准 +dws chat bot create \ + --app-name "销售助手" \ # 智能体应用名,2~20 字,企业内唯一(必填) + --robot-name "销售助手机器人" \ # 承载机器人名,2~20 字(必填) + --desc "销售线索查询与客户跟进" \ # 功能描述,≤200 字(必填) + --robot-media-id "@..." \ # 可选:机器人图标 mediaId,留空用服务端默认图标 + --preview-media-id "@..." # 可选:预览图 mediaId,留空复用 --robot-media-id +# 返回 agentId / robotCode / clientId / clientSecret(secret 仅此一次,务必立即保存) ``` +> 实现说明:该命令的 MCP 端点在 dws 内**硬编码**(不走服务发现),当前指向「钉钉开放平台应用管理」MCP。它是 MCP 广场的自包含 `?key=` URL,dws transport 对带 key 的 URL 保留 query 并跳过会话 bearer(key 即凭据,详见 `internal/transport`、`internal/app/direct_runtime.go`)。 +> `--robot-media-id` / `--preview-media-id` 的 mediaId 需先经 `media.upload` 上传获得。 + ##### ② Stream 建联:让机器人收→回(无公网 IP / Webhook) 建号本质只是拿到 `clientId` / `clientSecret`。**拿到这对凭据后,由一个 agent 运行时起 Stream 连接即可驱动收发,与凭据从哪来无关**(API 建号 / 手动从开放平台后台抄,建联这段都一样)。机制(参考实现:[dingtalk-openclaw-connector](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector)): @@ -1165,7 +1157,7 @@ curl -s -X POST 'https://api.dingtalk.com/v1.0/robot/groupMessages/send' \ 身份交叉验证(appKey ≠ robotCode): `dws chat bot search --client-id <新clientId> --client-secret <新secret> --format json` 查它名下机器人,拿 robotName / robotCode。 注意: - - **整条链路依赖 dws 之外的前置**(建号=上游 MCP 工具上线,或自带创建者凭证;建联=agent 运行时;两段都要 robot scope 审批),**不是纯 dws 能闭环的**;前置不满足时明确告知用户,不要伪造创建成功。 + - **链路前置**:建号已是原生命令 `dws chat bot create`(MCP 端点硬编码,dws 内可直接闭环);建联需一个 agent 运行时;两段都要 robot scope 审批。前置不满足时明确告知用户,不要伪造创建成功。 - **建号 ≠ 可用(血泪闸门,最易踩)**:能换 `access_token` 只代表【应用凭据】生效,机器人收发还需 `qyapi_robot_sendmsg` 审批;`connected` 不是能力回执。唯一可靠判定 = 第 ③ 段真实能力探测。 - **mediaId**:`robotMediaId` / `previewMediaId` 需先经 `media.upload` 上传获得;留空走服务端默认图标。 - 创建成功后,发消息/拉群仍走上面的 `chat message send-by-bot` / `group members add-bot`,用 `dws chat bot search` 返回的 `robotCode`。 From ca390136690dc2c5ed7321ecdf80101e4a5ad0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 4 Jun 2026 12:10:25 +0800 Subject: [PATCH 07/10] =?UTF-8?q?docs(chat-skill):=20=E5=9B=BA=E5=8C=96?= =?UTF-8?q?=E5=BB=BA=E8=81=94=E6=94=B6=E2=86=92=E5=9B=9E=E9=AA=8C=E8=AF=81?= =?UTF-8?q?=E8=84=9A=E6=9C=AC=20bot=5Fstream=5Fprobe.js=20+=20=E8=87=AA?= =?UTF-8?q?=E6=A3=80=E6=AD=A5=E9=AA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增 scripts/bot_stream_probe.js(mono+multi):用 dingtalk-stream DWClient 起 Stream 连接、订阅 TOPIC_ROBOT,收到 @机器人 消息后经 sessionWebhook 回 echo, 最小验证 dws chat bot create 建出机器人的「建联 + 收→回」闭环,不依赖完整 OpenClaw/Hermes 运行时。chat.md ② 段补「建联自检」步骤指向该脚本。 预发已验证:connect success(建联通过)。收→回的最后一公里需机器人过 qyapi_robot_sendmsg 审批 + 一条真实 @ 消息触发。 --- skills/mono/references/products/chat.md | 9 +++ skills/mono/scripts/bot_stream_probe.js | 67 +++++++++++++++++++ skills/multi/dingtalk-chat/references/chat.md | 9 +++ .../dingtalk-chat/scripts/bot_stream_probe.js | 67 +++++++++++++++++++ 4 files changed, 152 insertions(+) create mode 100644 skills/mono/scripts/bot_stream_probe.js create mode 100644 skills/multi/dingtalk-chat/scripts/bot_stream_probe.js diff --git a/skills/mono/references/products/chat.md b/skills/mono/references/products/chat.md index 0db6ce48..2db01586 100644 --- a/skills/mono/references/products/chat.md +++ b/skills/mono/references/products/chat.md @@ -1138,6 +1138,15 @@ dws chat bot create \ > 账号文件字段、启动、多机器人 `chatbotUserId` 协作等具体配置,以 connector 仓 README / `docs/DINGTALK_MANUAL_SETUP.md` 为准,本 skill 不复制其实现细节。 +**建联自检(不依赖完整 agent 运行时)**:用 `scripts/bot_stream_probe.js` 最小验证「建联 + 收→回」——起 DWClient 连接、订阅 `TOPIC_ROBOT`、收到 @机器人 消息后经 `sessionWebhook` 回 echo: + +``` +npm i dingtalk-stream +CID=<新clientId> SEC=<新secret> node scripts/bot_stream_probe.js +# 看到 "connect success" = 建联通过;再在钉钉把机器人拉进群 @它 一句, +# 看到 ">>> 收到消息" = 收通过;">>> 回复结果: 200" = 回通过(回复需先过 ③ 的 scope 审批) +``` + ##### ③ 审批开通 + 交叉验证(建号 ≠ 可用,最易踩) 建号成功 / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` / Stream WS 连上**都不是** DingTalk 的能力回执,**不能当作"建联成功"**。唯一可靠判定是真实发消息探测: diff --git a/skills/mono/scripts/bot_stream_probe.js b/skills/mono/scripts/bot_stream_probe.js new file mode 100644 index 00000000..c3b7baed --- /dev/null +++ b/skills/mono/scripts/bot_stream_probe.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * bot_stream_probe.js — 钉钉机器人「收→回」最小验证脚本 + * + * 用 dingtalk-stream SDK 起 DWClient Stream 连接,订阅 TOPIC_ROBOT;收到 + * @机器人 的消息后:ack → 打印消息 → 通过 sessionWebhook 回一条 echo。 + * 用来验证 `dws chat bot create` 建出的机器人「建联 + 收→回」闭环, + * 不依赖完整 OpenClaw/Hermes 运行时(机制等同 connector 的 connection.ts)。 + * + * 用法: + * npm i dingtalk-stream + * CID= SEC= node bot_stream_probe.js + * 然后在钉钉里把该机器人拉进群并 @它 发一句话。 + * + * 退出码:0 正常运行/收到消息;1 连接失败;2 缺凭据。 + * + * 注意(建号 ≠ 可用):连接与收消息只需有效应用凭据;但「回复」需机器人已通过 + * qyapi_robot_sendmsg 审批,未通过时连接/收消息正常、回复会被钉钉拒绝。 + * 审批入口见钉钉报错里的 open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg + */ +const { DWClient, TOPIC_ROBOT } = require('dingtalk-stream'); + +const clientId = process.env.CID; +const clientSecret = process.env.SEC; +if (!clientId || !clientSecret) { + console.error('缺少凭据:请设置环境变量 CID= SEC='); + process.exit(2); +} + +const client = new DWClient({ clientId, clientSecret }); + +client.registerCallbackListener(TOPIC_ROBOT, async (res) => { + // 立即 ack,避免钉钉重复投递 + const messageId = res.headers && res.headers.messageId; + if (messageId) client.socketCallBackResponse(messageId, { success: true }); + + let data = {}; + try { data = JSON.parse(res.data); } catch (_) { /* ignore */ } + const text = ((data.text && data.text.content) || '').trim(); + console.log('>>> 收到消息:', { + sender: data.senderNick, + conversationType: data.conversationType, // 1=单聊 2=群聊 + text, + sessionWebhook: data.sessionWebhook ? '有' : '无', + }); + + // 通过 sessionWebhook 回一条 echo(最小「回」验证) + if (data.sessionWebhook) { + try { + const r = await fetch(data.sessionWebhook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ msgtype: 'text', text: { content: `收到:${text || '(空)'}` } }), + }); + console.log('>>> 回复结果:', r.status, await r.text()); + } catch (e) { + console.error('>>> 回复失败:', (e && e.message) || e); + } + } +}); + +client.connect().then(() => { + console.log('Stream 已连接,订阅 TOPIC_ROBOT,等待 @机器人 消息… (Ctrl-C 退出)'); +}).catch((e) => { + console.error('连接失败:', (e && e.message) || e); + process.exit(1); +}); diff --git a/skills/multi/dingtalk-chat/references/chat.md b/skills/multi/dingtalk-chat/references/chat.md index d9c2844c..e0677885 100644 --- a/skills/multi/dingtalk-chat/references/chat.md +++ b/skills/multi/dingtalk-chat/references/chat.md @@ -1138,6 +1138,15 @@ dws chat bot create \ > 账号文件字段、启动、多机器人 `chatbotUserId` 协作等具体配置,以 connector 仓 README / `docs/DINGTALK_MANUAL_SETUP.md` 为准,本 skill 不复制其实现细节。 +**建联自检(不依赖完整 agent 运行时)**:用 `scripts/bot_stream_probe.js` 最小验证「建联 + 收→回」——起 DWClient 连接、订阅 `TOPIC_ROBOT`、收到 @机器人 消息后经 `sessionWebhook` 回 echo: + +``` +npm i dingtalk-stream +CID=<新clientId> SEC=<新secret> node scripts/bot_stream_probe.js +# 看到 "connect success" = 建联通过;再在钉钉把机器人拉进群 @它 一句, +# 看到 ">>> 收到消息" = 收通过;">>> 回复结果: 200" = 回通过(回复需先过 ③ 的 scope 审批) +``` + ##### ③ 审批开通 + 交叉验证(建号 ≠ 可用,最易踩) 建号成功 / 能换 `access_token` 只代表【应用凭据】生效;机器人真正收发还需 `qyapi_robot_sendmsg` 等 robot scope **审批开通**。`openclaw channels status` 显示 `connected` / Stream WS 连上**都不是** DingTalk 的能力回执,**不能当作"建联成功"**。唯一可靠判定是真实发消息探测: diff --git a/skills/multi/dingtalk-chat/scripts/bot_stream_probe.js b/skills/multi/dingtalk-chat/scripts/bot_stream_probe.js new file mode 100644 index 00000000..c3b7baed --- /dev/null +++ b/skills/multi/dingtalk-chat/scripts/bot_stream_probe.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node +/** + * bot_stream_probe.js — 钉钉机器人「收→回」最小验证脚本 + * + * 用 dingtalk-stream SDK 起 DWClient Stream 连接,订阅 TOPIC_ROBOT;收到 + * @机器人 的消息后:ack → 打印消息 → 通过 sessionWebhook 回一条 echo。 + * 用来验证 `dws chat bot create` 建出的机器人「建联 + 收→回」闭环, + * 不依赖完整 OpenClaw/Hermes 运行时(机制等同 connector 的 connection.ts)。 + * + * 用法: + * npm i dingtalk-stream + * CID= SEC= node bot_stream_probe.js + * 然后在钉钉里把该机器人拉进群并 @它 发一句话。 + * + * 退出码:0 正常运行/收到消息;1 连接失败;2 缺凭据。 + * + * 注意(建号 ≠ 可用):连接与收消息只需有效应用凭据;但「回复」需机器人已通过 + * qyapi_robot_sendmsg 审批,未通过时连接/收消息正常、回复会被钉钉拒绝。 + * 审批入口见钉钉报错里的 open-dev.dingtalk.com/appscope/apply?content=%23qyapi_robot_sendmsg + */ +const { DWClient, TOPIC_ROBOT } = require('dingtalk-stream'); + +const clientId = process.env.CID; +const clientSecret = process.env.SEC; +if (!clientId || !clientSecret) { + console.error('缺少凭据:请设置环境变量 CID= SEC='); + process.exit(2); +} + +const client = new DWClient({ clientId, clientSecret }); + +client.registerCallbackListener(TOPIC_ROBOT, async (res) => { + // 立即 ack,避免钉钉重复投递 + const messageId = res.headers && res.headers.messageId; + if (messageId) client.socketCallBackResponse(messageId, { success: true }); + + let data = {}; + try { data = JSON.parse(res.data); } catch (_) { /* ignore */ } + const text = ((data.text && data.text.content) || '').trim(); + console.log('>>> 收到消息:', { + sender: data.senderNick, + conversationType: data.conversationType, // 1=单聊 2=群聊 + text, + sessionWebhook: data.sessionWebhook ? '有' : '无', + }); + + // 通过 sessionWebhook 回一条 echo(最小「回」验证) + if (data.sessionWebhook) { + try { + const r = await fetch(data.sessionWebhook, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ msgtype: 'text', text: { content: `收到:${text || '(空)'}` } }), + }); + console.log('>>> 回复结果:', r.status, await r.text()); + } catch (e) { + console.error('>>> 回复失败:', (e && e.message) || e); + } + } +}); + +client.connect().then(() => { + console.log('Stream 已连接,订阅 TOPIC_ROBOT,等待 @机器人 消息… (Ctrl-C 退出)'); +}).catch((e) => { + console.error('连接失败:', (e && e.message) || e); + process.exit(1); +}); From d5dbcae36942d0e93f42f6c144503aba3a27db69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 4 Jun 2026 14:57:20 +0800 Subject: [PATCH 08/10] =?UTF-8?q?fix(chat):=20=E5=BB=BA=E5=8F=B7=20MCP=20?= =?UTF-8?q?=E7=AB=AF=E7=82=B9=E5=88=87=E5=88=B0=E5=91=BD=E5=90=8D=E5=88=AB?= =?UTF-8?q?=E5=90=8D=20/server/op-app?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit robotCreateEndpoint 从带 ?key= 的 hash 端点改为命名别名 https://pre-mcp-gw.dingtalk.com/server/op-app。无 key → 走会话 bearer 鉴权, 建出的机器人按当前登录身份归属(不再固定归 key 绑定的账号)。 注意:该端点为另一套寻址,需调用方 bearer 在已注册 op-app 的组织/身份下才暴露 create_dingtalk_robot;预发用 MCP 默认凭证登录暂报「未找到指定工具」,待正确身份/ 服务端注册后生效。transport 的 key-URL 兼容逻辑保留(此端点无 key 不触发)。 --- internal/app/direct_runtime.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/app/direct_runtime.go b/internal/app/direct_runtime.go index 74e53354..a09c5990 100644 --- a/internal/app/direct_runtime.go +++ b/internal/app/direct_runtime.go @@ -50,13 +50,13 @@ const ( // Hardcoded built-in endpoint for the DingTalk open-platform app-management MCP // server, which hosts the robot-provisioning tool create_dingtalk_robot. This is // wired by source (NOT service discovery) per product decision: the helper -// command `dws chat bot create` routes CanonicalProduct "opendev" here. The full -// URL — host, server hash and access key — is pinned literally so the call always -// reaches the (currently pre-only) gateway regardless of ~/.dws/mcp_url. Update -// the URL when the production server ships. +// command `dws chat bot create` routes CanonicalProduct "opendev" here. It is a +// named gateway alias (no ?key=), so the call is authenticated by the caller's +// session bearer token — the created robot is owned by the current login. Update +// the host to the production gateway when the prod server ships. const ( robotCreateProductID = "opendev" - robotCreateEndpoint = "https://pre-mcp-gw.dingtalk.com/server/d0d388844aa5537c4cc8d749e01ffa13909d4fecb70bc576995beb763253eeed?key=73dd09d84e42543474d0cfac20339f17" + robotCreateEndpoint = "https://pre-mcp-gw.dingtalk.com/server/op-app" ) func defaultPATServerDescriptor() market.ServerDescriptor { From c6d95aeaf2517e060caf4f0f6fb8a085f8d554eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Thu, 4 Jun 2026 20:04:03 +0800 Subject: [PATCH 09/10] =?UTF-8?q?fix(chat):=20bot=20create=20=E6=94=B9?= =?UTF-8?q?=E5=BC=82=E6=AD=A5=20submit+=E8=BD=AE=E8=AF=A2=EF=BC=8C?= =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E4=B8=A4=E5=B1=82=E5=A3=B3=E8=A7=A3=E5=8C=85?= =?UTF-8?q?=E8=87=B4=E8=BD=AE=E8=AF=A2=E7=A9=BA=E8=BD=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_dingtalk_robot(同步一把梭) → submit_robot_create_task + query_robot_create_result(异步) - 阻塞轮询到 SUCCESS/FAIL/EXPIRED;失败带 taskId,新增 --task-id 重试避免重复建号 - 修复 bug:服务端返回是 content→result 两层壳,原只解一层导致 taskId 读空、轮询空转 (已端到端验证:真建出机器人并起 Stream 建联 connect success) - 端点保持 op-app 命名别名(按登录态归属,不写死 ?key= 密钥) - 补轮询单测,mock 还原真实嵌套壳,负向验证能兜住该回归 --- internal/app/direct_runtime.go | 3 +- internal/helpers/chat.go | 188 +++++++++++++++++++++-- internal/helpers/chat_bot_create_test.go | 182 ++++++++++++++++++++++ 3 files changed, 361 insertions(+), 12 deletions(-) create mode 100644 internal/helpers/chat_bot_create_test.go diff --git a/internal/app/direct_runtime.go b/internal/app/direct_runtime.go index a09c5990..54488001 100644 --- a/internal/app/direct_runtime.go +++ b/internal/app/direct_runtime.go @@ -48,7 +48,8 @@ const ( ) // Hardcoded built-in endpoint for the DingTalk open-platform app-management MCP -// server, which hosts the robot-provisioning tool create_dingtalk_robot. This is +// server, which hosts the async robot-provisioning tools submit_robot_create_task +// and query_robot_create_result. This is // wired by source (NOT service discovery) per product decision: the helper // command `dws chat bot create` routes CanonicalProduct "opendev" here. It is a // named gateway alias (no ?key=), so the call is authenticated by the caller's diff --git a/internal/helpers/chat.go b/internal/helpers/chat.go index c3c404c4..c4e66b75 100644 --- a/internal/helpers/chat.go +++ b/internal/helpers/chat.go @@ -16,7 +16,9 @@ package helpers import ( "context" "encoding/json" + "fmt" "strings" + "time" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cli" "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/cobracmd" @@ -141,35 +143,195 @@ func botInvoke(runner executor.Runner, cmd *cobra.Command, tool string, params m return writeCommandPayload(cmd, result) } -// robotCreateInvoke routes robot-provisioning tools to the open-platform +// Robot provisioning is a two-step async flow on the open-platform +// app-management MCP server: submit_robot_create_task returns a taskId, then +// query_robot_create_result is polled until the task reaches a terminal state. +// The async pair (replacing the old one-shot create_dingtalk_robot) lets the +// server dedupe by taskId so a retry never creates a second robot. +const ( + robotCreateSubmitTool = "submit_robot_create_task" + robotCreateQueryTool = "query_robot_create_result" + + // Poll cadence guards: honor the server-provided interval but keep it sane. + robotCreatePollMinInterval = 1 * time.Second + robotCreatePollMaxInterval = 30 * time.Second + // Fallbacks when the submit response omits interval / expiresIn (seconds). + robotCreateDefaultInterval = 3 * time.Second + robotCreateDefaultDeadline = 5 * time.Minute +) + +// runRobotCreateTool routes a robot-provisioning tool to the open-platform // app-management MCP server via CanonicalProduct "opendev". That product's // endpoint is hardcoded in internal/app/direct_runtime.go (NOT resolved from // service discovery), so this command works without any discovery/overlay entry. -func robotCreateInvoke(runner executor.Runner, cmd *cobra.Command, tool string, params map[string]any) error { +func runRobotCreateTool(runner executor.Runner, cmd *cobra.Command, tool string, params map[string]any, dryRun bool) (executor.Result, error) { invocation := executor.NewHelperInvocation( cobracmd.LegacyCommandPath(cmd), "opendev", tool, params, ) - invocation.DryRun = commandDryRun(cmd) - result, err := runner.Run(cmd.Context(), invocation) + invocation.DryRun = dryRun + return runner.Run(cmd.Context(), invocation) +} + +// robotCreateProvision submits an async robot-create task and polls for its +// result until SUCCESS / FAIL / EXPIRED (or the deadline). On success it writes +// the full query payload (agentId / robotCode / clientId / clientSecret). On +// FAIL / EXPIRED it returns an error carrying the taskId so the caller can retry +// with --task-id without creating a duplicate robot. +func robotCreateProvision(runner executor.Runner, cmd *cobra.Command, submitParams map[string]any) error { + dryRun := commandDryRun(cmd) + + submitRes, err := runRobotCreateTool(runner, cmd, robotCreateSubmitTool, submitParams, dryRun) if err != nil { return err } - return writeCommandPayload(cmd, result) + // Dry-run only previews the submit routing; there is no real taskId to poll. + if dryRun { + return writeCommandPayload(cmd, submitRes) + } + + submitPayload := robotCreatePayload(submitRes.Response) + taskID := robotResultString(submitPayload, "taskId") + if taskID == "" { + // Server returned an inline (already-terminal) result without a taskId; + // surface it verbatim rather than poll a task that does not exist. + return writeCommandPayload(cmd, submitRes) + } + + interval := robotResultDuration(submitPayload, "interval", robotCreateDefaultInterval) + if interval < robotCreatePollMinInterval { + interval = robotCreatePollMinInterval + } + if interval > robotCreatePollMaxInterval { + interval = robotCreatePollMaxInterval + } + deadline := robotResultDuration(submitPayload, "expiresIn", robotCreateDefaultDeadline) + + ctx := cmd.Context() + elapsed := time.Duration(0) + queryParams := map[string]any{"taskId": taskID} + for { + if err := robotCreateSleepFn(ctx, interval); err != nil { + return err + } + elapsed += interval + + queryRes, err := runRobotCreateTool(runner, cmd, robotCreateQueryTool, queryParams, false) + if err != nil { + return err + } + queryPayload := robotCreatePayload(queryRes.Response) + switch strings.ToUpper(robotResultString(queryPayload, "status")) { + case "SUCCESS": + return writeCommandPayload(cmd, queryRes) + case "FAIL", "EXPIRED": + status := strings.ToUpper(robotResultString(queryPayload, "status")) + return apperrors.NewInternal(fmt.Sprintf( + "robot creation %s (taskId=%s); retry with: dws chat bot create ... --task-id %s", + status, taskID, taskID)) + case "WAITING", "": + // keep polling + default: + // Unknown terminal-ish status: surface the raw payload. + return writeCommandPayload(cmd, queryRes) + } + + if elapsed >= deadline { + return apperrors.NewInternal(fmt.Sprintf( + "robot creation still WAITING after %s (taskId=%s); check later or retry with: dws chat bot create ... --task-id %s", + deadline, taskID, taskID)) + } + } +} + +// robotCreateSleepFn is the poll-wait function; overridable in tests so the +// polling loop can run without real delays. +var robotCreateSleepFn = robotCreateSleep + +// robotCreateSleep waits for d or until the context is cancelled. +func robotCreateSleep(ctx context.Context, d time.Duration) error { + timer := time.NewTimer(d) + defer timer.Stop() + select { + case <-ctx.Done(): + return ctx.Err() + case <-timer.C: + return nil + } +} + +// robotCreatePayload unwraps the executor/MCP envelope so callers can read +// taskId / status / agentId from the innermost object. The real shape is +// Response{"content":{"errorCode","errorMsg","success","result":{...}}}, so we +// descend through "content" and then "result", tolerating either wrapper being +// absent. +func robotCreatePayload(resp map[string]any) map[string]any { + cur := resp + if cur == nil { + return nil + } + if inner, ok := cur["content"].(map[string]any); ok { + cur = inner + } + if inner, ok := cur["result"].(map[string]any); ok { + cur = inner + } + return cur +} + +// robotResultString reads a string field from an MCP response map, tolerating +// nil maps and non-string scalars. +func robotResultString(resp map[string]any, key string) string { + if resp == nil { + return "" + } + switch v := resp[key].(type) { + case string: + return strings.TrimSpace(v) + case fmt.Stringer: + return strings.TrimSpace(v.String()) + default: + return "" + } +} + +// robotResultDuration reads a numeric field as a second-count duration, falling +// back to def when the field is missing or unparseable. +func robotResultDuration(resp map[string]any, key string, def time.Duration) time.Duration { + if resp == nil { + return def + } + switch v := resp[key].(type) { + case float64: + if v > 0 { + return time.Duration(v) * time.Second + } + case int: + if v > 0 { + return time.Duration(v) * time.Second + } + case json.Number: + if n, err := v.Float64(); err == nil && n > 0 { + return time.Duration(n) * time.Second + } + } + return def } // newChatBotCreateCommand creates `dws chat bot create`, the robot-provisioning -// command. It calls the create_dingtalk_robot MCP tool, which creates an -// enterprise self-built Agent app plus its hosting robot in one shot and returns -// agentId / robotCode / clientId / clientSecret (clientSecret is returned only -// once). corpId and userid are injected server-side from the current login. +// command. It submits an async robot-create task (submit_robot_create_task) and +// blocks while polling query_robot_create_result until the task reaches a +// terminal state, then returns agentId / robotCode / clientId / clientSecret +// (clientSecret is returned only once). corpId and userid are injected +// server-side from the current login. If creation FAILs / EXPIREs, re-run with +// --task-id to retry without creating a duplicate robot. func newChatBotCreateCommand(runner executor.Runner) *cobra.Command { cmd := &cobra.Command{ Use: "create", Short: "创建钉钉智能体机器人", - Long: "一次性创建企业自建 Agent 应用及承载机器人,返回 agentId / robotCode / clientId / clientSecret。⚠️ clientSecret 仅返回一次,请立即安全保存。corpId 和 userid 由 MCP 服务端按当前登录身份注入。", + Long: "创建企业自建 Agent 应用及承载机器人。服务端异步建号,本命令会阻塞轮询直到成功,返回 agentId / robotCode / clientId / clientSecret。⚠️ clientSecret 仅返回一次,请立即安全保存。corpId 和 userid 由 MCP 服务端按当前登录身份注入。建号失败时用 --task-id <上次返回的 taskId> 重试,可避免重复建号。", Example: " dws chat bot create --app-name \"销售助手\" --robot-name \"销售助手机器人\" --desc \"销售线索查询与客户跟进\"", Args: cobra.NoArgs, DisableAutoGenTag: true, @@ -197,7 +359,10 @@ func newChatBotCreateCommand(runner executor.Runner) *cobra.Command { if v, _ := cmd.Flags().GetString("preview-media-id"); strings.TrimSpace(v) != "" { params["previewMediaId"] = v } - return robotCreateInvoke(runner, cmd, "create_dingtalk_robot", params) + if v, _ := cmd.Flags().GetString("task-id"); strings.TrimSpace(v) != "" { + params["taskId"] = strings.TrimSpace(v) + } + return robotCreateProvision(runner, cmd, params) }, } preferLegacyLeaf(cmd) @@ -206,6 +371,7 @@ func newChatBotCreateCommand(runner executor.Runner) *cobra.Command { cmd.Flags().String("desc", "", "机器人功能描述,≤200 字 (必填)") cmd.Flags().String("robot-media-id", "", "机器人图标 mediaId(可选,留空用服务端默认图标)") cmd.Flags().String("preview-media-id", "", "机器人预览图 mediaId(可选,留空复用 --robot-media-id)") + cmd.Flags().String("task-id", "", "重试用:上次建号返回的 taskId,避免重复建号(可选)") return cmd } diff --git a/internal/helpers/chat_bot_create_test.go b/internal/helpers/chat_bot_create_test.go new file mode 100644 index 00000000..520e738d --- /dev/null +++ b/internal/helpers/chat_bot_create_test.go @@ -0,0 +1,182 @@ +// 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 helpers + +import ( + "bytes" + "context" + "strings" + "testing" + "time" + + "github.com/DingTalk-Real-AI/dingtalk-workspace-cli/internal/executor" +) + +// scriptedRunner returns a scripted Result per call so we can drive the +// submit → poll(query) loop deterministically. +type scriptedRunner struct { + calls []executor.Invocation + fn func(call int, inv executor.Invocation) executor.Result +} + +func (r *scriptedRunner) Run(_ context.Context, inv executor.Invocation) (executor.Result, error) { + idx := len(r.calls) + r.calls = append(r.calls, inv) + return r.fn(idx, inv), nil +} + +// mcpEnvelope reproduces the real server response shape the executor hands back: +// Response{"content":{"errorCode","errorMsg","success","result":{...}}}. Tests +// must use this (not a flat map) or they would miss the nested-unwrap bug. +func mcpEnvelope(result map[string]any) map[string]any { + return map[string]any{ + "endpoint": "https://pre-mcp-gw.dingtalk.com/server/op-app", + "content": map[string]any{ + "errorCode": nil, + "errorMsg": nil, + "success": true, + "result": result, + }, + } +} + +// instantSleep replaces the poll-wait with a no-op so tests run without delay. +func instantSleep(t *testing.T) { + t.Helper() + prev := robotCreateSleepFn + robotCreateSleepFn = func(context.Context, time.Duration) error { return nil } + t.Cleanup(func() { robotCreateSleepFn = prev }) +} + +func runBotCreate(t *testing.T, runner executor.Runner, args ...string) (string, error) { + t.Helper() + cmd := newChatBotCreateCommand(runner) + var out bytes.Buffer + cmd.SetOut(&out) + cmd.SetErr(&out) + cmd.SetArgs(args) + err := cmd.Execute() + return out.String(), err +} + +func TestBotCreatePollsUntilSuccess(t *testing.T) { + instantSleep(t) + + runner := &scriptedRunner{fn: func(call int, inv executor.Invocation) executor.Result { + switch call { + case 0: // submit + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{ + "taskId": "T1", "status": "WAITING", + "interval": float64(1), "expiresIn": float64(60), + })} + case 1: // first poll → still waiting + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{"status": "WAITING"})} + default: // second poll → success + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{ + "status": "SUCCESS", "agentId": "ag1", "robotCode": "rc1", + "clientId": "ci1", "clientSecret": "cs1", + })} + } + }} + + out, err := runBotCreate(t, runner, "--app-name", "a", "--robot-name", "b", "--desc", "c") + if err != nil { + t.Fatalf("Execute() error = %v\n%s", err, out) + } + // First call must be the async submit, routed to opendev. + if got := runner.calls[0].Tool; got != robotCreateSubmitTool { + t.Fatalf("first tool = %q, want %q", got, robotCreateSubmitTool) + } + if got := runner.calls[0].CanonicalProduct; got != "opendev" { + t.Fatalf("product = %q, want opendev", got) + } + // Subsequent calls poll query with the submitted taskId. + if got := runner.calls[1].Tool; got != robotCreateQueryTool { + t.Fatalf("poll tool = %q, want %q", got, robotCreateQueryTool) + } + if got := runner.calls[1].Params["taskId"]; got != "T1" { + t.Fatalf("poll taskId = %#v, want T1", got) + } + if len(runner.calls) != 3 { + t.Fatalf("calls = %d (submit + 2 polls expected)", len(runner.calls)) + } + for _, want := range []string{"SUCCESS", "agentId", "robotCode", "clientId", "clientSecret", "cs1"} { + if !strings.Contains(out, want) { + t.Fatalf("output missing %q:\n%s", want, out) + } + } +} + +func TestBotCreateFailSurfacesTaskID(t *testing.T) { + instantSleep(t) + + runner := &scriptedRunner{fn: func(call int, inv executor.Invocation) executor.Result { + if call == 0 { + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{ + "taskId": "T-FAIL", "interval": float64(1), "expiresIn": float64(60), + })} + } + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{"status": "FAIL"})} + }} + + _, err := runBotCreate(t, runner, "--app-name", "a", "--robot-name", "b", "--desc", "c") + if err == nil { + t.Fatal("expected error on FAIL, got nil") + } + if !strings.Contains(err.Error(), "T-FAIL") || !strings.Contains(err.Error(), "--task-id") { + t.Fatalf("error should carry taskId + retry hint, got: %v", err) + } +} + +func TestBotCreateDeadlineSurfacesTaskID(t *testing.T) { + instantSleep(t) + + runner := &scriptedRunner{fn: func(call int, inv executor.Invocation) executor.Result { + if call == 0 { + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{ + "taskId": "T-SLOW", "interval": float64(2), "expiresIn": float64(5), + })} + } + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{"status": "WAITING"})} + }} + + _, err := runBotCreate(t, runner, "--app-name", "a", "--robot-name", "b", "--desc", "c") + if err == nil { + t.Fatal("expected deadline error, got nil") + } + if !strings.Contains(err.Error(), "T-SLOW") { + t.Fatalf("deadline error should carry taskId, got: %v", err) + } +} + +func TestBotCreatePassesTaskIDOnRetry(t *testing.T) { + instantSleep(t) + + runner := &scriptedRunner{fn: func(call int, inv executor.Invocation) executor.Result { + if call == 0 { + return executor.Result{Invocation: inv, Response: mcpEnvelope(map[string]any{ + "status": "SUCCESS", "agentId": "ag", "robotCode": "rc", + })} + } + return executor.Result{Invocation: inv} + }} + + _, err := runBotCreate(t, runner, "--app-name", "a", "--robot-name", "b", "--desc", "c", "--task-id", "PRIOR-1") + if err != nil { + t.Fatalf("Execute() error = %v", err) + } + if got := runner.calls[0].Params["taskId"]; got != "PRIOR-1" { + t.Fatalf("submit taskId = %#v, want PRIOR-1", got) + } +} From 54588805b6e5debd0d5129d95fb886473ffaea98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=AE=E9=9B=A8?= <47820304+PeterGuy326@users.noreply.github.com> Date: Fri, 5 Jun 2026 14:05:59 +0800 Subject: [PATCH 10/10] =?UTF-8?q?fix(chat):=20bot=20create=20=E7=AB=AF?= =?UTF-8?q?=E7=82=B9=E5=88=87=E5=88=B0=E6=AD=A3=E5=BC=8F=E7=BD=91=E5=85=B3?= =?UTF-8?q?=20mcp-gw.dingtalk.com?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 robotCreateEndpoint 从预发 pre-mcp-gw.dingtalk.com/server/op-app 改为正式 mcp-gw.dingtalk.com/server/op-app,与默认网关 host 一致。 Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/app/direct_runtime.go | 6 +++--- internal/helpers/chat_bot_create_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/app/direct_runtime.go b/internal/app/direct_runtime.go index 54488001..153ec091 100644 --- a/internal/app/direct_runtime.go +++ b/internal/app/direct_runtime.go @@ -53,11 +53,11 @@ const ( // wired by source (NOT service discovery) per product decision: the helper // command `dws chat bot create` routes CanonicalProduct "opendev" here. It is a // named gateway alias (no ?key=), so the call is authenticated by the caller's -// session bearer token — the created robot is owned by the current login. Update -// the host to the production gateway when the prod server ships. +// session bearer token — the created robot is owned by the current login. Points +// at the production open-platform gateway. const ( robotCreateProductID = "opendev" - robotCreateEndpoint = "https://pre-mcp-gw.dingtalk.com/server/op-app" + robotCreateEndpoint = "https://mcp-gw.dingtalk.com/server/op-app" ) func defaultPATServerDescriptor() market.ServerDescriptor { diff --git a/internal/helpers/chat_bot_create_test.go b/internal/helpers/chat_bot_create_test.go index 520e738d..475ebb5f 100644 --- a/internal/helpers/chat_bot_create_test.go +++ b/internal/helpers/chat_bot_create_test.go @@ -41,7 +41,7 @@ func (r *scriptedRunner) Run(_ context.Context, inv executor.Invocation) (execut // must use this (not a flat map) or they would miss the nested-unwrap bug. func mcpEnvelope(result map[string]any) map[string]any { return map[string]any{ - "endpoint": "https://pre-mcp-gw.dingtalk.com/server/op-app", + "endpoint": "https://mcp-gw.dingtalk.com/server/op-app", "content": map[string]any{ "errorCode": nil, "errorMsg": nil,