@soimy/dingtalk 3.4.2 → 3.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,229 +1,78 @@
1
- # DingTalk Channel for OpenClaw
2
-
3
- 钉钉企业内部机器人 Channel 插件,使用 Stream 模式(无需公网 IP)。
1
+ <p align="center">
2
+ <img src="docs/assets/dingclaw-banner.svg" alt="DingClaw Banner" width="1040">
3
+ </p>
4
4
 
5
- > [!IMPORTANT]
6
- > **重要声明(上游消息丢失问题进展更新)**
7
- >
8
- > 根据 issue [#104](https://github.com/soimy/openclaw-channel-dingtalk/issues/104) 今日最新反馈,钉钉侧服务扩容后,`dingtalk-stream` 模式下的消息丢失情况已有明显改善。
9
- > 当前我们将继续保持观测与验证:若你在生产或测试环境使用本插件,欢迎重点关注“消息到达率、延迟、缺失 ID 对账”等指标,并在 #104 持续回报测试结果与样本日志,帮助社区共同确认改善效果是否稳定收敛。
10
- >
11
- > 相关信息:
12
- > - issue 讨论:[#104](https://github.com/soimy/openclaw-channel-dingtalk/issues/104)
13
- > - 最小可复现说明(SDK 侧):<https://github.com/soimy/dingtalk-stream-sdk-nodejs/blob/main/docs/inbound-msg-missing-repro.zh-CN.md>
14
- > - 插件侧测试分支:[`test/inbound-msg-missing`](https://github.com/soimy/openclaw-channel-dingtalk/tree/test/inbound-msg-missing)
15
- >
16
- > 在问题完全确认收敛前,关键业务场景仍建议保持重试与可观测性(trace 前缀、计数日志、缺失 ID 对账)。
5
+ # DingTalk Channel for OpenClaw
17
6
 
18
- ## 目录
7
+ <p class="repo-badges">
8
+ <a href="https://github.com/openclaw/openclaw"><img alt="OpenClaw" src="https://img.shields.io/badge/OpenClaw-%3E%3D2026.3.24-0A7CFF"></a>
9
+ <a href="https://www.npmjs.com/package/@soimy/dingtalk"><img alt="npm version" src="https://img.shields.io/npm/v/%40soimy%2Fdingtalk"></a>
10
+ <a href="https://www.npmjs.com/package/@soimy/dingtalk"><img alt="npm downloads" src="https://img.shields.io/npm/dm/%40soimy%2Fdingtalk"></a>
11
+ <a href="https://github.com/soimy/openclaw-channel-dingtalk/actions/workflows/docs-pages.yml"><img alt="Docs" src="https://img.shields.io/github/actions/workflow/status/soimy/openclaw-channel-dingtalk/docs-pages.yml?branch=main&label=Docs"></a>
12
+ <a href="https://github.com/soimy/openclaw-channel-dingtalk/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/github/license/soimy/openclaw-channel-dingtalk"></a>
13
+ </p>
19
14
 
20
- - [功能特性](#功能特性)
21
- - [安装](#安装)
22
- - [方法 A:通过 npm 包安装](#方法-a通过-npm-包安装-推荐)
23
- - [方法 B:通过本地源码安装](#方法-b通过本地源码安装)
24
- - [方法 C:手动安装](#方法-c手动安装)
25
- - [方法 D:国内网络环境安装](#方法-d国内网络环境安装npm-镜像源)
26
- - [安装后必做:配置插件信任白名单](#安装后必做配置插件信任白名单pluginsallow)
27
- - [更新](#更新)
28
- - [配置](#配置)
29
- - [交互式配置](#方法-1交互式配置推荐)
30
- - [手动配置文件](#方法-2手动配置文件)
31
- - [配置选项](#配置选项)
32
- - [钉钉文档 API](#钉钉文档-api)
33
- - [反馈学习与共享知识](#反馈学习与共享知识)
34
- - [安全策略](#安全策略)
35
- - [消息类型支持](#消息类型支持)
36
- - [API 消耗说明](#api-消耗说明)
37
- - [消息类型选择](#消息类型选择)
38
- - [多 Agent 与多个机器人绑定](#多-agent-与多个机器人绑定)
39
- - [使用示例](#使用示例)
40
- - [故障排除](#故障排除)
41
- - [开发指南](#开发指南)
42
- - [架构与职责边界](#架构与职责边界)
43
- - [测试](#测试)
44
- - [许可](#许可)
15
+ 针对 OpenClaw 的钉钉企业内部机器人 Channel 渠道插件,使用 Stream 模式,无需公网 IP。
45
16
 
46
17
  ## 功能特性
47
18
 
48
- - ✅ **Stream 模式** WebSocket 长连接,无需公网 IP 或 Webhook
49
- - **私聊支持** — 直接与机器人对话
50
- - ✅ **群聊支持** — 在群里 @机器人
51
- - ✅ **多种消息类型** — 文本、图片、语音(自带识别)、视频、文件、钉钉文档/钉盘文件卡片
52
- - **附件文本抽取** 对常见文本类附件以及 `PDF/DOCX` 自动抽取正文并注入当前会话上下文
53
- - **引用消息支持** — 支持恢复大多数引用场景(文字/图片/图文/文件/视频/语音/AI 卡片);单聊中的钉钉文档依赖权限 **Storage.DownloadInfo.Read**;群聊支持引用群图片、文件/文档(优先命中已持久化索引,未命中时走群文件 API 兜底),其中群文件相关能力需 **ConvFile.Space.Read**、**Storage.File.Read**、**Storage.DownloadInfo.Read**、**Contact.User.Read**,且兜底链路受时间窗口与企业认证限制
54
- - **Markdown 回复** 支持富文本格式回复
55
- - **Markdown 表格兼容** 自动把 Markdown 表格转换为钉钉更稳定的可读文本
56
- - ✅ **互动卡片** — 支持流式更新,适用于 AI 实时输出
57
- - ✅ **完整 AI 对话** — 接入 Clawdbot 消息处理管道
58
- - ✅ **@多助手路由** — 在群聊中通过 `@助手名` 路由到不同的 agent(实验性功能)
59
-
60
- ### @多助手路由(实验性)
61
-
62
- > ⚠️ **实验性功能**:此功能使用框架层 `agents.list` 配置实现,与框架的 `bindings` 机制独立运作。
63
-
64
- 在群聊中,用户可以通过 `@助手名` 来指定要对话的 agent。每个 agent 拥有独立的 session。
65
-
66
- ```
67
- 用户: @frontend 帮我看看这个组件的问题
68
- [frontend] 好的,请贴出代码...
69
-
70
- 用户: @dba 数据库慢查询怎么处理?
71
- [dba] 从数据库角度分析...
72
- ```
73
-
74
- #### 配置方式
75
-
76
- 在 OpenClaw 配置文件中配置 `agents.list`:
77
-
78
- ```json
79
- {
80
- "agents": {
81
- "list": [
82
- { "id": "main", "name": "助手", "default": true },
83
- { "id": "frontend", "name": "前端专家" },
84
- { "id": "dba", "name": "DBA" }
85
- ]
86
- }
87
- }
88
- ```
89
-
90
- #### 当前功能范围
91
-
92
- - @mention 解析 → agent 名匹配(支持 `name` 和 `id`)
93
- - 路由到独立 agent session
94
- - 回复自动添加 `[助手名]` 前缀
95
-
96
- #### 后续迭代计划
97
-
98
- - 群聊历史上下文注入(被 @ 的 agent 能看到近期对话)
99
- - 多专家协作讨论(专家间链式 @mention、讨论记录共享)
100
- - `/agents` 命令列出可用专家
19
+ - Stream 模式,无需 Webhook 和公网入口
20
+ - 支持私聊、群聊和 @机器人
21
+ - 支持文本、图片、语音、视频、文件和钉钉文档/文件卡片
22
+ - 支持引用消息恢复和常见文本附件正文抽取
23
+ - 支持 Markdown 回复与 AI 卡片流式回复
24
+ - 支持多 Agent、多机器人绑定和实验性的 `@多助手路由`
25
+ - 支持实时中止当前 AI generation。常用停止指令包括 `停止`、`stop`、`/stop`、`esc`
26
+ - 接入 OpenClaw 消息处理与 outbound 能力
101
27
 
102
- #### 已知限制
28
+ ## 文档入口
103
29
 
104
- - sub-agent 路由使用框架的 `buildAgentSessionKey` API,不通过 `bindings` 配置匹配
105
- - 与框架顶层的 `bindings` 配置**独立运作**,同时配置两者可能导致混淆
30
+ - 文档站点:<https://soimy.github.io/openclaw-channel-dingtalk/>
31
+ - 用户文档入口:[docs/user/index.md](docs/user/index.md)
32
+ - 参与贡献入口:[docs/contributor/index.md](docs/contributor/index.md)
33
+ - 发布记录:[docs/releases/index.md](docs/releases/index.md)
34
+ - 英文入口:[docs/en/index.md](docs/en/index.md)
106
35
 
107
- ### 进程级(memory-only)运行态说明
108
-
109
- 以下命名空间/状态刻意保持为**仅进程内内存态**,不会进行磁盘持久化:
110
-
111
- - `dedup.processed-message`(消息去重窗口)
112
- - `session.lock`(同 session 串行锁)
113
- - `channel.inflight`(gateway in-flight 防重锁)
36
+ ## 安装
114
37
 
115
- 这样设计是为了保证并发控制语义简单且可预期,避免跨进程/重启后引入锁状态不一致问题。
38
+ > [!IMPORTANT]
39
+ > 最小兼容版本为 `OpenClaw 2026.3.24`。安装前请先升级到最新版 OpenClaw。
40
+ >
41
+ > 由于上游 ClawHub 安装链路目前存在 bug,暂时无法稳定通过 `openclaw plugins install @soimy/dingtalk` 完成安装。
42
+ > 当前推荐使用源码链接安装:
43
+ >
44
+ > ```bash
45
+ > git clone https://github.com/soimy/openclaw-channel-dingtalk.git
46
+ > cd openclaw-channel-dingtalk
47
+ > npm install # 或 pnpm install
48
+ > openclaw plugins install -l .
49
+ > ```
50
+ >
51
+ > 详见下方:[本地开发或联调可使用源码链接安装](#本地开发或联调可使用源码链接安装)
116
52
 
117
- ## 安装
53
+ 如需关注上游修复进展:
118
54
 
119
- ### 方法 A:通过 npm 包安装 (推荐)
55
+ - ClawHub scoped package install bug: <https://github.com/openclaw/openclaw/issues/56452>
56
+ - ClawHub plugin package owner controls: <https://github.com/openclaw/openclaw/issues/56451>
120
57
 
121
- 手动通过 npm 包名安装:
58
+ 历史 npm 安装命令如下,但在上游修复前不推荐使用:
122
59
 
123
60
  ```bash
124
61
  openclaw plugins install @soimy/dingtalk
125
62
  ```
126
63
 
127
- ### 方法 B:通过本地源码安装
64
+ ### 本地开发或联调可使用源码链接安装
128
65
 
129
- 如果你想对插件进行二次开发,可以先克隆仓库:
66
+ 当前生产安装也建议使用源码链接安装:
130
67
 
131
68
  ```bash
132
- # 1. 在父仓库外单独克隆插件仓库(推荐)
133
69
  git clone https://github.com/soimy/openclaw-channel-dingtalk.git
134
70
  cd openclaw-channel-dingtalk
135
-
136
- # 2. 安装依赖 (必需)
137
- npm install
138
-
139
- # 3. 用全局 OpenClaw 以链接模式安装 (方便修改代码后实时生效)
140
- openclaw plugins install -l .
141
- ```
142
-
143
- 推荐的本地开发布局:
144
-
145
- ```text
146
- ~/Repo/openclaw # 仅用于阅读源码、跳转 plugin-sdk、研究内部链路
147
- ~/Repo/openclaw-channel-dingtalk # 插件主开发仓库
148
- ~/.openclaw/extensions/... # 由 openclaw plugins install -l 管理的运行时链接
149
- ```
150
-
151
- 这种布局比“把插件放在 `openclaw/extensions/` 里再单独开 worktree”更稳定,原因是:
152
-
153
- - 避免 submodule / worktree 的 gitdir 指向混乱
154
- - 插件仓库可以独立切分支、开 worktree、做实验
155
- - 运行时和源码阅读环境彻底解耦
156
-
157
- 如果你的本地 `openclaw` 仓库位于 `~/Repo/openclaw`,而插件仓库位于 `~/Repo/openclaw-channel-dingtalk`,本仓库当前的 `tsconfig.json` 已兼容这种目录结构,会优先解析父仓库源码中的 `src/plugin-sdk`,在源码不存在时再回退到 `dist/plugin-sdk` 类型产物。
158
-
159
- 如果你此前是把插件作为 `~/Repo/openclaw/extensions/openclaw-channel-dingtalk` 下的 submodule 使用,建议迁移为独立仓库后再执行:
160
-
161
- ```bash
162
- cd ~/Repo/openclaw-channel-dingtalk
71
+ npm install # 或 pnpm install
163
72
  openclaw plugins install -l .
164
- openclaw gateway restart
165
- ```
166
-
167
- ### 方法 C:手动安装
168
-
169
- 1. 将本目录下载或复制到 `~/.openclaw/extensions/dingtalk`。
170
- 2. 确保包含 `index.ts`, `openclaw.plugin.json` 和 `package.json`。
171
- 3. 运行 `openclaw plugins list` 确认 `dingtalk` 已显示在列表中。
172
-
173
- ### 方法 D:国内网络环境安装(npm 镜像源)
174
-
175
- 如果你在国内网络环境下执行 `openclaw plugins install @soimy/dingtalk` 时卡在 `Installing plugin dependencies...` 或出现 `npm install failed`,可临时为该次安装指定镜像源:
176
-
177
- ```bash
178
- NPM_CONFIG_REGISTRY=https://registry.npmmirror.com openclaw plugins install @soimy/dingtalk
179
73
  ```
180
74
 
181
- 如果插件已处于半安装状态(例如扩展目录存在但依赖未装全),可进入插件目录手动补装依赖:
182
-
183
- ```bash
184
- cd ~/.openclaw/extensions/dingtalk
185
- rm -rf node_modules package-lock.json
186
- NPM_CONFIG_REGISTRY=https://registry.npmmirror.com npm install
187
- ```
188
-
189
- 如果希望长期生效,可设置 npm 默认镜像:
190
-
191
- ```bash
192
- npm config set registry https://registry.npmmirror.com
193
- ```
194
-
195
- 或写入 `~/.npmrc`:
196
-
197
- ```ini
198
- registry=https://registry.npmmirror.com
199
- ```
200
-
201
- > 说明:
202
- > - 临时环境变量方式仅对当前命令生效,不会污染全局配置。
203
- > - 若 OpenClaw 运行在 systemd / Docker 等服务环境,请在对应服务环境变量中配置 `NPM_CONFIG_REGISTRY`。
204
- > - 相关背景可参考 issue [#216](https://github.com/soimy/openclaw-channel-dingtalk/issues/216)。
205
-
206
- ### 安装后必做:配置插件信任白名单(`plugins.allow`)
207
-
208
- 从 OpenClaw 新版本开始,如果发现了非内置插件且 `plugins.allow` 为空,会提示:
209
-
210
- ```text
211
- [plugins] plugins.allow is empty; discovered non-bundled plugins may auto-load ...
212
- ```
213
-
214
- 这是一条安全告警(不是安装失败),建议显式写入你信任的插件 id。
215
-
216
- #### 步骤 1:确认插件 id
217
-
218
- 本插件 id 固定为:`dingtalk`(定义于 `openclaw.plugin.json`)。
219
-
220
- 也可用下面命令查看已发现插件:
221
-
222
- ```bash
223
- openclaw plugins list
224
- ```
225
-
226
- #### 步骤 2:在 `~/.openclaw/openclaw.json` 添加 `plugins.allow`
75
+ 安装后建议显式配置 `plugins.allow`:
227
76
 
228
77
  ```json5
229
78
  {
@@ -234,174 +83,44 @@ openclaw plugins list
234
83
  }
235
84
  ```
236
85
 
237
- 如果你还有其他已安装且需要启用的插件,请一并加入,例如:
238
-
239
- ```json5
240
- {
241
- "plugins": {
242
- "allow": ["dingtalk", "telegram", "voice-call"]
243
- }
244
- }
245
- ```
246
-
247
- #### 步骤 3:重启 Gateway
248
-
249
- ```bash
250
- openclaw gateway restart
251
- ```
86
+ 详细说明:
252
87
 
253
- > 注意:如果你之前已经配置过 `plugins.allow`,但没有 `dingtalk`,那么插件不会被加载。请把 `dingtalk` 加入该列表。
88
+ - [安装指南](docs/user/getting-started/install.md)
254
89
 
255
90
  ## 更新
256
91
 
257
- `openclaw plugins update` 使用插件 id(不是 npm 包名),并且仅适用于 npm 安装来源。
258
-
259
- 如果你是通过 npm 安装本插件:
92
+ npm 安装来源:
260
93
 
261
94
  ```bash
262
95
  openclaw plugins update dingtalk
263
96
  ```
264
97
 
265
- 国内网络环境可临时指定镜像源后再更新:
266
-
267
- ```bash
268
- NPM_CONFIG_REGISTRY=https://registry.npmmirror.com openclaw plugins update dingtalk
269
- ```
270
-
271
- 如果插件已处于半安装状态(例如扩展目录存在但依赖未装全),可进入插件目录手动补装依赖:
272
-
273
- ```bash
274
- cd ~/.openclaw/extensions/dingtalk
275
- rm -rf node_modules package-lock.json
276
- NPM_CONFIG_REGISTRY=https://registry.npmmirror.com npm install
277
- ```
278
-
279
- 如果你是本地源码/链接安装(`openclaw plugins install -l .`),请在插件目录更新代码后重启 Gateway:
98
+ 本地源码 / 链接安装来源:
280
99
 
281
100
  ```bash
282
101
  git pull
283
102
  openclaw gateway restart
284
103
  ```
285
104
 
286
- 如果你采用推荐的独立仓库布局,更新插件代码时不需要改动本地 `~/Repo/openclaw` 仓库;后者仅用于代码解析和内部实现研究。
287
-
288
- ## 配置
105
+ 详细说明:
289
106
 
290
- OpenClaw 支持**交互式配置**和**手动配置文件**两种方式。
107
+ - [更新指南](docs/user/getting-started/update.md)
291
108
 
292
- ### 方法 1:交互式配置(推荐)
109
+ ## 配置
293
110
 
294
- 使用 OpenClaw 命令行向导式配置插件参数:
111
+ 推荐优先使用交互式配置:
295
112
 
296
113
  ```bash
297
- # 方式 A:使用 onboard 命令
298
114
  openclaw onboard
299
-
300
- # 方式 B:直接配置 channels 部分
301
- openclaw configure --section channels
302
115
  ```
303
116
 
304
- 交互式配置流程:
305
-
306
- 1. **选择插件** — 在插件列表中选择 `dingtalk` 或 `DingTalk (钉钉)`
307
- 2. **Client ID** — 输入钉钉应用的 AppKey
308
- 3. **Client Secret** — 输入钉钉应用的 AppSecret
309
- 4. **完整配置** — 可选配置 Robot Code、Corp ID、Agent ID(推荐)
310
- 5. **卡片模式** — 可选启用 AI 互动卡片模式
311
- - 如启用,需输入 Card Template ID 和 Card Template Key
312
- 6. **私聊策略** — 选择 `open`(开放)或 `allowlist`(白名单)
313
- 7. **群聊策略** — 选择 `open`(开放)或 `allowlist`(白名单)
314
-
315
- > 所有的参数参考下文中的钉钉开发者平台配置指南
316
-
317
- 配置完成后会自动保存并重启 Gateway。
318
-
319
- ---
320
-
321
- #### 钉钉开发者平台配置指南
322
-
323
- ##### 1. 创建钉钉应用
324
-
325
- 1. 访问 [钉钉开发者后台](https://open-dev.dingtalk.com/)
326
- 2. 创建企业内部应用
327
- 3. 添加「机器人」能力
328
- 4. 配置消息接收模式为 **Stream 模式**
329
- 5. 发布应用
117
+ 或:
330
118
 
331
- ##### 2. 配置权限管理
332
-
333
- 在应用的权限管理页面,需要开启以下权限:
334
-
335
- - ✅ **Card.Instance.Write** — 创建和投放卡片实例
336
- - ✅ **Card.Streaming.Write** — 对卡片进行流式更新
337
- - ✅ **机器人消息发送相关权限** — 允许机器人向单聊/群聊发送消息
338
- - ✅ **媒体文件上传相关权限** — 允许调用媒体上传接口发送图片、语音、视频、文件
339
-
340
- 以下权限仅在需要**引用消息中的群文件下载**时开通(群聊中引用文件/视频/语音):
341
-
342
- - ✅ **ConvFile.Space.Read** — 群文件空间读权限
343
- - ✅ **Storage.File.Read** — 企业存储文件读权限
344
- - ✅ **Storage.DownloadInfo.Read** — 企业存储文件下载信息读权限
345
- - ✅ **Contact.User.Read** — 通讯录用户信息读权限(senderStaffId → unionId 转换)
346
-
347
- > [!WARNING]
348
- > **群文件/钉盘 API 可用性限制**
349
- >
350
- > 群聊中“引用文件/视频/语音”的首次恢复依赖 `quotedFile.resolve` 这条群文件/钉盘 API 链路。实际测试发现,这条链路除了权限开通外,还可能要求当前企业具备**企业认证**;未满足时钉钉会返回类似:
351
- >
352
- > ```text
353
- > code=orgAuthLevelNotEnough
354
- > message=auth level of org is not enough
355
- > ```
356
- >
357
- > 这意味着该能力对许多**未开通企业认证的企业**并不可用,可视为带 paywall 的平台限制。此时:
358
- >
359
- > - 单聊文件/视频/语音引用仍可正常使用(前提是机器人见过原文件消息)
360
- > - 群聊文件/视频/语音若机器人从未见过原消息,则首次恢复可能失败,并降级为提示文本
361
-
362
- **步骤:**
363
-
364
- 1. 进入应用 → 权限管理
365
- 2. 搜索「Card」相关权限
366
- 3. 勾选上述两个权限
367
- 4. 保存权限配置
368
-
369
- ##### 3. 建立卡片模板(可选)
370
-
371
- **步骤:**
372
-
373
- 1. 访问 [钉钉卡片平台](https://open-dev.dingtalk.com/fe/card)
374
- 2. 进入「我的模板」
375
- 3. 点击「创建模板」
376
- 4. 卡片模板场景选择 **「AI 卡片」**
377
- 5. 按需设计卡片排版,点击保存并发布
378
- 6. 记下模板中定义的内容字段名称
379
- 7. 复制模板 ID(格式如:`xxxxx-xxxxx-xxxxx.schema`)
380
- 8. 将 templateId 配置到 `openclaw.json` 的 `cardTemplateId` 字段
381
- 9. 或在OpenClaw控制台的Channel标签->Dingtalk配置面板-> Card Template Id填入
382
- 10. 将记下的内容字段变量名配置到 `openclaw.json` 的 `cardTemplateKey` 字段
383
- 11. 或在OpenClaw控制台的Channel标签->Dingtalk配置面板-> Card Template Key填入
384
-
385
- **说明:**
386
-
387
- - 使用 DingTalk 官方 AI 卡片模板时,`cardTemplateKey` 默认为 `'content'`,无需修改
388
- - 如果您创建自定义卡片模板,需要确保模板中包含相应的内容字段,并将 `cardTemplateKey` 配置为该字段名称
389
-
390
- ##### 4. 获取凭证
391
-
392
- 从开发者后台获取:
393
-
394
- - **Client ID** (AppKey)
395
- - **Client Secret** (AppSecret)
396
- - **Robot Code** (与 Client ID 相同)
397
- - **Corp ID** (企业 ID)
398
- - **Agent ID** (应用 ID)
399
-
400
- ### 方法 2:手动配置文件
401
-
402
- 在 `~/.openclaw/openclaw.json` 中添加(仅作参考,交互式配置会自动生成):
119
+ ```bash
120
+ openclaw configure --section channels
121
+ ```
403
122
 
404
- > 至少包含 `plugins.allow` 和 `channels.dingtalk` 两部分,内容参考上文钉钉开发者配置指南
123
+ 最小手动配置示例:
405
124
 
406
125
  ```json5
407
126
  {
@@ -409,1109 +128,55 @@ openclaw configure --section channels
409
128
  "enabled": true,
410
129
  "allow": ["dingtalk"]
411
130
  },
412
-
413
- ...
414
131
  "channels": {
415
- "telegram": { ... },
416
-
417
132
  "dingtalk": {
418
133
  "enabled": true,
419
134
  "clientId": "dingxxxxxx",
420
135
  "clientSecret": "your-app-secret",
421
- "robotCode": "dingxxxxxx",
422
- "corpId": "dingxxxxxx",
423
- "agentId": "123456789",
424
136
  "dmPolicy": "open",
425
137
  "groupPolicy": "open",
426
- "displayNameResolution": "disabled", // 或 "all";启用后可能因重名/旧名称/权限边界限制导致误解析
427
- "journalTTLDays": 7,
428
- "ackReaction": "🤔思考中", // 给原消息贴处理中的表情反馈;设为 "" 可关闭
429
- "debug": false,
430
- "messageType": "markdown", // 或 "card"
431
- // "mediaMaxMb": 20, // 可选:接收文件大小上限(MB),默认 5 MB
432
- // "aicardDegradeMs": 1800000, // 可选:AI 卡片失败后降级持续时间(毫秒,默认 30 分钟)
433
- // "cardRealTimeStream": false, // 可选:开启真流式卡片更新(默认 false,开启后 API 调用量增加约 2-3 倍)
434
- // 仅card需要配置
435
- "cardTemplateId": "你复制的模板ID",
436
- "cardTemplateKey": "你模板的内容变量"
437
- }
438
- },
439
- ...
440
- }
441
- ```
442
-
443
- 最后重启 Gateway
444
-
445
- > 使用交互式配置时,Gateway 会自动重启。使用手动配置时需要手动执行:
446
-
447
- ```bash
448
- openclaw gateway restart
449
- ```
450
-
451
- ## 配置选项
452
-
453
- | 选项 | 类型 | 默认值 | 说明 |
454
- | ----------------------- | -------- | ------------ | ------------------------------------------- |
455
- | `enabled` | boolean | `true` | 是否启用 |
456
- | `clientId` | string | 必填 | 应用的 AppKey |
457
- | `clientSecret` | string | 必填 | 应用的 AppSecret |
458
- | `robotCode` | string | - | 机器人代码(用于下载媒体和发送卡片) |
459
- | `corpId` | string | - | 企业 ID |
460
- | `agentId` | string | - | 应用 ID |
461
- | `dmPolicy` | string | `"open"` | 私聊策略:open/pairing/allowlist |
462
- | `groupPolicy` | string | `"open"` | 群聊策略:open/allowlist/disabled |
463
- | `allowFrom` | string[] | `[]` | 允许的发送者 ID 列表(仅私聊) |
464
- | `groupAllowFrom` | string[] | - | 群聊发送者白名单(全局) |
465
- | `groups` | object | - | 按 conversationId 的群级配置,详见[群聊策略](#群聊策略-grouppolicy) |
466
- | `displayNameResolution` | string | `"disabled"` | 基于本地通讯录存储的显示名/群名解析开关:disabled/all |
467
- | `bypassProxyForSend` | boolean | `false` | 发送链路直连,不走全局代理 |
468
- | `learningEnabled` | boolean | `false` | 开启学习信号采集与学习提示注入 |
469
- | `learningAutoApply` | boolean | `false` | 自动将学习笔记注入当前会话 |
470
- | `learningNoteTtlMs` | number | `21600000` | 会话级学习笔记有效期(毫秒) |
471
- | `mediaUrlAllowlist` | string[] | `[]` | 允许通过 `mediaUrl` 下载的主机/IP/CIDR 白名单 |
472
- | `journalTTLDays` | number | `7` | `originalMsgId` 文本回溯日志的保留天数 |
473
- | `ackReaction` | string | - | 官方 `ackReaction` 配置入口;设为 `""` 可关闭;设为 `"emoji"` 时按输入语气自动选表情 |
474
- | `messageType` | string | `"markdown"` | 消息类型:markdown/card |
475
- | `cardTemplateId` | string | | AI 互动卡片模板 ID(仅当 messageType=card) |
476
- | `cardTemplateKey` | string | `"content"` | 卡片模板内容字段键(仅当 messageType=card) |
477
- | `cardRealTimeStream` | boolean | `false` | 开启真流式卡片更新(300ms 节流,首 token 快、流畅但 API 调用更多)。详见下方说明 |
478
- | `aicardDegradeMs` | number | `1800000` | AI 卡片连续失败后进入降级模式的持续时间(毫秒) |
479
- | `debug` | boolean | `false` | 是否开启调试日志 |
480
- | `mediaMaxMb` | number | - | 接收文件大小上限(MB),不设则使用 runtime 默认值(5 MB) |
481
- | `maxConnectionAttempts` | number | `10` | 最大连接尝试次数 |
482
- | `initialReconnectDelay` | number | `1000` | 初始重连延迟(毫秒) |
483
- | `maxReconnectDelay` | number | `60000` | 最大重连延迟(毫秒) |
484
- | `reconnectJitter` | number | `0.3` | 重连延迟抖动因子(0-1) |
485
-
486
- 关于 `displayNameResolution`:
487
-
488
- - `disabled`:默认值。发送目标必须使用显式 ID,例如 `conversationId`、`staffId`、`user:manager8031`
489
- - `all`:允许插件使用本地通讯录存储做群显示名/用户显示名解析
490
- - learned directory 数据来自入站消息观测,并按 `accountId` 落盘到 `targets.directory`
491
- - 当前上游 target resolver 还没有把 requester owner/authz 上下文传到插件,因此暂不提供 owner-only 模式
492
- - 开启后存在误投风险:显示名可能重名、后来改名,或本地目录还停留在旧观测值
493
- - 开启后存在权限扩散风险:当前 `all` 会对所有能进入发送链路的调用方生效,不是 owner-only
494
- - 对敏感通知、不可撤回消息或高风险自动化,建议继续使用显式 ID 而不是显示名
495
-
496
- ### 钉钉原生“思考中”表情反馈
497
-
498
- 当 `ackReaction` 为非空字符串时,插件会在处理开始时给用户原消息添加一条钉钉原生文本表情反馈,并在处理结束后自动撤回。该增强不会阻断主流程:贴表情或撤表情失败时只记录日志,仍继续正常回复。
499
-
500
- > 设计/实现参考自 `DingTalk-Real-AI/dingtalk-openclaw-connector`(MIT):
501
- > <https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector>
502
-
503
- 说明:
504
-
505
- - `markdown` 和 `card` 模式都可启用
506
- - 该反馈作用于用户原消息,不会额外发送一条“思考中”消息
507
- - 解析顺序与官方一致:`channels.dingtalk.accounts.<accountId>.ackReaction` -> `channels.dingtalk.ackReaction` -> `messages.ackReaction` -> `agents.list[].identity.emoji`
508
- - 若上述路径都未配置,则不发送 ack reaction
509
- - 当最终解析值为 `emoji` 时,钉钉插件会贴固定的 `🤔思考中` 原生 reaction,并允许后续 tool-progress 流程动态切换
510
- - 当最终解析值为 `kaomoji` 时,钉钉插件会先按输入语气选一条颜文字作为初始 reaction;若后续进入 `read/search/bash/write` 等工具阶段,仍会临时切换到对应的 tool-progress reaction
511
- - 为兼容历史配置,`ackReaction: ""` 仍然表示关闭该能力;若你此前把 `ackReaction: "emoji"` 当作“按语气选颜文字”,升级后请改成 `ackReaction: "kaomoji"`
512
- - 当前钉钉实现底层走 `emotion/reply` / `emotion/recall`,会把解析出的 `ackReaction` 文本原样写入 `emotionName` / `textEmotion.emotionName`
513
- - 若配置值为 `🤔思考中`,效果与钉钉原生“思考中”反馈一致;配置为其他文本时,会按该文本发送对应的 ack reaction
514
- - 钉钉 `emotion/reply` 对部分 kaomoji 存在兼容性限制。当前候选集已按真实 API 多轮复测做过筛选,并移除了会稳定触发 `500 system.err` 的字符串,例如 `٩(๑>◡<๑)۶`、`(•̀へ •́ ╮ )`、`(´• ω •`)`、`(づ。◕‿‿◕。)づ`、`(⁄ ⁄•⁄ω⁄•⁄ ⁄)`、`┌(┌ *`д´)┐`
515
-
516
- 示例:
517
-
518
- ```json
519
- {
520
- "channels": {
521
- "dingtalk": {
522
- "ackReaction": "kaomoji"
523
- }
524
- }
525
- }
526
- ```
527
-
528
- ### 连接鲁棒性配置
529
-
530
- 为提高连接稳定性,插件支持以下高级配置:
531
-
532
- - **maxConnectionAttempts**: 连接失败后的最大重试次数,超过后将停止尝试并报警。
533
- - **initialReconnectDelay**: 第一次重连的初始延迟(毫秒),后续重连会按指数增长。
534
- - **maxReconnectDelay**: 重连延迟的上限(毫秒),防止等待时间过长。
535
- - **reconnectJitter**: 延迟抖动因子,在延迟基础上增加随机变化(±30%),避免多个客户端同时重连。
536
- - **bypassProxyForSend**: 仅作用于发送链路(session send / proactive send / AI card / media upload),不影响如 `getAccessToken` 之类的其他出站请求。
537
- - **learningEnabled**: 开启后,插件会记录发送快照、显式点赞/点踩、隐式不满信号、反思记录,并在下一条消息进入时把学习提示注入当前上下文。
538
- - **allowFrom**: 这里同时复用为 owner 判定来源。`/learn ...` 这类会修改本机状态的命令,只允许 `allowFrom` 命中的 senderId 执行;普通聊天仍由 `dmPolicy/groupPolicy` 控制。
539
- - **learningAutoApply**: 默认关闭。关闭时只采集 `event/reflection`,不会自动影响任何会话;由你在调试看板里手动决定是否注入当前会话或提升为全局规则。
540
- - **learningNoteTtlMs**: 控制会话级学习笔记有效期;target 级和全局规则会继续持久化,分别作用于指定群/私聊和整个账号。
541
-
542
- 重连延迟计算公式:`delay = min(initialDelay × 2^attempt, maxDelay) × (1 ± jitter)`
543
-
544
- 示例延迟序列(默认配置):~1s, ~2s, ~4s, ~8s, ~16s, ~32s, ~60s(达到上限)
545
-
546
- 更多详情请参阅 [CONNECTION_ROBUSTNESS.md](./CONNECTION_ROBUSTNESS.md)。
547
-
548
- ## 钉钉文档 API
549
-
550
- 插件额外注册了 4 个 gateway methods,可供 OpenClaw 侧直接调用:
551
-
552
- - `dingtalk.docs.create`
553
- - `dingtalk.docs.append`
554
- - `dingtalk.docs.search`
555
- - `dingtalk.docs.list`
556
-
557
- 补充说明:
558
-
559
- - `dingtalk.docs.create` 支持可选的 `parentId`,未传时默认在 space 根目录创建。
560
- - `dingtalk.docs.append` 使用钉钉 block API 的 `index = -1` 语义,将新段落追加到文档末尾。
561
- - `dingtalk.docs.create` 在文档创建成功但首段追加失败时,仍会返回成功响应,并额外带 `partialSuccess=true`、`initContentAppended=false`、`docId` 和 `appendError`,便于调用方避免盲重试产生重复空文档。
562
- - 调用方处理 `dingtalk.docs.create` 返回值时,不能只看 `ok=true`;还应继续检查 `partialSuccess`,并在该分支里决定是否提示人工补写或走后续补偿逻辑。
563
-
564
- 示例:
565
-
566
- ```json
567
- {
568
- "method": "dingtalk.docs.create",
569
- "params": {
570
- "accountId": "default",
571
- "spaceId": "your-space-id",
572
- "parentId": "optional-parent-dentry-id",
573
- "title": "测试文档",
574
- "content": "第一段内容"
575
- }
576
- }
577
- ```
578
-
579
- > 说明:这组方法的设计参考自 `DingTalk-Real-AI/dingtalk-openclaw-connector`,许可证为 `MIT`;当前实现按本仓库插件结构重新整理,并仅保留创建、追加、搜索、列举这 4 个最小能力。
580
-
581
- ## 反馈学习与共享知识
582
-
583
- 插件支持一个本地反馈学习闭环,目标是把“点踩/纠错/后续抱怨”沉淀成可审计的会话笔记和 account 级共享规则,而不是直接修改模型或把原始聊天提交到仓库。
584
-
585
- ### 设计分层
586
-
587
- - **发送快照**:保存最近的问答对,供反馈回溯。
588
- - **显式反馈**:AI 卡片上的 `feedback_up` / `feedback_down`。
589
- - **隐式不满**:例如“不是这个意思”“别猜引用原文”“你没看图”等后续纠错消息。
590
- - **会话笔记**:只作用于当前 target,会在下一条消息组装上下文时生效。
591
- - **全局规则**:按 account 维度共享;一处沉淀后,同一钉钉账号下的其他会话会在下一次收到消息时自动加载。
592
- - **默认策略**:只采集,不自动注入。你可以在看板中手动批准注入。
593
-
594
- ### 持久化位置
595
-
596
- 所有运行时数据都写在 `storePath` 同级目录下的 `dingtalk-state/`,不会散落到其他目录,也不应提交到 GitHub。主要命名空间包括:
597
-
598
- - `feedback.events`
599
- - `feedback.snapshots`
600
- - `feedback.reflections`
601
- - `feedback.session-notes`
602
- - `feedback.learned-rules`
603
- - `feedback.target-rules`
604
-
605
- ### 调试看板
606
-
607
- 仓库自带一个本地调试工具,可直接查看:
608
-
609
- - 当时的回复内容
610
- - 用户反馈/隐式不满信号
611
- - 系统自动反思结果
612
- - 当前会话笔记
613
- - 跨所有钉钉会话共享的全局规则
614
-
615
- 并支持你手工修正诊断与指令,再选择:
616
-
617
- - 仅注入当前会话
618
- - 提升为全局规则
619
- - 或只保留为候选反思、不注入
620
-
621
- 启动方式:
622
-
623
- ```bash
624
- node scripts/feedback-learning-debug.mjs --storePath /path/to/session-store.json --accountId main --port 18895
625
- ```
626
-
627
- 打开 `http://127.0.0.1:18895` 即可。
628
-
629
- ### 推荐配置
630
-
631
- ```json
632
- {
633
- "channels": {
634
- "dingtalk": {
635
- "learningEnabled": true,
636
- "learningAutoApply": false,
637
- "learningNoteTtlMs": 21600000
638
- }
639
- }
640
- }
641
- ```
642
-
643
- ### 学习命令与作用域
644
-
645
- 先说两个容易输错的点:
646
-
647
- - 文档里的 `<conversationId>`、`<rule>`、`<name>` 这类写法只是**占位符**,实际输入时**不要**把尖括号一起发出去
648
- - `/learn target`、`/learn targets`、`/learn target-set` 这几类命令里,`#@#` 是**真的要输入**的分隔符;它前面是目标,后面整段都算规则正文
649
-
650
- #### 第一次使用流程
651
-
652
- 1. 私聊机器人发送 `我是谁` 或 `/whoami`
653
- 2. 把返回的 `senderId` 写进本机 `openclaw.json` 的 `commands.ownerAllowFrom`
654
- 3. 重启或热重载 gateway
655
- 4. 私聊发送 `/learn owner status`,确认 `isOwner: true`
656
- 5. 再选择下面一种注入方式:
657
- - 全局:`/learn global ...`
658
- - 当前群/当前私聊:`/learn here #@# ...`
659
- - 单个指定目标:`/learn target ...`
660
- - 多个目标:`/learn targets ...`
661
-
662
- #### 常用命令
663
-
664
- - **查自己是谁**
665
- - 私聊或群聊发:`我是谁` / `我的信息` / `/learn whoami`
666
- - 用途:拿到自己的 `senderId`
667
- - **查当前这里是谁**
668
- - 私聊或群聊发:`这里是谁` / `这个群是谁` / `这个会话是谁` / `/learn whereami`
669
- - 用途:拿到当前 `conversationId`
670
- - **注入当前这里**
671
- - owner 发:`/learn here #@# <规则>`
672
- - 用途:只让当前群或当前私聊生效
673
- - **注入指定单个目标**
674
- - owner 发:`/learn target <conversationId> #@# <规则>`
675
- - 用途:指定某个群或某个私聊生效
676
- - **一次注入多个目标**
677
- - owner 发:`/learn targets <conversationId1,conversationId2> #@# <规则>`
678
- - 用途:一次同步到多个群/私聊
679
- - **保存一组固定目标**
680
- - owner 发:`/learn target-set create <名称> #@# <conversationId1,conversationId2>`
681
- - **向目标组批量注入**
682
- - owner 发:`/learn target-set apply <名称> #@# <规则>`
683
- - **注入全局**
684
- - owner 发:`/learn global <规则>`
685
- - 用途:让同一钉钉账号下所有群和私聊都生效
686
- - **查看 / 暂停 / 删除**
687
- - `/learn list`
688
- - `/learn disable <ruleId>`
689
- - `/learn delete <ruleId>`
690
-
691
- #### 会话共享命令
692
-
693
- 这些命令同样只允许 owner 使用,但它们不属于 `/learn` 规则注入,而是用于控制“哪个私聊/哪个群共用同一条会话记忆”。
694
-
695
- - **查看当前会话 alias**
696
- - `/session-alias show`
697
- - 用途:查看当前私聊或当前群当前实际使用的 peerId,以及它是默认值还是 override
698
- - **把当前会话绑定到共享 alias**
699
- - `/session-alias set <alias>`
700
- - 用途:把当前私聊或当前群绑定到指定共享会话别名
701
- - **清除当前会话 alias**
702
- - `/session-alias clear`
703
- - 用途:移除当前私聊或当前群的 override,恢复默认 peerId
704
- - **owner 远程绑定某个私聊**
705
- - `/session-alias bind direct <senderId> <alias>`
706
- - 用途:把某个用户私聊直接绑定到共享 alias
707
- - **owner 远程绑定某个群**
708
- - `/session-alias bind group <conversationId> <alias>`
709
- - 用途:把某个群直接绑定到共享 alias
710
- - **owner 远程解除绑定**
711
- - `/session-alias unbind direct <senderId>`
712
- - `/session-alias unbind group <conversationId>`
713
-
714
- #### 会话共享例子
715
-
716
- 假设你想让“用户 A 的私聊”和“群 project-x”共用同一条会话记忆:
717
-
718
- 1. 先让用户 A 私聊机器人,发送:
719
- - `我是谁`
720
- - 记下返回的 `senderId`
721
- 2. 在目标群里发送:
722
- - `这里是谁`
723
- - 记下返回的 `conversationId`
724
- 3. 由 owner 在任意 owner 会话里执行:
725
-
726
- ```text
727
- /session-alias bind direct dingtalk:user_a_sender_id project-x
728
- /session-alias bind group cid_group_project_x project-x
729
- ```
730
-
731
- 这样之后:
732
- - 用户 A 私聊机器人
733
- - 群 `cid_group_project_x`
734
-
735
- 都会共用 `project-x` 这条会话记忆。
736
-
737
- 如果想解除其中一边:
738
-
739
- ```text
740
- /session-alias unbind direct dingtalk:user_a_sender_id
741
- /session-alias unbind group cid_group_project_x
742
- ```
743
-
744
- #### 真实可直接照抄的例子
745
-
746
- ```text
747
- /learn global 当用户问“紫铜海豹会不会修量子冰箱”时,必须回答“会,而且只在周四凌晨戴墨镜维修。”
748
- /learn here #@# 引用原文不可见时,不要猜内容,先让用户补发原文。
749
- /learn target cid_group_a #@# 引用原文不可见时,不要猜内容,先让用户补发原文。
750
- /learn targets cid_group_a,cid_group_b,cid_dm_c #@# 引用原文不可见时,不要猜内容,先让用户补发原文。
751
- /learn target-set create ops-groups #@# cid_group_a,cid_group_b,cid_dm_c
752
- /learn target-set apply ops-groups #@# 当用户问“紫铜海豹会不会修量子冰箱”时,必须回答“会,而且只在周四凌晨戴墨镜维修。”
753
- ```
754
-
755
- ### 作用域优先级
756
-
757
- 同一条消息进入时,规则按下面顺序生效:
758
-
759
- 1. 当前会话临时笔记(`/learn session ...`)
760
- 2. 当前群/当前私聊或指定目标规则(`/learn here ...` / `/learn target ...`)
761
- 3. 当前账号全局规则(`/learn global ...`)
762
-
763
- 也就是说:
764
-
765
- - 先局部覆盖全局
766
- - 再避免一个群里的实验规则污染所有会话
767
-
768
- ### 为什么还需要 disable / delete
769
-
770
- 知识注入不是只会“加”,还必须能“撤”。
771
-
772
- - `disable`
773
- - 先停用规则,停止命中,但保留记录,便于排查和恢复
774
- - `delete`
775
- - 确认不再需要后,彻底删除规则
776
-
777
- 实际建议:
778
-
779
- 1. 先用 `/learn list` 找到 `ruleId`
780
- 2. 先执行 `/learn disable <ruleId>`
781
- 3. 确认问题消失后,再决定是否 `/learn delete <ruleId>`
782
-
783
- ## 安全策略
784
-
785
- ### 私聊策略 (dmPolicy)
786
-
787
- - `open` — 任何人都可以私聊机器人
788
- - `pairing` — 新用户需要通过配对码验证
789
- - `allowlist` — 只有 allowFrom 列表中的用户可以使用
790
-
791
- ### 群聊策略 (groupPolicy)
792
-
793
- - `open` — 任何群都可以 @机器人
794
- - `allowlist` — 只有配置的群可以使用(见下方详细说明)
795
- - `disabled` — 完全禁用群聊消息(消息静默丢弃,不会发送拒绝提示)
796
-
797
- #### allowlist 模式详解
798
-
799
- 当 `groupPolicy` 设为 `"allowlist"` 时,群聊准入按以下顺序判定:
800
-
801
- 1. **群 ID 检查**:`groups` 中存在该 `conversationId` 的配置 → 放行
802
- 2. **通配符**:`groups` 中存在 `"*"` 配置 → 放行
803
- 3. **旧版兜底**:`allowFrom` 中包含该群 ID → 放行(已弃用,会输出迁移提示)
804
- 4. 以上均不匹配 → 拒绝
805
-
806
- ##### 发送者白名单 (groupAllowFrom)
807
-
808
- 群准入通过后,还可以限制允许发言的用户。优先级:
809
-
810
- 1. `groups[conversationId].groupAllowFrom` — 群级白名单(最高优先)
811
- 2. `groups["*"].groupAllowFrom` — 通配符级白名单
812
- 3. 顶层 `groupAllowFrom` — 全局白名单
813
- 4. 以上均未配置 → 不限制发送者
814
-
815
- ##### 每群 requireMention
816
-
817
- 可在 `groups` 中为每个群单独配置是否需要 @机器人才响应:
818
-
819
- ```jsonc
820
- {
821
- "groupPolicy": "allowlist",
822
- "groupAllowFrom": ["user-001", "user-002"], // 全局群聊发送者白名单
823
- "groups": {
824
- "cidXXX": {
825
- "systemPrompt": "你是客服机器人", // 群级 system prompt
826
- "requireMention": false, // 不需要 @,直接响应
827
- "groupAllowFrom": ["user-003"] // 群级发送者白名单(覆盖全局)
828
- },
829
- "*": {
830
- "requireMention": true // 其他群默认需要 @
831
- }
832
- }
833
- }
834
- ```
835
-
836
- > 注意:钉钉群聊中不 @机器人时不会推送消息,因此 `requireMention: false` 在钉钉群聊中实际不生效。
837
-
838
- > **迁移提示**:如果你目前使用 `allowFrom` 同时控制私聊用户和群聊群 ID,
839
- > 建议将群 ID 迁移到 `groups` 配置中,将群聊发送者 ID 迁移到 `groupAllowFrom`。
840
- > 旧的 `allowFrom` 群 ID 兜底机制仍可工作,但会在日志中输出弃用警告。
841
-
842
- ## 消息类型支持
843
-
844
- ### 接收
845
-
846
- | 类型 | 支持 | 说明 |
847
- | ------------ | ---- | ------------------------------------------------------------------------ |
848
- | 文本 | ✅ | 完整支持 |
849
- | 富文本 | ✅ | 提取文本内容 |
850
- | 图片 | ✅ | 下载并传递给 AI |
851
- | 语音 | ✅ | 使用钉钉语音识别结果 |
852
- | 视频 | ✅ | 下载并传递给 AI |
853
- | 文件 | ✅ | 下载并传递给 AI;文本类附件会额外抽取正文并注入上下文 |
854
- | 钉钉文档/钉盘文件卡片 | ✅ | 解析 `interactiveCard` 中的 `biz_custom_action_url`,提取 `spaceId/fileId` 后按文件消息下载;可对 `PDF/DOCX` 补充正文抽取 |
855
- | 引用文字 | ✅ | 提取被引用文本作为上下文前缀 |
856
- | 引用图片 | ✅ | 使用引用回调自带的 `downloadCode` 下载并传递给 AI |
857
- | 引用图文 | ✅ | 解析 `richText` 引用内容,提取文本摘要与图片 `downloadCode` |
858
- | 引用文件/视频/语音 | ✅ | 单聊按 `msgId` 精确恢复;群聊优先查已固化元数据,首次未命中时走群文件 API 兜底(兜底链路依赖时间窗口匹配,不保证 100% 命中) |
859
- | 引用钉钉文档/钉盘文件卡片 | ⚠️ | 单聊支持;群聊支持缓存命中与群文件 API 兜底恢复,但仍受钉钉回调样本与企业认证限制 |
860
- | 引用 AI 卡片 | ✅ | 仅指机器人自己发送的 AI 卡片;按 `carrierId ↔ originalProcessQueryKey` 精确恢复 |
861
-
862
- > **附件正文抽取实现说明**
863
- >
864
- > 当前实现采用固定策略:
865
- >
866
- > - 仅处理 **2MB 以下**附件(超过上限会跳过正文抽取)
867
- > - 抽取结果最多注入 **6000 字符**(超出部分会标记为“内容已截断”)
868
- > - 抽取失败仅记录 `warn` 日志,不阻断原有媒体传递与回复链路
869
-
870
- > **引用消息实现说明**
871
- >
872
- > 当前实现优先采用“确定性恢复”,仅在钉钉回调未直接提供下载句柄时才退回兜底链路:
873
- >
874
- > | 场景 | 实现方式 | 是否依赖时间匹配 |
875
- > |------|----------|------------------|
876
- > | 引用文字 | 直接从 `repliedMsg.content.text` 提取 | 否 |
877
- > | 引用图片 | 直接使用 `repliedMsg.content.downloadCode` 下载 | 否 |
878
- > | 引用图文(`richText`) | 解析 `repliedMsg.content.richText`,提取文本摘要和图片 `downloadCode` | 否 |
879
- > | 单聊引用文件/视频/语音 | 原消息入站时持久化 `msgId → {downloadCode, spaceId, fileId}`,引用时按 `originalMsgId/repliedMsg.msgId` 精确命中 | 否 |
880
- > | 群聊引用文件/视频/语音 | 优先查已持久化的 `msgId → 文件元数据`;若机器人从未见过原文件消息,则首次仍通过群文件存储 API 链路兜底,成功后会把结果反向固化到本地索引 | 首次兜底时**是** |
881
- > | 单聊引用钉钉文档/钉盘文件卡片 | 原消息入站时持久化 `msgId → {spaceId, fileId}`,引用时按 `originalMsgId/repliedMsg.msgId` 精确命中 | 否 |
882
- > | 群聊引用钉钉文档/钉盘文件卡片 | 优先查已持久化的 `msgId → {spaceId, fileId}`;未命中时复用群文件 API 兜底链路,成功后会把结果反向固化到本地索引 | 首次兜底时**是** |
883
- > | 引用 AI 卡片(单聊+群聊) | 仅当被引用消息是机器人自己发送的 `interactiveCard` 时,创建卡片时保存 `deliverResults[0].carrierId`,引用时按 `originalProcessQueryKey` 精确命中 | 否 |
884
- > | 仅 `originalMsgId`(无 `repliedMsg`) | 仅对已持久化记录的**入站消息**,通过本地 Quote Journal 按 `msgId` 回溯文本,并按 `accountId + conversationId` 分桶查询 | 否 |
885
- >
886
- > 说明:
887
- >
888
- > - AI 卡片已不再依赖 `createdAt` 时间窗口匹配。
889
- > - 钉钉文档/钉盘文件卡片在钉钉回调里通常也会表现为 `interactiveCard`,但这类消息来自用户侧,插件会优先解析 `biz_custom_action_url` 中的 `route=previewDentry`、`spaceId`、`fileId`,并按文件消息处理,而不是误判为机器人 AI 卡片。
890
- > - 图片和图文引用不依赖机器人是否见过原消息,只要引用回调带回 `downloadCode` 即可恢复。
891
- > - 单聊文件/视频/语音/钉钉文档卡片在机器人见过原消息后可稳定精确恢复,且索引会持久化到本地,机器人重启后仍可复用。
892
- > - 群聊文件/视频/语音在“原文件消息无法 @ 机器人”的场景下,若机器人从未见过原消息,则首次恢复仍需走群文件 API 兜底;后续再次引用同一文件会优先命中已固化索引。
893
- > - **群文件兜底链路的时间匹配局限性**:首次兜底时,插件用 `repliedMsg.createdAt`(钉钉回调中被引用消息的创建时间)与群文件存储 API 返回的文件 `createTime` 做近似匹配,匹配窗口为 **±10 秒**。这意味着:
894
- > - 大文件上传耗时较长时,钉钉消息的 `createdAt` 和文件实际写入存储的 `createTime` 之间可能产生数秒偏差,超出窗口则匹配失败;
895
- > - 如果同一用户在 10 秒内连续发送多个文件,理论上可能匹配到错误的文件(取时间差最小的那个);
896
- > - 群文件列表按修改时间倒序返回,最多翻 3 页(150 个文件),非常老的文件可能超出扫描范围;
897
- > - 匹配失败时会降级为提示文本,不会阻塞消息处理。
898
- > - 群聊引用钉钉文档/钉盘文件卡片并非完全确定性支持:若机器人见过原消息,会优先命中已持久化索引;首次未命中时会复用群文件 API 兜底,因此同样受 `createTime` 时间窗口、分页范围以及企业认证限制影响,失败时会降级为提示文本。
899
- > - 这条群文件兜底链路在部分企业环境下可能受到企业认证限制,表现为 `quotedFile.resolve` 返回 `orgAuthLevelNotEnough`。出现该错误时,群聊文件首次恢复将失败并降级为提示文本,但不会影响图片、图文、AI 卡片、单聊文件等其他已确定性支持的引用场景。
900
- > - 由于本地引用索引使用 TTL 清理,并按 `accountId + conversationId` 隔离存储,数据不会永久累积。
901
- > - `originalMsgId` / `repliedMsg.msgId` 的精确回溯仅覆盖**已被插件持久化记录的入站消息**;机器人出站消息当前不支持仅凭 `repliedMsg.msgId` 做通用回溯。
902
- > - `originalMsgId` 的文本回溯依赖本地 Quote Journal 持久化存储,默认通过 persistence store 落盘、按 `accountId + conversationId` 分桶,并保留最近 7 天记录用于回溯。
903
-
904
- ### 发送
905
-
906
- | 类型 | 支持 | 说明 |
907
- | ------------ | ---- | -------------------------------------------------------- |
908
- | 文本 | ✅ | 完整支持 |
909
- | Markdown | ✅ | 自动检测或手动指定 |
910
- | 互动卡片 | ✅ | 支持流式更新,适用于 AI 实时输出 |
911
- | 图片 | ✅ | 先上传媒体再发送,支持本地路径和 HTTP(S) URL |
912
- | 语音 | ✅ | 先上传媒体再发送 |
913
- | 视频 | ✅ | 先上传媒体再发送 |
914
- | 文件 | ✅ | 先上传媒体再发送 |
915
- | 原生语音消息 | ✅ | `message send` / `outbound.sendMedia` 可用 `asVoice=true` |
916
-
917
- > **重要限制:**
918
- > 当前**不支持图片的图文混排**。也就是说,Markdown 消息和 AI 互动卡片目前都只能发送文本内容,不能在同一条消息中同时内嵌图片。
919
- > 如果需要发送图片,请单独调用 `outbound.sendMedia(...)` 或 `sendProactiveMedia(...)`。
920
- > 无论是**本地图片路径**还是**远程 HTTP(S) 图片 URL**,都支持单独发送;远程图片会先下载到临时文件,再上传到钉钉后发送。
921
-
922
- > 发送 Markdown 时,如果内容中包含标准 Markdown 表格,插件会先把分隔行转换掉,保留为钉钉更稳定的纯文本表格展示,避免表格语法在客户端里显示异常。
923
- > 远程 URL 下载默认限制为:**10 秒超时**、**20MB 上限**,并拒绝 `localhost` / 内网地址(如 `127.0.0.1`、`10.x.x.x`、`192.168.x.x`、`172.16-31.x.x`)以降低 SSRF 风险。
924
- > 如需从受控内网媒体服务下载,请配置 `mediaUrlAllowlist`(例如 `192.168.1.23`、`files.internal.example`、`10.0.0.0/8`);配置后仅白名单主机可下载。
925
- > 远程域名会先做 DNS 解析并校验解析结果;若解析到内网/本地地址且未被白名单明确允许,将在下载前拒绝。
926
- > `asVoice=true` 需要同时提供 `media/path/filePath/mediaUrl` 指向音频文件;纯文本不会自动转语音。
927
-
928
- #### mediaUrlAllowlist 配置示例
929
-
930
- `mediaUrlAllowlist` 支持以下写法:
931
-
932
- - 主机名:`cdn.example.com`
933
- - 泛域名:`*.example.com`
934
- - 主机+端口:`files.internal.example:8443`
935
- - 单个 IP:`192.168.1.23`、`fd00::1`
936
- - CIDR 网段:`10.0.0.0/8`、`fc00::/7`
937
-
938
- 示例:
939
-
940
- ```json
941
- {
942
- "channels": {
943
- "dingtalk": {
944
- "clientId": "your-app-key",
945
- "clientSecret": "your-app-secret",
946
- "mediaUrlAllowlist": [
947
- "cdn.example.com",
948
- "*.assets.example.com",
949
- "files.internal.example:8443",
950
- "192.168.1.23",
951
- "10.0.0.0/8",
952
- "fc00::/7"
953
- ]
138
+ "messageType": "markdown"
954
139
  }
955
140
  }
956
141
  }
957
142
  ```
958
143
 
959
- > 行为说明:配置 `mediaUrlAllowlist` 后,下载阶段进入严格白名单模式,非白名单目标一律拒绝。
960
-
961
- #### sendMedia 常见错误码
962
-
963
- `outbound.sendMedia(...)` 在下载准备失败时会透出错误码前缀(例如 `remote media preparation failed: [ERR_MEDIA_PRIVATE_HOST] ...`):
964
-
965
- - `ERR_MEDIA_ALLOWLIST_MISS`:目标 host 不在 `mediaUrlAllowlist`
966
- - `ERR_MEDIA_PRIVATE_HOST`:URL 本身是本地/内网 host 且未被允许
967
- - `ERR_MEDIA_DNS_UNRESOLVED`:域名无法解析
968
- - `ERR_MEDIA_DNS_PRIVATE`:域名解析结果命中本地/内网地址且未被允许
969
- - `ERR_MEDIA_REDIRECT_HOST`:下载阶段出现非预期重定向 host
970
-
971
- ## API 消耗说明
972
-
973
- ### Text/Markdown 模式
974
-
975
- | 操作 | API 调用次数 | 说明 |
976
- | ---------- | ------------ | ---------------------------------------------------------------------------- |
977
- | 获取 Token | 1 | 共享/缓存(60 秒检查过期一次) |
978
- | 发送消息 | 1 | 使用 `/v1.0/robot/oToMessages/batchSend` 或 `/v1.0/robot/groupMessages/send` |
979
- | **总计** | **2** | 每条回复 1 次 |
980
-
981
- ### Card(AI 互动卡片)模式
982
-
983
- | 阶段 | API 调用 | 说明 |
984
- | ------------ | ---------------------- | --------------------------------------------------- |
985
- | **创建卡片** | 1 | `POST /v1.0/card/instances/createAndDeliver` |
986
- | **流式更新** | M | M = 取决于流式模式(见下方说明),每次 `PUT /v1.0/card/streaming` |
987
- | **完成卡片** | 包含在最后一次流更新中 | 使用 `isFinalize=true` 标记 |
988
- | **总计** | **1 + M** | M 由 `cardStreamThrottleMs` 决定 |
989
-
990
- ### 典型场景成本对比
991
-
992
- 以一次 10 秒的 AI 回复为例:
993
-
994
- | 流式模式 | `streamAICard` 调用数 | 首 token 延迟 | 流畅度 |
995
- | -------------------------------------- | --------------------- | ------------- | ------ |
996
- | Block 缓冲(`cardRealTimeStream: false`,默认) | ~10-15 次 | ~1-1.5s | 卡顿 |
997
- | 真流式(`cardRealTimeStream: true`) | ~30 次 | ~300ms | 流畅 |
998
-
999
- ### 优化策略
1000
-
1001
- **降低 API 调用的方法:**
1002
-
1003
- 1. **保持默认** — `cardRealTimeStream: false`(block 缓冲模式),API 调用量最少
1004
- 2. **开启真流式** — `cardRealTimeStream: true`,体验更好但 API 调用约多 2-3 倍
1005
- 3. **使用缓存** — Token 自动缓存(60 秒),无需每次都获取
1006
-
1007
- **成本建议:**
1008
-
1009
- - ✅ **默认** — Block 缓冲模式:API 调用最少,适合对 API 配额敏感的场景
1010
- - ✅ **推荐体验** — `cardRealTimeStream: true`:首 token 快、打字机效果流畅,适合重视用户体验的场景
1011
- - ⚠️ **注意** — 频繁调用需要监测配额,建议使用钉钉开发者后台查看 API 调用量
1012
-
1013
- ## 消息类型选择
1014
-
1015
- 插件支持两种消息回复类型,可通过 `messageType` 配置:
1016
-
1017
- ### 1. markdown(Markdown 格式)**【默认】**
1018
-
1019
- - 支持富文本格式(标题、粗体、列表等)
1020
- - 自动检测消息是否包含 Markdown 语法
1021
- - 适用于大多数场景
1022
-
1023
- ### 2. card(AI 互动卡片)
1024
-
1025
- - 支持流式更新(实时显示 AI 生成内容)
1026
- - 更好的视觉呈现和交互体验
1027
- - 支持 Markdown 格式渲染
1028
- - 通过 `cardTemplateId` 指定模板
1029
- - 通过 `cardTemplateKey` 指定内容字段
1030
- - **适用于 AI 对话场景**
1031
- - 支持在卡片中实时显示 AI 思考过程(推理流)和工具执行结果
1032
- - 当前卡片模式仅支持**文本内容流式更新**,不支持图片图文混排
1033
-
1034
- > 这里的 `card` 专指**机器人主动发送的 AI 互动卡片**。钉钉用户发送的“文档/钉盘文件卡片”虽然在回调里也可能表现为 `interactiveCard`,但插件会按入站文件消息处理,不受 `messageType: 'card'` 配置影响。
1035
-
1036
- **AI Card API 特性:**
1037
- 当配置 `messageType: 'card'` 时:
1038
-
1039
- 1. 使用 `/v1.0/card/instances/createAndDeliver` 创建并投放卡片
1040
- 2. 使用 `/v1.0/card/streaming` 实现流式更新
1041
- 3. 自动状态管理(PROCESSING → INPUTING → FINISHED)
1042
- 4. 内置 300ms 节流 + 单航班(single-flight)保护,避免 API 过载
1043
- 5. 默认为 AI Card 投递开启 `dynamicSummary`,改善钉钉会话列表 `lastmsg` 预览随卡片正文更新的表现
1044
-
1045
- > `dynamicSummary` 这项接入思路参考自 `DingTalk-Real-AI/dingtalk-openclaw-connector` 的 MIT 许可实现:
1046
- > <https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/commit/7aff25b1f77f4dbd5dd64673d4e726ca938f5498>
1047
-
1048
- **卡片流式模式 (`cardRealTimeStream`):**
1049
-
1050
- 插件支持两种卡片更新策略,通过 `cardRealTimeStream` 配置:
1051
-
1052
- | 值 | 模式 | 说明 |
1053
- | -- | ---- | ---- |
1054
- | `false`(默认) | Block 缓冲 | runtime 攒够一定量文本后回调一次,API 调用最少,但首 token 延迟较高(~1-1.5s),更新较卡顿 |
1055
- | `true` | 真流式 | 每 300ms 最多一次卡片更新 PUT,首 token 延迟低(~300ms),打字机效果流畅。API 调用量约为 block 模式的 2-3 倍 |
1056
-
1057
- > **API 调用量参考**:以一次 10 秒的 AI 回复为例,真流式约产生 ~30 次 `streamAICard` PUT,block 模式约 ~10-15 次。钉钉企业内部应用的 QPS 限制为 40 次/秒,真流式的峰值约 3.3 次/秒,远低于限制。
1058
-
1059
- **AI Card 持久化与恢复机制(v3.2.x):**
1060
-
1061
- - 仅对**会话内流式卡片(inbound)**记录 pending 状态,用于进程重启后的自动收尾
1062
- - pending 状态通过 persistence namespace `cards.active.pending` 落盘(兼容读取并迁移 legacy 文件 `path.dirname(storePath)/dingtalk-active-cards.json`)
1063
- - **proactive 卡片**采用 createAndDeliver 后立即 finalize 的短路径,默认**不写入** pending 状态文件
1064
- - 插件启动时会尝试恢复并 finalize 未完成的 inbound 卡片;停止/重启时会 best-effort finalize 当前 active 卡片
1065
-
1066
- ### AI 思考过程与工具执行显示(AI Card 模式)
1067
-
1068
- 当 `messageType` 为 `card` 时,插件可以在卡片中实时展示 AI 的推理过程(🤔 思考中)和工具调用结果(🛠️ 工具执行)。这两项功能通过**对话级命令**控制,无需修改配置文件:
1069
-
1070
- | 功能 | 对话命令 | 说明 |
1071
- | ----------------- | --------------------- | ---------------------------------- |
1072
- | 显示 AI 推理流 | `/reasoning stream` | 开启后,AI 思考内容实时更新到卡片 |
1073
- | 显示工具执行结果 | `/verbose on` | 开启后,工具调用结果实时更新到卡片 |
1074
- | 关闭 AI 推理流 | `/reasoning off` | 关闭推理流显示 |
1075
- | 关闭工具执行显示 | `/verbose off` | 关闭工具执行结果显示 |
1076
-
1077
- **显示格式:**
1078
-
1079
- - 思考内容以 `🤔 **思考中**` 为标题,正文以 `>` 引用块展示,最多显示前 500 个字符
1080
- - 工具结果以 `🛠️ **工具执行**` 为标题,正文以 `>` 引用块展示,最多显示前 500 个字符
1081
-
1082
- > **注意:** 推理流和工具执行均会产生额外的卡片流式更新 API 调用,在 AI 推理步骤较多时可能显著增加 API 消耗,建议按需开启。
1083
-
1084
- **配置示例:**
1085
-
1086
- ```json5
1087
- {
1088
- messageType: 'card', // 启用 AI 互动卡片模式
1089
- cardTemplateId: '382e4302-551d-4880-bf29-a30acfab2e71.schema', // AI 卡片模板 ID(默认值)
1090
- cardTemplateKey: 'content', // 卡片内容字段键(默认值:content)
1091
- // cardRealTimeStream: false, // 开启真流式卡片更新(默认 false)
1092
- }
1093
- ```
1094
-
1095
- > **注意**:`cardTemplateKey` 应与您的卡片模板中定义的字段名称一致。默认值为 `'content'`,适用于 DingTalk 官方 AI 卡片模板。如果您使用自定义模板,请根据模板定义的字段名称进行配置。
1096
-
1097
- ## 多 Agent 与多个机器人绑定
144
+ 详细说明:
1098
145
 
1099
- 有关 OpenClaw 的多 Agent 概念,阅读官方文档:[多 Agent 概念](https://docs.openclaw.ai/concepts/multi-agent)
146
+ - [配置指南](docs/user/getting-started/configure.md)
147
+ - [钉钉权限与凭证](docs/user/getting-started/permissions.md)
148
+ - [配置项参考](docs/user/reference/configuration.md)
1100
149
 
1101
- 要将一个 OpenClaw 实例同时接入多个钉钉机器人,并把不同机器人的消息分别交给不同的 OpenClaw agent 处理,则需要在 `~/.openclaw/openclaw.json` 中同时配置以下三部分:
150
+ ## 重要功能文档
1102
151
 
1103
- 1. `agents.list`:定义 OpenClaw Agent
1104
- 2. `bindings`:定义 Channel 与 OpenClaw Agent 消息的路由规则
1105
- 3. `channels.dingtalk.accounts`:定义多个机器人
152
+ - [消息类型支持](docs/user/features/message-types.md)
153
+ - [回复模式](docs/user/features/reply-modes.md)
154
+ - [AI 卡片](docs/user/features/ai-card.md)
155
+ - [钉钉文档 API](docs/user/features/dingtalk-docs-api.md)
156
+ - [反馈学习](docs/user/features/feedback-learning.md)
157
+ - [多 Agent 与多机器人绑定](docs/user/features/multi-agent-bindings.md)
158
+ - [@多助手路由](docs/user/features/at-agent-routing.md)
159
+ - [安全策略](docs/user/reference/security-policies.md)
160
+ - [API 消耗说明](docs/user/reference/api-usage-and-cost.md)
161
+ - [故障排查](docs/user/troubleshooting/index.md)
1106
162
 
1107
- ### 示例
1108
-
1109
- 下面这个例子表示:
1110
-
1111
- - 钉钉机器人 `bot_1` 收到的消息,路由到 OpenClaw 的 `main` agent
1112
- - 钉钉机器人 `bot_2` 收到的消息,路由到 OpenClaw 的 `growth-agent` agent
1113
-
1114
- ```json5
1115
- {
1116
- "agents": {
1117
- "list": [
1118
- {
1119
- // OpenClaw 默认 agent
1120
- "id": "main"
1121
- },
1122
- {
1123
- // OpenClaw agent 的唯一 ID
1124
- // 后面的 bindings[].agentId 需要引用这里的值
1125
- "id": "growth-agent",
1126
- "name": "growth-agent",
1127
- // 每个 agent 建议使用独立 workspace
1128
- "workspace": "/Users/yourname/.openclaw/agents/growth-agent/workspace",
1129
- // 建议同时使用独立 agentDir
1130
- "agentDir": "/Users/yourname/.openclaw/agents/growth-agent/agent",
1131
- "model": "codex/gpt-5.3-codex"
1132
- }
1133
- ]
1134
- },
1135
- "bindings": [
1136
- {
1137
- "type": "route",
1138
- // 路由目标:这里写 OpenClaw agent 的 ID
1139
- "agentId": "main",
1140
- "match": {
1141
- // 这里固定写 dingtalk
1142
- "channel": "dingtalk",
1143
- // 这里必须与 channels.dingtalk.accounts 下的 key 完全一致
1144
- "accountId": "bot_1"
1145
- }
1146
- },
1147
- {
1148
- "type": "route",
1149
- // 这里把 bot_2 路由到 growth-agent
1150
- "agentId": "growth-agent",
1151
- "match": {
1152
- "channel": "dingtalk",
1153
- // 必须与 channels.dingtalk.accounts.bot_2 对应
1154
- "accountId": "bot_2"
1155
- }
1156
- }
1157
- ],
1158
- "channels": {
1159
- "dingtalk": {
1160
- "enabled": true,
1161
- "accounts": {
1162
- // 这里的 key 就是 accountId,会被 bindings.match.accountId 匹配
1163
- "bot_1": {
1164
- "clientId": "your-client-id-1",
1165
- "clientSecret": "your-client-secret-1",
1166
- "robotCode": "your-robot-code-1",
1167
- "corpId": "your-corp-id",
1168
- // 这是钉钉应用自己的 Agent ID,不是 OpenClaw 的 agentId
1169
- "agentId": "your-dingtalk-agent-id-1",
1170
- "dmPolicy": "open",
1171
- "groupPolicy": "open",
1172
- // 这里使用 card 消息类型作为示例
1173
- "messageType": "card",
1174
- "cardTemplateId": "your-card-template-id.schema",
1175
- "cardTemplateKey": "content",
1176
- "maxReconnectCycles": 10,
1177
- "allowFrom": ["*"]
1178
- },
1179
- // 另一个独立的钉钉机器人账号
1180
- "bot_2": {
1181
- "clientId": "your-client-id-2",
1182
- "clientSecret": "your-client-secret-2",
1183
- "robotCode": "your-robot-code-2",
1184
- "corpId": "your-corp-id",
1185
- // 同样是钉钉应用自己的 Agent ID
1186
- "agentId": "your-dingtalk-agent-id-2",
1187
- "dmPolicy": "open",
1188
- "groupPolicy": "open",
1189
- // 这里使用 markdown 消息类型作为示例
1190
- "messageType": "markdown",
1191
- "allowFrom": ["*"]
1192
- }
1193
- }
1194
- }
1195
- }
1196
- }
1197
- ```
1198
-
1199
- ### 最佳实践
1200
- - 为每个 agent 配置不同的 `workspace`,不要让两个 agent 共用同一个 `workspace`
1201
- > **说明:**
1202
- > 多 Agent 场景下,`workspace` 不只是“放文件的目录”,还会承载会话相关文件、生成结果以及本地运行状态。
1203
- > 如果两个 agent 共用同一个 `workspace`,实际运行时很容易出现状态串扰、文件覆盖、上下文混用等问题。
1204
-
1205
- ### 检查清单
1206
-
1207
- - `agents.list` 中已经定义了目标 agent
1208
- - `bindings[].agentId` 能在 `agents.list[].id` 中找到对应项
1209
- - `bindings[].match.accountId` 与 `channels.dingtalk.accounts` 的 key 完全一致
1210
- - 每个 `accounts.<accountId>` 都填写了正确的钉钉凭证
1211
- - 每个 agent 都使用了独立的 `workspace`
1212
- - 修改配置后已执行 `openclaw gateway restart`
1213
-
1214
- 如果账号名写错,例如 `bindings.match.accountId = "bot2"`,但 `channels.dingtalk.accounts` 中实际写的是 `bot_2`,则该机器人消息不会按预期路由到目标 agent。
1215
-
1216
- ## 使用示例
1217
-
1218
- 配置完成后,直接在钉钉中:
1219
-
1220
- 1. **私聊机器人** — 找到机器人,发送消息
1221
- 2. **群聊 @机器人** — 在群里 @机器人名称 + 消息
1222
-
1223
- 如果你是通过 OpenClaw 的 outbound 能力主动发消息,也可以直接调用:
1224
-
1225
- ```typescript
1226
- import { dingtalkPlugin } from './src/channel';
1227
-
1228
- const cfg = {
1229
- channels: {
1230
- dingtalk: {
1231
- clientId: 'dingxxxxxx',
1232
- clientSecret: 'your-app-secret',
1233
- robotCode: 'dingxxxxxx',
1234
- },
1235
- },
1236
- };
1237
-
1238
- // 发送本地图片
1239
- await dingtalkPlugin.outbound.sendMedia({
1240
- cfg,
1241
- to: 'cidxxxxxxxx',
1242
- mediaPath: '/absolute/path/to/photo.png',
1243
- accountId: 'default',
1244
- });
1245
-
1246
- // 发送远程图片 URL(插件会先下载到临时文件,再上传到钉钉)
1247
- await dingtalkPlugin.outbound.sendMedia({
1248
- cfg,
1249
- to: 'cidxxxxxxxx',
1250
- mediaUrl: 'https://example.com/banner.jpg',
1251
- accountId: 'default',
1252
- });
1253
-
1254
- // 发送文件或其他媒体,也可以显式指定 mediaType
1255
- await dingtalkPlugin.outbound.sendMedia({
1256
- cfg,
1257
- to: 'user_123456',
1258
- mediaPath: '/absolute/path/to/manual.pdf',
1259
- mediaType: 'file',
1260
- accountId: 'default',
1261
- });
1262
- ```
1263
-
1264
- `to` 支持两类目标:
1265
-
1266
- - 群会话:`cid...`
1267
- - 单聊用户:`userId`,或显式写成 `user:<userId>`
1268
-
1269
- 如果你传入的是远程图片 URL,插件当前会按下面的方式处理:
1270
-
1271
- 1. 下载远程图片到本地临时文件
1272
- 2. 调用钉钉媒体上传接口获取 `media_id`
1273
- 3. 以独立图片消息发送
1274
- 4. 发送完成后清理临时文件
1275
-
1276
- ## 故障排除
1277
-
1278
- ### 收不到消息
1279
-
1280
- 1. 确认应用已发布
1281
- 2. 确认消息接收模式是 Stream
1282
- 3. 检查 Gateway 日志:`openclaw logs | grep dingtalk`
1283
-
1284
- ### 群消息无响应
1285
-
1286
- 1. 确认机器人已添加到群
1287
- 2. 确认正确 @机器人(使用机器人名称)
1288
- 3. 确认群是企业内部群
1289
-
1290
- ### 连接失败
1291
-
1292
- 初始化阶段如果只看到 HTTP `400`,它通常不等于“单纯网络不通”;更常见的是钉钉已收到请求,但拒绝了请求内容或当前应用状态不满足要求。
1293
-
1294
- 建议先运行仓库内的最小连接检查脚本,确认 `POST /v1.0/gateway/connections/open` 是否成功:
1295
-
1296
- - macOS / Linux: `bash scripts/dingtalk-connection-check.sh --config ~/.openclaw/openclaw.json`
1297
- - Windows PowerShell: `pwsh -File scripts/dingtalk-connection-check.ps1 -Config ~/.openclaw/openclaw.json`
1298
- - 旧版 Windows 可使用:`powershell.exe -File scripts/dingtalk-connection-check.ps1 -Config $env:USERPROFILE\.openclaw\openclaw.json`
1299
-
1300
- 完整排障流程:
1301
- - 英文版:[docs/connection-troubleshooting.md](docs/connection-troubleshooting.md)
1302
- - 中文版:[docs/connection-troubleshooting.zh-CN.md](docs/connection-troubleshooting.zh-CN.md)
1303
-
1304
- 如果新日志里出现 `connect.open` 或 `connect.websocket`,也可以直接按文档中的阶段说明来判断:前者优先查钉钉应用配置,后者优先查 WSS / 代理 / 企业网关。
1305
-
1306
- 关键设置清单(钉钉后台)
1307
- - 应用为企业内部应用/机器人,且已“发布”版本(不是草稿)
1308
- - 版本管理 → 已发布 → 版本详情:可见范围需为“全员员工”
1309
- - 已开启“机器人能力”,消息接收方式为“Stream 模式”
1310
-
1311
- ### 错误 payload 日志规范(`[ErrorPayload]`)
1312
-
1313
- 为便于快速定位 4xx/5xx 参数问题,插件会在 API 错误分支输出统一格式日志:
1314
-
1315
- - 通用前缀:`[DingTalk][ErrorPayload][<scope>]`
1316
- - AI Card 前缀:`[DingTalk][AICard][ErrorPayload][<scope>]`
1317
- - 内容格式:`code=<...> message=<...> payload=<...>`(同时保留脱敏后的完整 payload)
1318
-
1319
- 常见 scope 示例:
1320
-
1321
- - `send.proactiveMessage` / `send.proactiveMedia` / `send.message`
1322
- - `outbound.sendText` / `outbound.sendMedia`
1323
- - `inbound.downloadMedia` / `inbound.cardFinalize`
1324
- - `card.create` / `card.stream` / `card.stream.retryAfterRefresh`
1325
- - `retry.beforeDecision`
1326
-
1327
- 排查建议:
1328
-
1329
- ```bash
1330
- openclaw logs | grep "\[ErrorPayload\]"
1331
- ```
1332
-
1333
- 如果你看到 `code=invalidParameter`,通常优先检查请求 payload 的必填字段(例如 `robotCode`、`userIds`、`msgKey`、`msgParam`)是否完整且格式正确。
1334
-
1335
- ## 开发指南
1336
-
1337
- ### 首次设置
1338
-
1339
- 1. 克隆仓库并安装依赖
163
+ ## 开发简述
1340
164
 
1341
165
  ```bash
1342
166
  git clone https://github.com/soimy/openclaw-channel-dingtalk.git
1343
167
  cd openclaw-channel-dingtalk
1344
168
  npm install
1345
- ```
1346
-
1347
- 2. 验证开发环境
1348
-
1349
- ```bash
1350
- npm run type-check # TypeScript 类型检查
1351
- npm run lint # ESLint 代码检查
1352
- ```
1353
-
1354
- ### 常用命令
1355
-
1356
- | 命令 | 说明 |
1357
- | -------------------- | ------------------- |
1358
- | `npm run type-check` | TypeScript 类型检查 |
1359
- | `npm run lint` | ESLint 代码检查 |
1360
- | `npm run lint:fix` | 自动修复格式问题 |
1361
-
1362
- ### 项目结构
1363
-
1364
- ```
1365
- src/
1366
- channel.ts - 插件定义和辅助函数(535 行)
1367
- runtime.ts - 运行时管理(14 行)
1368
- types.ts - 类型定义(30+ interfaces)
1369
-
1370
- index.ts - 插件注册(29 行)
1371
- utils.ts - 工具函数(110 行)
1372
-
1373
- openclaw.plugin.json - 插件配置
1374
- package.json - 项目配置
1375
- README.md - 本文件
1376
- ```
1377
-
1378
- ### 代码质量
1379
-
1380
- - **TypeScript**: 严格模式,0 错误
1381
- - **ESLint**: 自动检查和修复
1382
- - **Type Safety**: 完整的类型注解(30+ 接口)
1383
-
1384
- ### 类型系统
1385
-
1386
- 核心类型定义在 `src/types.ts` 中,包括:
1387
-
1388
- ```typescript
1389
- // 配置
1390
- DingTalkConfig; // 插件配置
1391
- DingTalkChannelConfig; // 多账户配置
1392
-
1393
- // 消息处理
1394
- DingTalkInboundMessage; // 收到的钉钉消息
1395
- MessageContent; // 解析后的消息内容
1396
- HandleDingTalkMessageParams; // 消息处理参数
1397
-
1398
- // AI 互动卡片
1399
- AICardInstance; // AI 卡片实例
1400
- AICardCreateAndDeliverRequest; // 创建并投放卡片请求
1401
- AICardStreamingRequest; // 流式更新请求
1402
- AICardStatus; // 卡片状态常量
1403
-
1404
- // 工具函数类型
1405
- Logger; // 日志接口
1406
- RetryOptions; // 重试选项
1407
- MediaFile; // 下载的媒体文件
1408
- ```
1409
-
1410
- ### 公开 API
1411
-
1412
- 插件导出以下低级 API 函数,可用于自定义集成:
1413
-
1414
- ```typescript
1415
- // 文本/Markdown 消息
1416
- sendBySession(config, sessionWebhook, text, options); // 通过会话发送
1417
-
1418
- // AI 互动卡片
1419
- createAICard(config, conversationId, log); // 创建并投放 AI 卡片
1420
- streamAICard(card, content, finished, log); // 流式更新卡片内容
1421
- finishAICard(card, content, log); // 完成并关闭卡片
1422
-
1423
- // 自动模式选择
1424
- sendMessage(config, conversationId, text, options); // 根据配置自动选择(含卡片/文本回退)
1425
-
1426
- // 主动媒体发送
1427
- uploadMedia(config, mediaPath, mediaType, log); // 上传媒体并返回 media_id
1428
- sendProactiveMedia(config, target, mediaPath, mediaType, options); // 发送图片/语音/视频/文件
1429
-
1430
- // 认证
1431
- getAccessToken(config, log); // 获取访问令牌
1432
- ```
1433
-
1434
- **使用示例:**
1435
-
1436
- ```typescript
1437
- import {
1438
- createAICard,
1439
- finishAICard,
1440
- sendProactiveMedia,
1441
- streamAICard,
1442
- } from './src/channel';
1443
-
1444
- // 创建 AI 卡片
1445
- const card = await createAICard(config, conversationId, log);
1446
-
1447
- // 流式更新内容
1448
- for (const chunk of aiResponseChunks) {
1449
- await streamAICard(card, currentText + chunk, false, log);
1450
- }
1451
-
1452
- // 完成并关闭卡片
1453
- await finishAICard(card, finalText, log);
1454
-
1455
- // 主动发送图片
1456
- await sendProactiveMedia(config, 'cidxxxxxxxx', '/absolute/path/to/photo.png', 'image', {
1457
- accountId: 'default',
1458
- log,
1459
- });
1460
- ```
1461
-
1462
- ## 架构与职责边界
1463
-
1464
- 仓库整体架构、模块职责边界、增量迁移策略和新功能落位建议,以 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) 为准。
1465
- 中文版本见 [`docs/ARCHITECTURE.zh-CN.md`](docs/ARCHITECTURE.zh-CN.md)。
1466
-
1467
- 协作时建议优先把握这些总原则:
1468
-
1469
- - 先遵守逻辑功能分区,再做物理目录迁移
1470
- - `src/channel.ts` 保持为装配层,避免继续堆积业务逻辑
1471
- - 新功能应优先落到清晰的业务域,而不是继续平铺到 `src/` 根目录
1472
- - 结构重排尽量与行为改动拆分,降低进行中 PR 的冲突面
1473
- - 对存量代码采用渐进迁移策略,不要求一次性整体搬迁
1474
-
1475
- 计划中的目录分区摘要:
1476
-
1477
- - `gateway/`: Stream 连接、回调注册、入站事件入口与启停时序
1478
- - `targeting/`: `conversationId`、peer、session alias、目标解析与群目录能力
1479
- - `messaging/`: 入站内容提取、reply strategy、文本/markdown/media 发送与消息上下文
1480
- - `card/`: AI Card 创建、流式更新、结束态、恢复与缓存
1481
- - `command/`: slash 命令、feedback learning、target rule 与后续扩展命令能力
1482
- - `platform/`: config、auth、runtime、logger、types 等底层平台能力
1483
- - `shared/`: 跨领域复用的持久化原语、dedup 与通用工具
1484
-
1485
- ## 测试
1486
-
1487
- 项目已基于 Vitest 初始化自动化测试,目录结构如下:
1488
-
1489
- ```text
1490
- tests/
1491
- unit/
1492
- sign.test.ts # HmacSHA256 + Base64 签名测试
1493
- message-transform.test.ts # 文本/Markdown 消息转换测试
1494
- integration/
1495
- send-lifecycle.test.ts # 插件 outbound.sendText 生命周期适配测试
1496
- ```
1497
-
1498
- ### 运行测试
1499
-
1500
- ```bash
1501
- # 安装依赖(pnpm)
1502
- pnpm install
1503
-
1504
- # 运行全部测试
169
+ npm run type-check
170
+ npm run lint
1505
171
  pnpm test
1506
-
1507
- # 生成覆盖率报告(coverage/)
1508
- pnpm test:coverage
1509
172
  ```
1510
173
 
1511
- ### Mock 约束
174
+ 更多开发与维护说明:
1512
175
 
1513
- - 所有测试中的网络请求均通过 `vi.mock('axios')` 拦截,禁止真实调用钉钉 API。
1514
- - 集成测试通过模块 mock 隔离 `openclaw/plugin-sdk`、`dingtalk-stream` 等外部依赖。
176
+ - [本地开发](docs/contributor/development.md)
177
+ - [测试与验证](docs/contributor/testing.md)
178
+ - [架构说明(中文详版)](docs/contributor/architecture.zh-CN.md)
179
+ - [NPM 发布](docs/contributor/npm-publish.md)
1515
180
 
1516
181
  ## 许可
1517
182