@soimy/dingtalk 2.6.5
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 +482 -0
- package/clawbot.plugin.json +9 -0
- package/index.ts +17 -0
- package/openclaw.plugin.json +9 -0
- package/package.json +79 -0
- package/src/AGENTS.md +63 -0
- package/src/channel.ts +1807 -0
- package/src/config-schema.ts +92 -0
- package/src/connection-manager.ts +434 -0
- package/src/media-utils.ts +132 -0
- package/src/onboarding.ts +325 -0
- package/src/openclaw-channel-dingtalk.code-workspace +17 -0
- package/src/peer-id-registry.ts +35 -0
- package/src/runtime.ts +14 -0
- package/src/types.ts +543 -0
- package/src/utils.ts +106 -0
package/README.md
ADDED
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
# DingTalk Channel for OpenClaw
|
|
2
|
+
|
|
3
|
+
钉钉企业内部机器人 Channel 插件,使用 Stream 模式(无需公网 IP)。
|
|
4
|
+
|
|
5
|
+
## 功能特性
|
|
6
|
+
|
|
7
|
+
- ✅ **Stream 模式** — WebSocket 长连接,无需公网 IP 或 Webhook
|
|
8
|
+
- ✅ **私聊支持** — 直接与机器人对话
|
|
9
|
+
- ✅ **群聊支持** — 在群里 @机器人
|
|
10
|
+
- ✅ **多种消息类型** — 文本、图片、语音(自带识别)、视频、文件
|
|
11
|
+
- ✅ **Markdown 回复** — 支持富文本格式回复
|
|
12
|
+
- ✅ **互动卡片** — 支持流式更新,适用于 AI 实时输出
|
|
13
|
+
- ✅ **完整 AI 对话** — 接入 Clawdbot 消息处理管道
|
|
14
|
+
|
|
15
|
+
## 安装
|
|
16
|
+
|
|
17
|
+
### 方法 A:通过 npm 包安装 (推荐)
|
|
18
|
+
|
|
19
|
+
手动通过 npm 包名安装:
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
openclaw plugins install @soimy/dingtalk
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 方法 B:通过本地源码安装
|
|
26
|
+
|
|
27
|
+
如果你想对插件进行二次开发,可以先克隆仓库:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
# 1. 克隆仓库
|
|
31
|
+
git clone https://github.com/soimy/openclaw-channel-dingtalk.git
|
|
32
|
+
cd openclaw-channel-dingtalk
|
|
33
|
+
|
|
34
|
+
# 2. 安装依赖 (必需)
|
|
35
|
+
npm install
|
|
36
|
+
|
|
37
|
+
# 3. 以链接模式安装 (方便修改代码后实时生效)
|
|
38
|
+
openclaw plugins install -l .
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
### 方法 C:手动安装
|
|
42
|
+
|
|
43
|
+
1. 将本目录下载或复制到 `~/.openclaw/extensions/dingtalk`。
|
|
44
|
+
2. 确保包含 `index.ts`, `openclaw.plugin.json` 和 `package.json`。
|
|
45
|
+
3. 运行 `openclaw plugins list` 确认 `dingtalk` 已显示在列表中。
|
|
46
|
+
|
|
47
|
+
## 更新
|
|
48
|
+
|
|
49
|
+
```
|
|
50
|
+
openclaw plugins update @soimy/dingtalk
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
## 配置
|
|
54
|
+
|
|
55
|
+
OpenClaw 支持**交互式配置**和**手动配置文件**两种方式。
|
|
56
|
+
|
|
57
|
+
### 方法 1:交互式配置(推荐)
|
|
58
|
+
|
|
59
|
+
使用 OpenClaw 命令行向导式配置插件参数:
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
# 方式 A:使用 onboard 命令
|
|
63
|
+
openclaw onboard
|
|
64
|
+
|
|
65
|
+
# 方式 B:直接配置 channels 部分
|
|
66
|
+
openclaw configure --section channels
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
交互式配置流程:
|
|
70
|
+
|
|
71
|
+
1. **选择插件** — 在插件列表中选择 `dingtalk` 或 `DingTalk (钉钉)`
|
|
72
|
+
2. **Client ID** — 输入钉钉应用的 AppKey
|
|
73
|
+
3. **Client Secret** — 输入钉钉应用的 AppSecret
|
|
74
|
+
4. **完整配置** — 可选配置 Robot Code、Corp ID、Agent ID(推荐)
|
|
75
|
+
5. **卡片模式** — 可选启用 AI 互动卡片模式
|
|
76
|
+
- 如启用,需输入 Card Template ID 和 Card Template Key
|
|
77
|
+
6. **私聊策略** — 选择 `open`(开放)或 `allowlist`(白名单)
|
|
78
|
+
7. **群聊策略** — 选择 `open`(开放)或 `allowlist`(白名单)
|
|
79
|
+
|
|
80
|
+
> 所有的参数参考下文中的钉钉开发者平台配置指南
|
|
81
|
+
|
|
82
|
+
配置完成后会自动保存并重启 Gateway。
|
|
83
|
+
|
|
84
|
+
---
|
|
85
|
+
|
|
86
|
+
#### 钉钉开发者平台配置指南
|
|
87
|
+
|
|
88
|
+
##### 1. 创建钉钉应用
|
|
89
|
+
|
|
90
|
+
1. 访问 [钉钉开发者后台](https://open-dev.dingtalk.com/)
|
|
91
|
+
2. 创建企业内部应用
|
|
92
|
+
3. 添加「机器人」能力
|
|
93
|
+
4. 配置消息接收模式为 **Stream 模式**
|
|
94
|
+
5. 发布应用
|
|
95
|
+
|
|
96
|
+
##### 2. 配置权限管理
|
|
97
|
+
|
|
98
|
+
在应用的权限管理页面,需要开启以下权限:
|
|
99
|
+
|
|
100
|
+
- ✅ **Card.Instance.Write** — 创建和投放卡片实例
|
|
101
|
+
- ✅ **Card.Streaming.Write** — 对卡片进行流式更新
|
|
102
|
+
|
|
103
|
+
**步骤:**
|
|
104
|
+
|
|
105
|
+
1. 进入应用 → 权限管理
|
|
106
|
+
2. 搜索「Card」相关权限
|
|
107
|
+
3. 勾选上述两个权限
|
|
108
|
+
4. 保存权限配置
|
|
109
|
+
|
|
110
|
+
##### 3. 建立卡片模板(可选)
|
|
111
|
+
|
|
112
|
+
**步骤:**
|
|
113
|
+
|
|
114
|
+
1. 访问 [钉钉卡片平台](https://open-dev.dingtalk.com/fe/card)
|
|
115
|
+
2. 进入「我的模板」
|
|
116
|
+
3. 点击「创建模板」
|
|
117
|
+
4. 卡片模板场景选择 **「AI 卡片」**
|
|
118
|
+
5. 按需设计卡片排版,点击保存并发布
|
|
119
|
+
6. 记下模板中定义的内容字段名称
|
|
120
|
+
7. 复制模板 ID(格式如:`xxxxx-xxxxx-xxxxx.schema`)
|
|
121
|
+
8. 将 templateId 配置到 `openclaw.json` 的 `cardTemplateId` 字段
|
|
122
|
+
9. 或在OpenClaw控制台的Channel标签->Dingtalk配置面板-> Card Template Id填入
|
|
123
|
+
10. 将记下的内容字段变量名配置到 `openclaw.json` 的 `cardTemplateKey` 字段
|
|
124
|
+
11. 或在OpenClaw控制台的Channel标签->Dingtalk配置面板-> Card Template Key填入
|
|
125
|
+
|
|
126
|
+
**说明:**
|
|
127
|
+
|
|
128
|
+
- 使用 DingTalk 官方 AI 卡片模板时,`cardTemplateKey` 默认为 `'msgContent'`,无需修改
|
|
129
|
+
- 如果您创建自定义卡片模板,需要确保模板中包含相应的内容字段,并将 `cardTemplateKey` 配置为该字段名称
|
|
130
|
+
|
|
131
|
+
##### 4. 获取凭证
|
|
132
|
+
|
|
133
|
+
从开发者后台获取:
|
|
134
|
+
|
|
135
|
+
- **Client ID** (AppKey)
|
|
136
|
+
- **Client Secret** (AppSecret)
|
|
137
|
+
- **Robot Code** (与 Client ID 相同)
|
|
138
|
+
- **Corp ID** (企业 ID)
|
|
139
|
+
- **Agent ID** (应用 ID)
|
|
140
|
+
|
|
141
|
+
### 方法 2:手动配置文件
|
|
142
|
+
|
|
143
|
+
在 `~/.openclaw/openclaw.json` 的 `channels` 下添加(仅作参考,交互式配置会自动生成):
|
|
144
|
+
|
|
145
|
+
> 只添加dingtalk部分,内容参考上文钉钉开发者配置指南
|
|
146
|
+
|
|
147
|
+
```json5
|
|
148
|
+
{
|
|
149
|
+
...
|
|
150
|
+
"channels": {
|
|
151
|
+
"telegram": { ... },
|
|
152
|
+
|
|
153
|
+
"dingtalk": {
|
|
154
|
+
"enabled": true,
|
|
155
|
+
"clientId": "dingxxxxxx",
|
|
156
|
+
"clientSecret": "your-app-secret",
|
|
157
|
+
"robotCode": "dingxxxxxx",
|
|
158
|
+
"corpId": "dingxxxxxx",
|
|
159
|
+
"agentId": "123456789",
|
|
160
|
+
"dmPolicy": "open",
|
|
161
|
+
"groupPolicy": "open",
|
|
162
|
+
"debug": false,
|
|
163
|
+
"messageType": "markdown", // 或 "card"
|
|
164
|
+
// 仅card需要配置
|
|
165
|
+
"cardTemplateId": "你复制的模板ID",
|
|
166
|
+
"cardTemplateKey": "你模板的内容变量"
|
|
167
|
+
}
|
|
168
|
+
},
|
|
169
|
+
...
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
最后重启 Gateway
|
|
174
|
+
|
|
175
|
+
> 使用交互式配置时,Gateway 会自动重启。使用手动配置时需要手动执行:
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
openclaw gateway restart
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## 配置选项
|
|
182
|
+
|
|
183
|
+
| 选项 | 类型 | 默认值 | 说明 |
|
|
184
|
+
| ----------------------- | -------- | ------------ | ------------------------------------------- |
|
|
185
|
+
| `enabled` | boolean | `true` | 是否启用 |
|
|
186
|
+
| `clientId` | string | 必填 | 应用的 AppKey |
|
|
187
|
+
| `clientSecret` | string | 必填 | 应用的 AppSecret |
|
|
188
|
+
| `robotCode` | string | - | 机器人代码(用于下载媒体和发送卡片) |
|
|
189
|
+
| `corpId` | string | - | 企业 ID |
|
|
190
|
+
| `agentId` | string | - | 应用 ID |
|
|
191
|
+
| `dmPolicy` | string | `"open"` | 私聊策略:open/pairing/allowlist |
|
|
192
|
+
| `groupPolicy` | string | `"open"` | 群聊策略:open/allowlist |
|
|
193
|
+
| `allowFrom` | string[] | `[]` | 允许的发送者 ID 列表 |
|
|
194
|
+
| `messageType` | string | `"markdown"` | 消息类型:markdown/card |
|
|
195
|
+
| `cardTemplateId` | string | | AI 互动卡片模板 ID(仅当 messageType=card) |
|
|
196
|
+
| `cardTemplateKey` | string | `"content"` | 卡片模板内容字段键(仅当 messageType=card) |
|
|
197
|
+
| `debug` | boolean | `false` | 是否开启调试日志 |
|
|
198
|
+
| `maxConnectionAttempts` | number | `10` | 最大连接尝试次数 |
|
|
199
|
+
| `initialReconnectDelay` | number | `1000` | 初始重连延迟(毫秒) |
|
|
200
|
+
| `maxReconnectDelay` | number | `60000` | 最大重连延迟(毫秒) |
|
|
201
|
+
| `reconnectJitter` | number | `0.3` | 重连延迟抖动因子(0-1) |
|
|
202
|
+
|
|
203
|
+
### 连接鲁棒性配置
|
|
204
|
+
|
|
205
|
+
为提高连接稳定性,插件支持以下高级配置:
|
|
206
|
+
|
|
207
|
+
- **maxConnectionAttempts**: 连接失败后的最大重试次数,超过后将停止尝试并报警。
|
|
208
|
+
- **initialReconnectDelay**: 第一次重连的初始延迟(毫秒),后续重连会按指数增长。
|
|
209
|
+
- **maxReconnectDelay**: 重连延迟的上限(毫秒),防止等待时间过长。
|
|
210
|
+
- **reconnectJitter**: 延迟抖动因子,在延迟基础上增加随机变化(±30%),避免多个客户端同时重连。
|
|
211
|
+
|
|
212
|
+
重连延迟计算公式:`delay = min(initialDelay × 2^attempt, maxDelay) × (1 ± jitter)`
|
|
213
|
+
|
|
214
|
+
示例延迟序列(默认配置):~1s, ~2s, ~4s, ~8s, ~16s, ~32s, ~60s(达到上限)
|
|
215
|
+
|
|
216
|
+
更多详情请参阅 [CONNECTION_ROBUSTNESS.md](./CONNECTION_ROBUSTNESS.md)。
|
|
217
|
+
|
|
218
|
+
## 安全策略
|
|
219
|
+
|
|
220
|
+
### 私聊策略 (dmPolicy)
|
|
221
|
+
|
|
222
|
+
- `open` — 任何人都可以私聊机器人
|
|
223
|
+
- `pairing` — 新用户需要通过配对码验证
|
|
224
|
+
- `allowlist` — 只有 allowFrom 列表中的用户可以使用
|
|
225
|
+
|
|
226
|
+
### 群聊策略 (groupPolicy)
|
|
227
|
+
|
|
228
|
+
- `open` — 任何群都可以 @机器人
|
|
229
|
+
- `allowlist` — 只有配置的群可以使用
|
|
230
|
+
|
|
231
|
+
## 消息类型支持
|
|
232
|
+
|
|
233
|
+
### 接收
|
|
234
|
+
|
|
235
|
+
| 类型 | 支持 | 说明 |
|
|
236
|
+
| ------ | ---- | -------------------- |
|
|
237
|
+
| 文本 | ✅ | 完整支持 |
|
|
238
|
+
| 富文本 | ✅ | 提取文本内容 |
|
|
239
|
+
| 图片 | ✅ | 下载并传递给 AI |
|
|
240
|
+
| 语音 | ✅ | 使用钉钉语音识别结果 |
|
|
241
|
+
| 视频 | ✅ | 下载并传递给 AI |
|
|
242
|
+
| 文件 | ✅ | 下载并传递给 AI |
|
|
243
|
+
|
|
244
|
+
### 发送
|
|
245
|
+
|
|
246
|
+
| 类型 | 支持 | 说明 |
|
|
247
|
+
| -------- | ---- | -------------------------------- |
|
|
248
|
+
| 文本 | ✅ | 完整支持 |
|
|
249
|
+
| Markdown | ✅ | 自动检测或手动指定 |
|
|
250
|
+
| 互动卡片 | ✅ | 支持流式更新,适用于 AI 实时输出 |
|
|
251
|
+
| 图片 | ⏳ | 需要通过媒体上传 API |
|
|
252
|
+
|
|
253
|
+
## API 消耗说明
|
|
254
|
+
|
|
255
|
+
### Text/Markdown 模式
|
|
256
|
+
|
|
257
|
+
| 操作 | API 调用次数 | 说明 |
|
|
258
|
+
| ---------- | ------------ | ---------------------------------------------------------------------------- |
|
|
259
|
+
| 获取 Token | 1 | 共享/缓存(60 秒检查过期一次) |
|
|
260
|
+
| 发送消息 | 1 | 使用 `/v1.0/robot/oToMessages/batchSend` 或 `/v1.0/robot/groupMessages/send` |
|
|
261
|
+
| **总计** | **2** | 每条回复 1 次 |
|
|
262
|
+
|
|
263
|
+
### Card(AI 互动卡片)模式
|
|
264
|
+
|
|
265
|
+
| 阶段 | API 调用 | 说明 |
|
|
266
|
+
| ------------ | ---------------------- | --------------------------------------------------- |
|
|
267
|
+
| **创建卡片** | 1 | `POST /v1.0/card/instances/createAndDeliver` |
|
|
268
|
+
| **流式更新** | M | M = 回复块数量,每块一次 `PUT /v1.0/card/streaming` |
|
|
269
|
+
| **完成卡片** | 包含在最后一次流更新中 | 使用 `isFinalize=true` 标记 |
|
|
270
|
+
| **总计** | **1 + M** | M = Agent 产生的回复块数 |
|
|
271
|
+
|
|
272
|
+
### 典型场景成本对比
|
|
273
|
+
|
|
274
|
+
| 场景 | Text/Markdown | Card | 节省 |
|
|
275
|
+
| ---------------- | ------------- | ---- | ------ |
|
|
276
|
+
| 简短回复(1 块) | 2 | 2 | ✓ 相同 |
|
|
277
|
+
| 中等回复(5 块) | 6 | 6 | ✓ 相同 |
|
|
278
|
+
| 长回复(10 块) | 12 | 11 | ✓ 1 次 |
|
|
279
|
+
|
|
280
|
+
### 优化策略
|
|
281
|
+
|
|
282
|
+
**降低 API 调用的方法:**
|
|
283
|
+
|
|
284
|
+
1. **合并回复块** — 通过调整 Agent 输出配置,减少块数量
|
|
285
|
+
2. **使用缓存** — Token 自动缓存(60 秒),无需每次都获取
|
|
286
|
+
3. **Buffer 模式** — 使用 `dispatchReplyWithBufferedBlockDispatcher` 合并多个小块
|
|
287
|
+
|
|
288
|
+
**成本建议:**
|
|
289
|
+
|
|
290
|
+
- ✅ **推荐** — Card 模式:流式体验更好,成本与 Text/Markdown 相当或更低
|
|
291
|
+
- ⚠️ **谨慎** — 频繁调用需要监测配额,建议使用钉钉开发者后台查看 API 调用量
|
|
292
|
+
|
|
293
|
+
## 消息类型选择
|
|
294
|
+
|
|
295
|
+
插件支持两种消息回复类型,可通过 `messageType` 配置:
|
|
296
|
+
|
|
297
|
+
### 1. markdown(Markdown 格式)**【默认】**
|
|
298
|
+
|
|
299
|
+
- 支持富文本格式(标题、粗体、列表等)
|
|
300
|
+
- 自动检测消息是否包含 Markdown 语法
|
|
301
|
+
- 适用于大多数场景
|
|
302
|
+
|
|
303
|
+
### 2. card(AI 互动卡片)
|
|
304
|
+
|
|
305
|
+
- 支持流式更新(实时显示 AI 生成内容)
|
|
306
|
+
- 更好的视觉呈现和交互体验
|
|
307
|
+
- 支持 Markdown 格式渲染
|
|
308
|
+
- 通过 `cardTemplateId` 指定模板
|
|
309
|
+
- 通过 `cardTemplateKey` 指定内容字段
|
|
310
|
+
- **适用于 AI 对话场景**
|
|
311
|
+
|
|
312
|
+
**AI Card API 特性:**
|
|
313
|
+
当配置 `messageType: 'card'` 时:
|
|
314
|
+
|
|
315
|
+
1. 使用 `/v1.0/card/instances/createAndDeliver` 创建并投放卡片
|
|
316
|
+
2. 使用 `/v1.0/card/streaming` 实现真正的流式更新
|
|
317
|
+
3. 自动状态管理(PROCESSING → INPUTING → FINISHED)
|
|
318
|
+
4. 更稳定的流式体验,无需手动节流
|
|
319
|
+
|
|
320
|
+
**配置示例:**
|
|
321
|
+
|
|
322
|
+
```json5
|
|
323
|
+
{
|
|
324
|
+
messageType: 'card', // 启用 AI 互动卡片模式
|
|
325
|
+
cardTemplateId: '382e4302-551d-4880-bf29-a30acfab2e71.schema', // AI 卡片模板 ID(默认值)
|
|
326
|
+
cardTemplateKey: 'msgContent', // 卡片内容字段键(默认值:msgContent)
|
|
327
|
+
}
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
> **注意**:`cardTemplateKey` 应与您的卡片模板中定义的字段名称一致。默认值为 `'msgContent'`,适用于 DingTalk 官方 AI 卡片模板。如果您使用自定义模板,请根据模板定义的字段名称进行配置。
|
|
331
|
+
|
|
332
|
+
## 使用示例
|
|
333
|
+
|
|
334
|
+
配置完成后,直接在钉钉中:
|
|
335
|
+
|
|
336
|
+
1. **私聊机器人** — 找到机器人,发送消息
|
|
337
|
+
2. **群聊 @机器人** — 在群里 @机器人名称 + 消息
|
|
338
|
+
|
|
339
|
+
## 故障排除
|
|
340
|
+
|
|
341
|
+
### 收不到消息
|
|
342
|
+
|
|
343
|
+
1. 确认应用已发布
|
|
344
|
+
2. 确认消息接收模式是 Stream
|
|
345
|
+
3. 检查 Gateway 日志:`openclaw logs | grep dingtalk`
|
|
346
|
+
|
|
347
|
+
### 群消息无响应
|
|
348
|
+
|
|
349
|
+
1. 确认机器人已添加到群
|
|
350
|
+
2. 确认正确 @机器人(使用机器人名称)
|
|
351
|
+
3. 确认群是企业内部群
|
|
352
|
+
|
|
353
|
+
### 连接失败
|
|
354
|
+
|
|
355
|
+
1. 检查 clientId 和 clientSecret 是否正确
|
|
356
|
+
2. 确认网络可以访问钉钉 API
|
|
357
|
+
|
|
358
|
+
## 开发指南
|
|
359
|
+
|
|
360
|
+
### 首次设置
|
|
361
|
+
|
|
362
|
+
1. 克隆仓库并安装依赖
|
|
363
|
+
|
|
364
|
+
```bash
|
|
365
|
+
git clone https://github.com/soimy/openclaw-channel-dingtalk.git
|
|
366
|
+
cd openclaw-channel-dingtalk
|
|
367
|
+
npm install
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
2. 验证开发环境
|
|
371
|
+
|
|
372
|
+
```bash
|
|
373
|
+
npm run type-check # TypeScript 类型检查
|
|
374
|
+
npm run lint # ESLint 代码检查
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
### 常用命令
|
|
378
|
+
|
|
379
|
+
| 命令 | 说明 |
|
|
380
|
+
| -------------------- | ------------------- |
|
|
381
|
+
| `npm run type-check` | TypeScript 类型检查 |
|
|
382
|
+
| `npm run lint` | ESLint 代码检查 |
|
|
383
|
+
| `npm run lint:fix` | 自动修复格式问题 |
|
|
384
|
+
|
|
385
|
+
### 项目结构
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
src/
|
|
389
|
+
channel.ts - 插件定义和辅助函数(535 行)
|
|
390
|
+
runtime.ts - 运行时管理(14 行)
|
|
391
|
+
types.ts - 类型定义(30+ interfaces)
|
|
392
|
+
|
|
393
|
+
index.ts - 插件注册(29 行)
|
|
394
|
+
utils.ts - 工具函数(110 行)
|
|
395
|
+
|
|
396
|
+
openclaw.plugin.json - 插件配置
|
|
397
|
+
package.json - 项目配置
|
|
398
|
+
README.md - 本文件
|
|
399
|
+
```
|
|
400
|
+
|
|
401
|
+
### 代码质量
|
|
402
|
+
|
|
403
|
+
- **TypeScript**: 严格模式,0 错误
|
|
404
|
+
- **ESLint**: 自动检查和修复
|
|
405
|
+
- **Type Safety**: 完整的类型注解(30+ 接口)
|
|
406
|
+
|
|
407
|
+
### 类型系统
|
|
408
|
+
|
|
409
|
+
核心类型定义在 `src/types.ts` 中,包括:
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
// 配置
|
|
413
|
+
DingTalkConfig; // 插件配置
|
|
414
|
+
DingTalkChannelConfig; // 多账户配置
|
|
415
|
+
|
|
416
|
+
// 消息处理
|
|
417
|
+
DingTalkInboundMessage; // 收到的钉钉消息
|
|
418
|
+
MessageContent; // 解析后的消息内容
|
|
419
|
+
HandleDingTalkMessageParams; // 消息处理参数
|
|
420
|
+
|
|
421
|
+
// AI 互动卡片
|
|
422
|
+
AICardInstance; // AI 卡片实例
|
|
423
|
+
AICardCreateAndDeliverRequest; // 创建并投放卡片请求
|
|
424
|
+
AICardStreamingRequest; // 流式更新请求
|
|
425
|
+
AICardStatus; // 卡片状态常量
|
|
426
|
+
|
|
427
|
+
// 工具函数类型
|
|
428
|
+
Logger; // 日志接口
|
|
429
|
+
RetryOptions; // 重试选项
|
|
430
|
+
MediaFile; // 下载的媒体文件
|
|
431
|
+
```
|
|
432
|
+
|
|
433
|
+
### 公开 API
|
|
434
|
+
|
|
435
|
+
插件导出以下低级 API 函数,可用于自定义集成:
|
|
436
|
+
|
|
437
|
+
```typescript
|
|
438
|
+
// 文本/Markdown 消息
|
|
439
|
+
sendBySession(config, sessionWebhook, text, options); // 通过会话发送
|
|
440
|
+
|
|
441
|
+
// AI 互动卡片
|
|
442
|
+
createAICard(config, conversationId, data, log); // 创建并投放 AI 卡片
|
|
443
|
+
streamAICard(card, content, finished, log); // 流式更新卡片内容
|
|
444
|
+
finishAICard(card, content, log); // 完成并关闭卡片
|
|
445
|
+
|
|
446
|
+
// 自动模式选择
|
|
447
|
+
sendMessage(config, conversationId, text, options); // 根据配置自动选择(含卡片/文本回退)
|
|
448
|
+
|
|
449
|
+
// 认证
|
|
450
|
+
getAccessToken(config, log); // 获取访问令牌
|
|
451
|
+
```
|
|
452
|
+
|
|
453
|
+
**使用示例:**
|
|
454
|
+
|
|
455
|
+
```typescript
|
|
456
|
+
import { createAICard, streamAICard, finishAICard } from './src/channel';
|
|
457
|
+
|
|
458
|
+
// 创建 AI 卡片
|
|
459
|
+
const card = await createAICard(config, conversationId, messageData, log);
|
|
460
|
+
|
|
461
|
+
// 流式更新内容
|
|
462
|
+
for (const chunk of aiResponseChunks) {
|
|
463
|
+
await streamAICard(card, currentText + chunk, false, log);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// 完成并关闭卡片
|
|
467
|
+
await finishAICard(card, finalText, log);
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### 架构
|
|
471
|
+
|
|
472
|
+
插件遵循 Telegram 参考实现的架构模式:
|
|
473
|
+
|
|
474
|
+
- **index.ts**: 最小化插件注册入口
|
|
475
|
+
- **src/channel.ts**: 所有 DingTalk 特定的逻辑(API、消息处理、配置等)
|
|
476
|
+
- **src/runtime.ts**: 运行时管理(getter/setter)
|
|
477
|
+
- **src/types.ts**: 类型定义
|
|
478
|
+
- **utils.ts**: 通用工具函数
|
|
479
|
+
|
|
480
|
+
## 许可
|
|
481
|
+
|
|
482
|
+
MIT
|
package/index.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { OpenClawPluginApi } from 'openclaw/plugin-sdk';
|
|
2
|
+
import { emptyPluginConfigSchema } from 'openclaw/plugin-sdk';
|
|
3
|
+
import { dingtalkPlugin } from './src/channel';
|
|
4
|
+
import { setDingTalkRuntime } from './src/runtime';
|
|
5
|
+
|
|
6
|
+
const plugin = {
|
|
7
|
+
id: 'dingtalk',
|
|
8
|
+
name: 'DingTalk Channel',
|
|
9
|
+
description: 'DingTalk (钉钉) messaging channel via Stream mode',
|
|
10
|
+
configSchema: emptyPluginConfigSchema(),
|
|
11
|
+
register(api: OpenClawPluginApi): void {
|
|
12
|
+
setDingTalkRuntime(api.runtime);
|
|
13
|
+
api.registerChannel({ plugin: dingtalkPlugin });
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export default plugin;
|
package/package.json
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@soimy/dingtalk",
|
|
3
|
+
"version": "2.6.5",
|
|
4
|
+
"description": "DingTalk (钉钉) channel plugin for OpenClaw",
|
|
5
|
+
"main": "index.ts",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"files": [
|
|
8
|
+
"index.ts",
|
|
9
|
+
"src",
|
|
10
|
+
"openclaw.plugin.json",
|
|
11
|
+
"clawbot.plugin.json"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"type-check": "tsc --noEmit",
|
|
15
|
+
"lint": "eslint index.ts src/",
|
|
16
|
+
"lint:fix": "eslint --fix index.ts src/ && prettier --write index.ts src/"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"clawdbot",
|
|
20
|
+
"openclaw",
|
|
21
|
+
"dingtalk",
|
|
22
|
+
"channel",
|
|
23
|
+
"stream",
|
|
24
|
+
"钉钉",
|
|
25
|
+
"bot"
|
|
26
|
+
],
|
|
27
|
+
"author": "YM Shen <soimy@163.com> (http://github.com/soimy)",
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"repository": {
|
|
30
|
+
"type": "git",
|
|
31
|
+
"url": "git+https://github.com/soimy/openclaw-channel-dingtalk.git"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/soimy/openclaw-channel-dingtalk",
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"axios": "^1.6.0",
|
|
36
|
+
"dingtalk-stream": "^2.1.4",
|
|
37
|
+
"form-data": "^4.0.0",
|
|
38
|
+
"zod": "^4.3.6"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"@types/node": "^25.2.0",
|
|
42
|
+
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
|
43
|
+
"@typescript-eslint/parser": "^6.0.0",
|
|
44
|
+
"eslint": "^8.0.0",
|
|
45
|
+
"eslint-config-prettier": "^9.0.0",
|
|
46
|
+
"prettier": "^3.0.0",
|
|
47
|
+
"typescript": "^5.3.0"
|
|
48
|
+
},
|
|
49
|
+
"peerDependencies": {
|
|
50
|
+
"openclaw": ">=2026.2.13"
|
|
51
|
+
},
|
|
52
|
+
"openclaw": {
|
|
53
|
+
"extensions": [
|
|
54
|
+
"./index.ts"
|
|
55
|
+
],
|
|
56
|
+
"channels": [
|
|
57
|
+
"dingtalk"
|
|
58
|
+
],
|
|
59
|
+
"installDependencies": true,
|
|
60
|
+
"channel": {
|
|
61
|
+
"id": "dingtalk",
|
|
62
|
+
"label": "DingTalk",
|
|
63
|
+
"selectionLabel": "DingTalk (钉钉)",
|
|
64
|
+
"docsPath": "/channels/dingtalk",
|
|
65
|
+
"docsLabel": "dingtalk",
|
|
66
|
+
"blurb": "钉钉企业内部机器人,使用 Stream 模式,无需公网 IP。",
|
|
67
|
+
"order": 70,
|
|
68
|
+
"aliases": [
|
|
69
|
+
"dd",
|
|
70
|
+
"ding"
|
|
71
|
+
]
|
|
72
|
+
},
|
|
73
|
+
"install": {
|
|
74
|
+
"npmSpec": "@soimy/dingtalk",
|
|
75
|
+
"localPath": ".",
|
|
76
|
+
"defaultChoice": "npm"
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
package/src/AGENTS.md
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# SOURCE DIRECTORY
|
|
2
|
+
|
|
3
|
+
**Parent:** `./AGENTS.md`
|
|
4
|
+
|
|
5
|
+
## OVERVIEW
|
|
6
|
+
|
|
7
|
+
All DingTalk plugin implementation logic.
|
|
8
|
+
|
|
9
|
+
## STRUCTURE
|
|
10
|
+
|
|
11
|
+
```
|
|
12
|
+
src/
|
|
13
|
+
├── channel.ts # Main plugin definition, API calls, message handling, AI Card
|
|
14
|
+
├── types.ts # Type definitions (30+ interfaces, AI Card types)
|
|
15
|
+
├── runtime.ts # Runtime getter/setter pattern
|
|
16
|
+
└── config-schema.ts # Zod validation for configuration
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## WHERE TO LOOK
|
|
20
|
+
|
|
21
|
+
| Task | Location | Notes |
|
|
22
|
+
| ------------------------- | ---------------------- | -------------------------------------------- |
|
|
23
|
+
| Channel plugin definition | `channel.ts:862` | `dingtalkPlugin` export |
|
|
24
|
+
| AI Card operations | `channel.ts:374-600` | createAICard, streamAICard, finishAICard |
|
|
25
|
+
| Message sending | `channel.ts:520-700` | sendMessage, sendBySession |
|
|
26
|
+
| Token management | `channel.ts:156-177` | getAccessToken with cache |
|
|
27
|
+
| Message processing | `channel.ts:643-859` | handleDingTalkMessage, extractMessageContent |
|
|
28
|
+
| Type exports | `types.ts` | All interfaces/constants |
|
|
29
|
+
| Public API exports | `channel.ts:1068-1076` | sendBySession, createAICard, etc. |
|
|
30
|
+
|
|
31
|
+
## CONVENTIONS
|
|
32
|
+
|
|
33
|
+
Same as root. No src-specific deviations.
|
|
34
|
+
|
|
35
|
+
## ANTI-PATTERNS
|
|
36
|
+
|
|
37
|
+
**Prohibited:**
|
|
38
|
+
|
|
39
|
+
- Mutating module-level state outside of initialized functions
|
|
40
|
+
- Creating multiple AI Card instances for same conversationId (use cached)
|
|
41
|
+
- Calling DingTalk APIs without access token
|
|
42
|
+
- Suppressing errors in async handlers
|
|
43
|
+
|
|
44
|
+
## UNIQUE STYLES
|
|
45
|
+
|
|
46
|
+
**AI Card State Machine:**
|
|
47
|
+
|
|
48
|
+
- States: PROCESSING → INPUTING → FINISHED/FAILED
|
|
49
|
+
- Cached in `Map<string, AICardInstance>` with TTL cleanup
|
|
50
|
+
- Terminal states (FINISHED/FAILED) cleaned after 1 hour
|
|
51
|
+
|
|
52
|
+
**Access Token Caching:**
|
|
53
|
+
|
|
54
|
+
- Module-level variables: `accessToken`, `accessTokenExpiry`
|
|
55
|
+
- Refresh 60s before expiry
|
|
56
|
+
- Retry logic for 401/429/5xx errors
|
|
57
|
+
|
|
58
|
+
**Message Type Handling:**
|
|
59
|
+
|
|
60
|
+
- `text`: Plain text messages
|
|
61
|
+
- `richText`: Extract text + @mentions
|
|
62
|
+
- `picture/audio/video/file`: Download to `/tmp/dingtalk_*`
|
|
63
|
+
- Auto-detect Markdown syntax for auto-formatting
|