@jeik/dingtalk-connector 0.8.28 → 0.8.30

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.
Files changed (40) hide show
  1. package/CHANGELOG.md +40 -30
  2. package/README.en.md +114 -130
  3. package/README.md +112 -131
  4. package/dist/{background-work-Cxb4b-91.mjs → background-work-DkaSDqWk.mjs} +12 -1
  5. package/dist/card-content-cache-C7-XHhHw.mjs +2 -0
  6. package/dist/{card-content-cache-bDQFz2u8.mjs → card-content-cache-DT_OYThh.mjs} +15 -1
  7. package/dist/chunk-upload-BKM1y4Ba.mjs +2 -0
  8. package/dist/{chunk-upload-BE71drPg.mjs → chunk-upload-CIU5G_ay.mjs} +1 -1
  9. package/dist/{common-BZiVgd3A.mjs → common-BfIYs1yC.mjs} +3 -3
  10. package/dist/{common-DgL28gCZ.mjs → common-C0Oaf0z-.mjs} +1 -1
  11. package/dist/{connection-CZ7ZoN5_.mjs → connection-obtrPkj4.mjs} +56 -65
  12. package/dist/entry-bundled.mjs +1 -1
  13. package/dist/{gateway-methods-kNX3qvqw.mjs → gateway-methods-BdcpfxEZ.mjs} +1 -1
  14. package/dist/gateway-methods-DQ6TwOAr.mjs +2 -0
  15. package/dist/index.mjs +2 -2
  16. package/dist/{logger-mZ9OSbmD.mjs → logger-CnBTcwyq.mjs} +16 -2
  17. package/dist/logger-D8H2IcYz.mjs +2 -0
  18. package/dist/{media-BqpUGCni.mjs → media-DDtX37T2.mjs} +1 -1
  19. package/dist/{media-DnQB2BrL.mjs → media-g8tJtpfU.mjs} +29 -20
  20. package/dist/{message-handler-B1usz1wb.mjs → message-handler-BeTQQCUe.mjs} +33 -30
  21. package/dist/{messaging-DVS3UKOp.mjs → messaging-CK0Wz3Ze.mjs} +18 -20
  22. package/dist/{runtime-A6G7WAWn.mjs → runtime-CjE4Z9G5.mjs} +6 -5
  23. package/dist/{utils-Bjh4r_qS.mjs → utils-Bwz2Fls_.mjs} +1 -1
  24. package/docs/RELEASE_NOTES_V0.8.28.md +4 -4
  25. package/docs/RELEASE_NOTES_V0.8.29.md +72 -0
  26. package/openclaw.plugin.json +9 -5
  27. package/package.json +1 -1
  28. package/src/config/schema.ts +2 -1
  29. package/src/core/connection.ts +90 -113
  30. package/src/core/message-handler.ts +24 -16
  31. package/src/reply-dispatcher.ts +33 -20
  32. package/src/services/media.ts +60 -27
  33. package/src/services/messaging/card-content-cache.ts +11 -6
  34. package/src/services/messaging.ts +52 -38
  35. package/src/utils/background-work.ts +3 -1
  36. package/src/utils/logger.ts +23 -2
  37. package/dist/card-content-cache-c4Ls96eJ.mjs +0 -2
  38. package/dist/chunk-upload-PwhogwfE.mjs +0 -2
  39. package/dist/gateway-methods-BkI1gKaC.mjs +0 -2
  40. package/dist/logger-BmJkQkm1.mjs +0 -2
package/README.md CHANGED
@@ -4,7 +4,7 @@
4
4
  <p>基于官方最新 <strong>v0.8.24</strong> 的社区增强版:吸收官方长连接改进,并重点优化消息接收、排队反馈与模型错误展示。<br/>
5
5
  保留社区在图片、答案卡、首响体验上的增强,持续修官方暂未覆盖的实际使用问题。</p>
6
6
 
7
- <p><strong>当前发布版:<a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.28</strong>(推荐生产;一键安装:`npx -y @jeik/dingtalk-connector@0.8.28 install --force`)</p>
7
+ <p><strong>当前正式版:<a href="https://www.npmjs.com/package/@jeik/dingtalk-connector">@jeik/dingtalk-connector</a> v0.8.30</strong></p>
8
8
 
9
9
  <p>
10
10
  <a href="https://www.npmjs.com/package/@jeik/dingtalk-connector"><img src="https://img.shields.io/npm/v/@jeik/dingtalk-connector.svg?style=flat&colorA=18181B&colorB=28CF8D" alt="npm version" /></a>
@@ -23,87 +23,89 @@
23
23
 
24
24
  ## 🔧 最近更新
25
25
 
26
- ### 🚀 v0.8.28 · 2026-07-25(当前正式版)
26
+ ### 🚀 v0.8.30 · 2026-07-29(当前正式版)
27
27
 
28
- **主题:基于官方 0.8.24 · 长连接更稳 · 消息少丢 · 错误提示更清楚**
28
+ **主题:正式加固 · 日志更干净 · 排障只开 debug**
29
29
 
30
- 本版在社区增强能力之上,对齐官方 **0.8.24** 的连接改进,并重点打磨「发了消息却没反应」「错误卡卡住」「排队提示不出现」等真实使用体验。
30
+ **0.8.28**(连接更稳 / 消息少丢 / 错误卡可定稿)基础上再打磨:
31
31
 
32
- #### 基于官方 0.8.24 吸收了什么
32
+ | 体验 | 效果 |
33
+ |------|------|
34
+ | 🧹 **默认日志更轻** | 无图正常回复不再刷 LocalImage / MediaIdTrace / CardCache |
35
+ | 🔍 **排障更简单** | 配置 `"debug": true` 即输出连接、图片、引用、队列详细日志(无需环境变量) |
36
+ | 🔗 **连接 / 队列** | 继承 0.8.28:能收消息才算在线;连发、错误后再发更稳;排队反馈更及时 |
37
+ | ⚠️ **模型故障** | 503 / 欠费 / 无通道等更容易定格中文错误,少卡「正在召唤大模型」 |
38
+ | 🃏 **本仓库独有** | 答案卡、图片全路径、首响体验等继续保留 |
33
39
 
34
- - 更稳妥的长连接建立与保活方式,减少假死、误重连
35
- - 机器人处理长任务时连接不易被中途掐断
36
- - 控制台少刷无用日志,排障更清爽
40
+ ### 📦 v0.8.28 · 2026-07-25
37
41
 
38
- #### 社区本版额外优化(你会感受到的效果)
42
+ 基于官方 0.8.24 的长连接与消息体验加固(当前能力已并入 0.8.30)。
39
43
 
40
- | 体验 | 效果 |
41
- |------|------|
42
- | 🔗 **连接更靠谱** | 只有真正能收消息时才显示「已连接」;连不上会自动多试几轮,而不是假装在线 |
43
- | 📩 **消息更少丢** | 明显缓解:连发多条、错误回复后、任务刚结束时「发了没进网关」的情况 |
44
- | ⏳ **排队反馈更及时** | 上一条还在处理时再发,更容易马上看到「已排队 / 处理中」和思考中表情 |
45
- | ⚠️ **模型故障更好懂** | 欠费、无可用线路、503 等失败时,卡片更容易定格成中文说明,少卡在「正在召唤大模型」 |
46
- | 🃏 **答案卡 / 图片等** | 社区原有大段答案独立卡片、图片全路径修复等能力继续保留 |
44
+ ### 📦 v0.8.26 / 0.8.25 及更早
47
45
 
48
- ```bash
49
- npx -y @jeik/dingtalk-connector@0.8.28 install --force && openclaw gateway restart
50
- ```
46
+ 目标 ID 约束、发送人身份、答案卡、图片修复等——详见 [CHANGELOG.md](CHANGELOG.md)。
51
47
 
52
- ### 📦 v0.8.26 · 2026-07-25
48
+ ---
53
49
 
54
- 目标 ID 填写约束(保留 `==` 等后缀与大小写)。
50
+ ## 🚀 快速开始
55
51
 
56
- ### 📦 v0.8.25 · 2026-07-24
52
+ ### 要求
57
53
 
58
- 发送人岗位与身份信息;裸钉钉 ID 推导为单聊。
54
+ - **OpenClaw** 已安装并正常运行([官网](https://openclaw.ai/))
55
+ - **版本**:OpenClaw ≥ **2026.4.9**(`openclaw -v`)
56
+ - 与官方插件同 channel id(`dingtalk-connector`),`--force` 可直接覆盖,**无需先卸载**
57
+ - 安装/更新后**必须** `openclaw gateway restart`
59
58
 
60
- ### 📦 v0.8.21-fix49 及更早
59
+ 包名:[`@jeik/dingtalk-connector`](https://www.npmjs.com/package/@jeik/dingtalk-connector)
61
60
 
62
- 答案卡、图片全路径修复、流式定稿、中文错误映射等——详见 [CHANGELOG.md](CHANGELOG.md)。
61
+ ### A)npx 一键扫码安装(按顺序试)
63
62
 
64
- 完整说明:[CHANGELOG.md](CHANGELOG.md) · [FIXES.md](FIXES.md)
63
+ ```bash
64
+ # 1)装最新
65
+ npx @jeik/dingtalk-connector install --force && openclaw gateway restart
65
66
 
66
- ---
67
+ # 2)若装到的不是最新,指定版本号
68
+ npx @jeik/dingtalk-connector@0.8.30 install --force && openclaw gateway restart
67
69
 
68
- ## 增强功能
70
+ # 3)若仍装不了,强制走 npm 官方源
71
+ NPM_CONFIG_REGISTRY=https://registry.npmjs.org npx @jeik/dingtalk-connector@0.8.30 install --force && openclaw gateway restart
72
+ ```
69
73
 
70
- ### 🖼️ 图片发送:全面修复与增强
74
+ ### B)只装插件(凭证已配好时,按顺序试)
71
75
 
72
- > 🔥 **毫无死角** · 混合发图 · 全面碾压官方薄弱的图片机制
73
- > ✅ 对话发图 · ✅ message 直发 · ✅ markdown 嵌图 · ✅ 文图可分可合
76
+ ```bash
77
+ # 1)装最新
78
+ openclaw plugins install @jeik/dingtalk-connector --force && openclaw gateway restart
74
79
 
75
- 对官方图片发送做了**全路径修复增强**,多通道混合发图,全部显示正常:
80
+ # 2)若装到的不是最新,指定版本号
81
+ openclaw plugins install @jeik/dingtalk-connector@0.8.30 --force && openclaw gateway restart
76
82
 
77
- | | 发送方式 | 能力 |
78
- |--|----------|------|
79
- | 💬 | **普通对话回复** | Agent 用 `![注释](…)` 嵌图,定稿前自动上传 mediaId |
80
- | 📤 | **message · mediaUrl** | 机器人主动触发直发(本地 / 内网 / 公网) |
81
- | 📝 | **message · markdown 正文** | 正文嵌 `![](…)`(本地 / `file://` / 直链)与文字同条 |
82
- | ⚙️ | **文图策略可配** | 默认 📎 **分开**;`messageImageMd: true` → 🧩 **合并**一条 markdown |
83
+ # 3)若仍装不了,强制走 npm 官方源
84
+ NPM_CONFIG_REGISTRY=https://registry.npmjs.org openclaw plugins install @jeik/dingtalk-connector@0.8.30 --force && openclaw gateway restart
85
+ ```
83
86
 
84
- #### 🌐 路径与协议全覆盖
87
+ ### 本地 tgz / 源码(开发、离线)
85
88
 
86
- | | 类型 | 示例 |
87
- |--|------|------|
88
- | 🌍 | 公网直链 | `https://…` |
89
- | 🏠 | 内网直链 | `http://内网主机/…` |
90
- | 📁 | 本地绝对路径 | `/tmp/…` · `/root/…` |
91
- | 💾 | **`/mnt` 挂载盘** | 中文目录 · 共享盘 · SMB |
92
- | 🔗 | `file://` URI | `file:///mnt/…` · `file:///tmp/…` |
93
- | 🆔 | 已有 mediaId | `@lADP…` |
89
+ ```bash
90
+ git clone https://github.com/jeikl/dingtalk-openclaw-connector-fix-Community.git
91
+ cd dingtalk-openclaw-connector-fix-Community
92
+ npm install && npm run build && npm pack
93
+ openclaw plugins install ./jeik-dingtalk-connector-0.8.30.tgz --force && openclaw gateway restart
94
+ ```
94
95
 
95
- #### 🛡️ 细节兜底
96
+ ### 安装后自检
96
97
 
97
- - 📦 代码块 / 行内 code 路径 **不会误上传**(参数说明原文保留)
98
- - ⬇️ 远程图 **先下载再上传**;本地失败会 **`/tmp` 重试**
99
- - 🫧 + 下载链接 **同泡共存**:`![]` → mediaId,下载 URL 仍是原链
98
+ ```bash
99
+ openclaw -v # 2026.4.9
100
+ openclaw plugins list # 应看到 dingtalk-connector / @jeik/dingtalk-connector
101
+ # 发一条钉钉消息:应先「正在召唤大模型…」,再流式/答案卡
102
+ ```
100
103
 
101
- ### 🎨 AI Card 模板
104
+ ---
102
105
 
103
- - 支持自定义流式卡;**不填 `cardTemplateId` 默认**
104
- `0d2c84b3-12c1-473b-b14a-f329a7a102cd.schema`(含 📋 复制按钮等)
106
+ ## ⚙️ 配置
105
107
 
106
- ### ⚙️ 最小配置(够跑)
108
+ ### 最小配置(够跑)
107
109
 
108
110
  ```json
109
111
  "channels": {
@@ -121,8 +123,6 @@ npx -y @jeik/dingtalk-connector@0.8.28 install --force && openclaw gateway resta
121
123
 
122
124
  ### 最大化配置(展示用,默认值写全)
123
125
 
124
- 按单 Agent / 多 Agent 两种写法(值均为默认,**不等于额外能力**)。
125
-
126
126
  **单 Agent(顶层凭证):**
127
127
 
128
128
  ```json
@@ -197,12 +197,64 @@ npx -y @jeik/dingtalk-connector@0.8.28 install --force && openclaw gateway resta
197
197
  | `answerCardTemplateId` | string | `d246b7f5-1783-4e9b-bb46-bef52d63050e.schema` | 答案静态卡模板 |
198
198
  | `messageAnswerCard` | boolean | **true** | message 工具正文走答案静态卡;`false`=普通消息 |
199
199
  | `messageImageMd` | boolean | **false** | message 图文:`false` 文图分开;`true` 可合并 markdown |
200
+ | `debug` | boolean | **false** | 详细诊断日志(连接/图片 mediaId/引用回填/队列)。**日常关闭**;排障时顶层或某账号设 `true`,改完 `openclaw gateway restart` |
200
201
 
201
202
  > `answerCard` = 对话流式收尾;`messageAnswerCard` = message 工具外发。
202
203
 
204
+ **临时开排障日志示例:**
205
+
206
+ ```json
207
+ "channels": {
208
+ "dingtalk-connector": {
209
+ "enabled": true,
210
+ "clientId": "…",
211
+ "clientSecret": "…",
212
+ "debug": true
213
+ }
214
+ }
215
+ ```
216
+
203
217
  ---
204
218
 
205
- ## 🎯 回复标记 + 答案卡 + 工具进度(核心增强)
219
+ ## 增强功能
220
+
221
+ ### 🖼️ 图片发送:全面修复与增强
222
+
223
+ > 🔥 **毫无死角** · 混合发图 · 全面碾压官方薄弱的图片机制
224
+ > ✅ 对话发图 · ✅ message 直发 · ✅ markdown 嵌图 · ✅ 文图可分可合
225
+
226
+ 对官方图片发送做了**全路径修复增强**,多通道混合发图,全部显示正常:
227
+
228
+ | | 发送方式 | 能力 |
229
+ |--|----------|------|
230
+ | 💬 | **普通对话回复** | Agent 用 `![注释](…)` 嵌图,定稿前自动上传 mediaId |
231
+ | 📤 | **message · mediaUrl** | 机器人主动触发直发(本地 / 内网 / 公网) |
232
+ | 📝 | **message · markdown 正文** | 正文嵌 `![](…)`(本地 / `file://` / 直链)与文字同条 |
233
+ | ⚙️ | **文图策略可配** | 默认 📎 **分开**;`messageImageMd: true` → 🧩 **合并**一条 markdown |
234
+
235
+ #### 🌐 路径与协议全覆盖
236
+
237
+ | | 类型 | 示例 |
238
+ |--|------|------|
239
+ | 🌍 | 公网直链 | `https://…` |
240
+ | 🏠 | 内网直链 | `http://内网主机/…` |
241
+ | 📁 | 本地绝对路径 | `/tmp/…` · `/root/…` |
242
+ | 💾 | **`/mnt` 挂载盘** | 中文目录 · 共享盘 · SMB |
243
+ | 🔗 | `file://` URI | `file:///mnt/…` · `file:///tmp/…` |
244
+ | 🆔 | 已有 mediaId | `@lADP…` |
245
+
246
+ #### 🛡️ 细节兜底
247
+
248
+ - 📦 代码块 / 行内 code 路径 **不会误上传**(参数说明原文保留)
249
+ - ⬇️ 远程图 **先下载再上传**;本地失败会 **`/tmp` 重试**
250
+ - 🫧 图 + 下载链接 **同泡共存**:`![]` → mediaId,下载 URL 仍是原链
251
+
252
+ ### 🎨 AI Card 模板
253
+
254
+ - ✨ 支持自定义流式卡;**不填 `cardTemplateId` 默认**
255
+ `0d2c84b3-12c1-473b-b14a-f329a7a102cd.schema`(含 📋 复制按钮等)
256
+
257
+ ### 🎯 回复标记 + 答案卡 + 工具进度
206
258
 
207
259
  这套机制让钉钉侧的「过程 → 最终答案」渲染更干净、更稳定,规避钉钉流式 AI Card 的官方渲染 bug:
208
260
 
@@ -236,83 +288,12 @@ npx -y @jeik/dingtalk-connector@0.8.28 install --force && openclaw gateway resta
236
288
 
237
289
  | 项目 | 说明 |
238
290
  |------|------|
239
- | 基础版本 | 官方 v0.8.20,功能完全一致 |
240
- | 修复内容 | 官方一直不修的 Bug(见上方最近修复) |
291
+ | 基础版本 | 对齐官方 **v0.8.24** 长连接能力,并叠加本仓库独有优化 |
292
+ | 修复内容 | 连接假死、消息丢失、错误卡卡住、图片灰图等实际使用问题 |
241
293
  | 维护方式 | 社区维护,持续跟进官方更新 |
242
294
 
243
295
  ---
244
296
 
245
- ## 安装与要求
246
-
247
- 开始之前,请确保:
248
-
249
- - **OpenClaw**:已安装并正常运行。详情请访问 [OpenClaw 官网](https://openclaw.ai/)
250
- - **版本要求**:OpenClaw ≥ **2026.4.9**,通过 `openclaw -v` 查看
251
-
252
- > 如低于此版本,执行 `npm install -g openclaw` 升级。
253
-
254
- ---
255
-
256
- ## 安装
257
-
258
- > 与官方插件同 channel id(`dingtalk-connector`),`--force` 直接覆盖更新,**无需先卸载**官方版或旧版。
259
- > **当前稳定版:`0.8.21-fix31`**(npm `latest` 已指向本版)。安装/更新后**必须** `openclaw gateway restart`。
260
-
261
- ### 方式一:npm(推荐)
262
-
263
- 包名:[`@jeik/dingtalk-connector`](https://www.npmjs.com/package/@jeik/dingtalk-connector)
264
-
265
- **1)一键扫码安装**(推荐:创建机器人 → 取凭证 → 装插件 → 写配置):
266
-
267
- ```bash
268
- npx -y @jeik/dingtalk-connector install
269
-
270
- # 已有 dingtalk-connector / 装不上时强制覆盖
271
- npx -y @jeik/dingtalk-connector install --force
272
- ```
273
-
274
- **2)只装插件**(凭证已配好,或走手动配置文档):
275
-
276
- ```bash
277
- openclaw plugins install @jeik/dingtalk-connector --force
278
- openclaw gateway restart
279
- ```
280
-
281
- **3)升级到最新版:**
282
-
283
- ```bash
284
- openclaw plugins install @jeik/dingtalk-connector --force
285
- openclaw gateway restart
286
- ```
287
-
288
- ### 方式二:本地 tgz / 源码构建(开发、离线、预发验证)
289
-
290
- ```bash
291
- git clone https://github.com/jeikl/dingtalk-openclaw-connector-fix-Community.git
292
- cd dingtalk-openclaw-connector-fix-Community
293
- # 可选:钉到某次发布(这是 git tag,不是分支;clone 后可直接 checkout)
294
- # git fetch --tags && git checkout v0.8.21-fix31
295
-
296
- npm install && npm run build && npm pack
297
- # → jeik-dingtalk-connector-0.8.21-fix31.tgz
298
-
299
- openclaw plugins install ./jeik-dingtalk-connector-0.8.21-fix31.tgz --force
300
- openclaw gateway restart
301
- ```
302
-
303
- > 国内若 clone 慢,可用镜像前缀,例如:
304
- > `git clone https://ghfast.top/https://github.com/jeikl/dingtalk-openclaw-connector-fix-Community.git`
305
-
306
- ### 安装后自检
307
-
308
- ```bash
309
- openclaw -v # OpenClaw ≥ 2026.4.9
310
- openclaw plugins list # 应看到 dingtalk-connector / @jeik/dingtalk-connector
311
- # 发一条钉钉消息:应先出现「🦸 正在召唤大模型…」,再进入流式回复
312
- ```
313
-
314
- ---
315
-
316
297
  ## 使用指南
317
298
 
318
299
  [OpenClaw 钉钉官方插件使用指南](https://alidocs.dingtalk.com/i/nodes/2Amq4vjg89GEno0zfPqoPGqdV3kdP0wQ?utm_scene=team_space)
@@ -1,4 +1,15 @@
1
+ import { r as isDingtalkDebug } from "./logger-CnBTcwyq.mjs";
1
2
  //#region src/utils/background-work.ts
3
+ /**
4
+ * 后台任务引用计数。
5
+ *
6
+ * 钉钉 WS 回调只「入队」后就返回,真正的 AI 跑在 sessionQueues 后台 Promise 里。
7
+ * 若只在 WS 回调里 markMessageProcessingStart/End,会在入队后立刻 End,
8
+ * 长任务(尤其 503 重试)期间心跳超时会触发幽灵重连 → 后续消息「网关没反应」。
9
+ *
10
+ * 用法:session 队列任务创建后 trackBackgroundWork(taskPromise)。
11
+ * connection 层订阅计数变化,在 count>0 时保持处理中 keepAlive。
12
+ */
2
13
  let activeCount = 0;
3
14
  const listeners = /* @__PURE__ */ new Set();
4
15
  function getActiveBackgroundWorkCount() {
@@ -31,7 +42,7 @@ function trackBackgroundWork(work, label) {
31
42
  notify();
32
43
  };
33
44
  work.then(done, done);
34
- if (label && typeof process !== "undefined" && process.env?.DINGTALK_DEBUG_QUEUE === "1") {
45
+ if (label && typeof process !== "undefined" && isDingtalkDebug()) {
35
46
  console.log(`[DingTalk][bgWork] +1 label=${label} count=${activeCount}`);
36
47
  work.finally(() => {
37
48
  console.log(`[DingTalk][bgWork] -1 label=${label} count=${activeCount}`);
@@ -0,0 +1,2 @@
1
+ import { r as rememberCardContent } from "./card-content-cache-DT_OYThh.mjs";
2
+ export { rememberCardContent };
@@ -1,4 +1,18 @@
1
+ import { r as isDingtalkDebug } from "./logger-CnBTcwyq.mjs";
1
2
  //#region src/services/messaging/card-content-cache.ts
3
+ /**
4
+ * AI / 交互卡片正文缓存
5
+ *
6
+ * 钉钉「引用 AI 卡片」时,回调里的 repliedMsg 经常是:
7
+ * { msgType: "interactiveCard", content: {} 或仅有 templateId }
8
+ * 不带可读正文 → 模型侧只看到 `[引用] [interactiveCard消息]`。
9
+ *
10
+ * 对策:我们在 finishAICard / 定稿时把 outTrackId → 正文 记下来;
11
+ * 引用时仅用载荷里的 outTrackId / msgId 等精确回填。
12
+ * 默认不做「会话最近一条」兜底(会把错误卡片正文当成用户引用的内容)。
13
+ *
14
+ * 内存 LRU + TTL,进程重启后清空(可接受)。
15
+ */
2
16
  const MAX_ENTRIES = 300;
3
17
  /** 默认保留 7 天(引用历史卡片) */
4
18
  const DEFAULT_TTL_MS = 10080 * 60 * 1e3;
@@ -39,7 +53,7 @@ function rememberCardContent(params) {
39
53
  byConversation.set(cid, next);
40
54
  byKey.set(normalizeKey("convRef", `${cid}|${ref}`), entry);
41
55
  }
42
- try {
56
+ if (isDingtalkDebug()) try {
43
57
  console.log(`[DingTalk][CardCache] 已缓存 | outTrack=${params.outTrackId || "-"} conv=${params.conversationId || "-"} len=${text.length}`);
44
58
  } catch {}
45
59
  }
@@ -0,0 +1,2 @@
1
+ import { a as uploadLargeFileByChunks } from "./chunk-upload-CIU5G_ay.mjs";
2
+ export { uploadLargeFileByChunks };
@@ -1,4 +1,4 @@
1
- import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
1
+ import { t as createLogger } from "./logger-CnBTcwyq.mjs";
2
2
  import { n as dingtalkOapiHttp } from "./http-client-DFWZgO1n.mjs";
3
3
  import * as fs from "fs";
4
4
  import * as path from "path";
@@ -1,6 +1,6 @@
1
- import { t as createLogger } from "./logger-mZ9OSbmD.mjs";
1
+ import { t as createLogger } from "./logger-CnBTcwyq.mjs";
2
2
  import { r as dingtalkUploadHttp } from "./http-client-DFWZgO1n.mjs";
3
- import { t as CHUNK_CONFIG } from "./chunk-upload-BE71drPg.mjs";
3
+ import { t as CHUNK_CONFIG } from "./chunk-upload-CIU5G_ay.mjs";
4
4
  import * as fs from "fs";
5
5
  import * as path from "path";
6
6
  import FormData from "form-data";
@@ -48,7 +48,7 @@ async function uploadMediaToDingTalk(filePath, mediaType, oapiToken, maxSize = 2
48
48
  if ((mediaType === "video" || mediaType === "file") && fileSize > CHUNK_CONFIG.SIZE_THRESHOLD) {
49
49
  log?.info?.(`文件超过 20MB,使用分块上传:${absPath} (${fileSizeMB}MB)`);
50
50
  try {
51
- const { uploadLargeFileByChunks } = await import("./chunk-upload-PwhogwfE.mjs");
51
+ const { uploadLargeFileByChunks } = await import("./chunk-upload-BKM1y4Ba.mjs");
52
52
  const downloadCode = await uploadLargeFileByChunks(absPath, mediaType, oapiToken, debugEnabled);
53
53
  if (downloadCode) {
54
54
  log?.info?.(`分块上传成功:${absPath}, download_code: ${downloadCode}`);
@@ -1,2 +1,2 @@
1
- import { i as VIDEO_MARKER_PATTERN, r as FILE_MARKER_PATTERN, t as AUDIO_MARKER_PATTERN } from "./common-BZiVgd3A.mjs";
1
+ import { i as VIDEO_MARKER_PATTERN, r as FILE_MARKER_PATTERN, t as AUDIO_MARKER_PATTERN } from "./common-BfIYs1yC.mjs";
2
2
  export { AUDIO_MARKER_PATTERN, FILE_MARKER_PATTERN, VIDEO_MARKER_PATTERN };
@@ -1,5 +1,5 @@
1
1
  import { i as checkAndMarkDingtalkMessage } from "./utils-legacy-DQ3HeGHh.mjs";
2
- import { n as onBackgroundWorkCountChange, t as getActiveBackgroundWorkCount } from "./background-work-Cxb4b-91.mjs";
2
+ import { n as onBackgroundWorkCountChange, t as getActiveBackgroundWorkCount } from "./background-work-DkaSDqWk.mjs";
3
3
  import * as fs from "fs";
4
4
  //#region src/core/connection.ts
5
5
  /**
@@ -64,7 +64,7 @@ let _connectionNoticePrinted = false;
64
64
  function printConnectionNoticeOnce() {
65
65
  if (_connectionNoticePrinted) return;
66
66
  _connectionNoticePrinted = true;
67
- console.log("[dingtalk-connector] ℹ️ 上游 dingtalk-stream 噪音已过滤;须 OPEN+REGISTERED 才报 ready,REGISTERED 超时会强制重连(禁止假 online)。正常运行不应出现 ≤30s 周期性硬重连。");
67
+ console.log("[dingtalk-connector] ℹ️ 上游 dingtalk-stream 噪音已过滤;OPEN 成功即可上线,REGISTERED 尽量等待;若环境不推 REGISTERED,首条消息会确认订阅。正常运行不应出现 ≤30s 周期性硬重连。");
68
68
  }
69
69
  async function monitorSingleAccount(opts) {
70
70
  const { cfg, account, runtime, abortSignal, messageHandler, onStatusChange } = opts;
@@ -72,7 +72,8 @@ async function monitorSingleAccount(opts) {
72
72
  silenceDingtalkStreamConsoleNoise();
73
73
  const clawdbotConfig = cfg;
74
74
  const log = runtime?.log;
75
- const { createLoggerFromConfig } = await import("./logger-BmJkQkm1.mjs");
75
+ const { createLoggerFromConfig, setDingtalkDebug } = await import("./logger-D8H2IcYz.mjs");
76
+ if (account.config?.debug) setDingtalkDebug(true);
76
77
  const logger = createLoggerFromConfig(account.config, `DingTalk:${accountId}`);
77
78
  if (!account.clientId || !account.clientSecret) throw new Error(`[DingTalk][${accountId}] Missing credentials: clientId=${account.clientId ? "present" : "MISSING"}, clientSecret=${account.clientSecret ? "present" : "MISSING"}. Please check your configuration in channels.dingtalk-connector.`);
78
79
  const clientIdStr = String(account.clientId);
@@ -169,13 +170,14 @@ async function monitorSingleAccount(opts) {
169
170
  return Math.min(exponentialDelay + jitter, MAX_BACKOFF_DELAY);
170
171
  }
171
172
  /**
172
- * 等待钉钉 Stream 真正可收消息。
173
+ * 等待钉钉 Stream 可服务。
173
174
  *
174
- * dingtalk-stream client.connect() 在创建 WebSocket 后立刻 resolve,
175
- * **不等** socket open,更不等服务端 SYSTEM/REGISTERED。
175
+ * - OPEN:必须成功,否则 throw
176
+ * - REGISTERED:尽量等待;超时则 **降级 OPEN 上线**(不 throw)
176
177
  *
177
- * 顺序:socket OPEN SYSTEM topic=REGISTERED(client.registered=true)
178
- * 任一步失败均 throw,禁止 OPEN-only ready
178
+ * 原因(生产日志):部分环境 socket OPEN CALLBACK 能收消息,
179
+ * 但长期收不到 SYSTEM/REGISTERED client.registered 不被置 true
180
+ * 若硬性失败会触发网关 auto-restart 风暴,而旧连接/半连接其实仍在处理消息。
179
181
  */
180
182
  async function waitForStreamReady(timeoutMs = STREAM_READY_TIMEOUT_MS) {
181
183
  const started = Date.now();
@@ -209,7 +211,7 @@ async function monitorSingleAccount(opts) {
209
211
  }
210
212
  if (client.registered === true) {
211
213
  logger.info(`✅ Stream 已就绪(OPEN + REGISTERED),耗时 ${Date.now() - started}ms`);
212
- return;
214
+ return { registered: true };
213
215
  }
214
216
  if (await new Promise((resolve) => {
215
217
  const socket = client.socket;
@@ -245,9 +247,10 @@ async function monitorSingleAccount(opts) {
245
247
  socket.on("message", onMessage);
246
248
  }) || client.registered === true) {
247
249
  logger.info(`✅ Stream 已就绪(OPEN + REGISTERED),耗时 ${Date.now() - started}ms`);
248
- return;
250
+ return { registered: true };
249
251
  }
250
- throw new Error(`SYSTEM/REGISTERED 超时(${timeoutMs}ms):socket 已 OPEN 但订阅未生效,禁止报 connected`);
252
+ logger.warn(`⚠️ 未观察到 SYSTEM/REGISTERED(${timeoutMs}ms),socket 已 OPEN 降级上线。 收到首条机器人消息后会记为「订阅 empirically 生效」。`);
253
+ return { registered: false };
251
254
  }
252
255
  /**
253
256
  * connect 之后、wait ready 之前挂上 pong/message/close。
@@ -261,26 +264,27 @@ async function monitorSingleAccount(opts) {
261
264
  setupCloseListener();
262
265
  }
263
266
  /**
264
- * 单次:connect + 挂 listener + 等 OPEN/REGISTERED
265
- * 失败 throw,不报 connected。
267
+ * 单次:connect + 挂 listener + 等 OPENREGISTERED 可选)。
268
+ * 仅 OPEN 失败 throw;REGISTERED 超时则降级成功。
266
269
  */
267
- async function connectAndWaitRegistered() {
270
+ async function connectAndWaitReady() {
268
271
  await client.connect();
269
272
  attachSocketLifecycleListeners();
270
- await waitForStreamReady(STREAM_READY_TIMEOUT_MS);
271
- if (client.registered !== true) throw new Error("connect 后 client.registered 仍为 false");
273
+ return await waitForStreamReady(STREAM_READY_TIMEOUT_MS);
272
274
  }
273
275
  /**
274
- * 直到 REGISTERED 成功或次数用尽。
275
- * 用于初次启动与硬重连:绝不在未订阅时对外 connected=true。
276
+ * 建立可服务连接。
277
+ * - 优先拿到 REGISTERED
278
+ * - OPEN 成功即可上线(避免环境不推 REGISTERED 时 restart 风暴)
279
+ * - 仅在 OPEN 都失败时多轮重试
276
280
  */
277
281
  async function ensureRegisteredConnection(maxAttempts = REGISTERED_CONNECT_MAX_ATTEMPTS) {
278
282
  let lastError;
279
283
  for (let attempt = 1; attempt <= maxAttempts; attempt++) {
280
- if (isStopped) throw new Error("连接已停止,中止 REGISTERED 等待");
284
+ if (isStopped) throw new Error("连接已停止,中止 Stream 建立");
281
285
  try {
282
286
  if (attempt > 1) {
283
- logger.warn(`🔄 REGISTERED 未就绪,强制重连 ${attempt}/${maxAttempts}…`);
287
+ logger.warn(`🔄 Stream 建立重试 ${attempt}/${maxAttempts}…`);
284
288
  try {
285
289
  if (client.socket) await client.disconnect();
286
290
  } catch (discErr) {
@@ -288,18 +292,18 @@ async function monitorSingleAccount(opts) {
288
292
  }
289
293
  const delay = Math.min(1e3 * attempt, 5e3);
290
294
  await new Promise((r) => setTimeout(r, delay));
291
- } else logger.info(`⏳ 建立 Stream 并等待 REGISTERED(单次超时 ${STREAM_READY_TIMEOUT_MS}ms,最多 ${maxAttempts} 次)…`);
292
- await connectAndWaitRegistered();
293
- noteSocketAlive("registered-ok");
295
+ } else logger.info(`⏳ 建立 Stream(OPEN 必须;REGISTERED 尽量等待 ${STREAM_READY_TIMEOUT_MS}ms)…`);
296
+ const ready = await connectAndWaitReady();
297
+ noteSocketAlive(ready.registered ? "registered-ok" : "open-degraded");
294
298
  connectionEstablishedTime = Date.now();
295
- logger.info(`✅ 订阅已生效 registered=true(attempt ${attempt}/${maxAttempts}, socket=${client.socket?.readyState})`);
299
+ logger.info(`✅ Stream 可服务(attempt ${attempt}/${maxAttempts}, registered=${ready.registered || Boolean(client.registered)}, socket=${client.socket?.readyState})`);
296
300
  return;
297
301
  } catch (err) {
298
302
  lastError = err instanceof Error ? err : new Error(String(err));
299
- logger.warn(`⚠️ Stream 就绪失败 attempt=${attempt}/${maxAttempts}: ${lastError.message}`);
303
+ logger.warn(`⚠️ Stream 建立失败 attempt=${attempt}/${maxAttempts}: ${lastError.message}`);
300
304
  }
301
305
  }
302
- throw new Error(`钉钉 Stream 在 ${maxAttempts} 次尝试后仍未 REGISTERED,拒绝假 connected。 最后错误: ${lastError?.message || "unknown"}`);
306
+ throw new Error(`钉钉 Stream 在 ${maxAttempts} 次尝试后仍无法 OPEN。 最后错误: ${lastError?.message || "unknown"}`);
303
307
  }
304
308
  /** 统一重连函数,带指数退避(无限重连) */
305
309
  async function doReconnect(immediate = false) {
@@ -506,21 +510,19 @@ async function monitorSingleAccount(opts) {
506
510
  receivedCount++;
507
511
  lastMessageTime = Date.now();
508
512
  noteSocketAlive("inbound-callback");
513
+ if (client.registered !== true) {
514
+ client.registered = true;
515
+ logger.info(`✅ 收到机器人 CALLBACK,视为订阅已生效(此前可能未观察到 SYSTEM/REGISTERED)`);
516
+ }
509
517
  onStatusChange?.({ lastInboundAt: Date.now() });
510
518
  const messageId = res.headers?.messageId;
511
- const timestamp = (/* @__PURE__ */ new Date()).toISOString();
512
- logger.info(`\n========== 收到新消息 ==========`);
513
- logger.info(`时间:${timestamp}`);
514
- logger.info(`MessageId: ${messageId || "N/A"}`);
515
- logger.info(`Headers: ${JSON.stringify(res.headers || {})}`);
516
- logger.info(`Data 长度:${res.data?.length || 0} 字符`);
517
519
  let acked = false;
518
520
  const ackMessage = (why) => {
519
521
  if (acked || !messageId) return;
520
522
  try {
521
523
  client.socketCallBackResponse(messageId, { success: true });
522
524
  acked = true;
523
- logger.info(`✅ 已确认回调 messageId=${messageId} (${why})`);
525
+ logger.debug(`ACK messageId=${messageId} (${why})`);
524
526
  } catch (ackErr) {
525
527
  logger.warn(`确认回调失败: ${ackErr?.message || ackErr}`);
526
528
  }
@@ -531,37 +533,30 @@ async function monitorSingleAccount(opts) {
531
533
  try {
532
534
  data = JSON.parse(res.data);
533
535
  } catch (parseError) {
534
- logger.error("Failed to parse response data as JSON:", {
535
- error: parseError instanceof Error ? parseError.message : String(parseError),
536
- rawData: typeof res.data === "string" ? res.data.substring(0, 500) : res.data,
537
- dataType: typeof res.data
538
- });
539
- throw new Error(`Invalid JSON response from DingTalk API. Error: ${parseError instanceof Error ? parseError.message : String(parseError)}. Raw data (first 100 chars): ${String(res.data).substring(0, 100)}`);
536
+ logger.error(`JSON 解析失败: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
537
+ throw new Error(`Invalid JSON from DingTalk: ${parseError instanceof Error ? parseError.message : String(parseError)}`);
540
538
  }
541
- logger.info(`\n----- 消息详情 -----`);
542
- logger.info(`消息类型:${data.msgtype || "unknown"}`);
543
- logger.info(`会话类型:${data.conversationType === "1" ? "DM (单聊)" : data.conversationType === "2" ? "Group (群聊)" : data.conversationType}`);
544
- logger.info(`发送者:${data.senderNick || "unknown"} (${data.senderStaffId || data.senderId || "unknown"})`);
545
- logger.info(`会话 ID: ${data.conversationId || "N/A"}`);
546
- logger.info(`消息 ID: ${data.msgId || "N/A"}`);
547
- logger.info(`SessionWebhook: ${data.sessionWebhook ? "已提供" : "未提供"}`);
548
- logger.info(`RobotCode: ${data.robotCode || account.config?.clientId || "N/A"}`);
549
- if (data.chatbotUserId || data.chatbotCorpId) console.log(`[DingTalk:${accountId}] [BotIdentity] accountId=${accountId} chatbotUserId=${data.chatbotUserId || "N/A"} chatbotCorpId=${data.chatbotCorpId || "N/A"}`);
550
539
  const businessMsgId = data.msgId;
540
+ const convType = data.conversationType === "1" ? "DM" : data.conversationType === "2" ? "Group" : String(data.conversationType ?? "?");
541
+ const preview = (data.text?.content || "").trim().slice(0, 80) || data.msgtype || "(no text)";
542
+ logger.info(`📩 入站 ${convType} from=${data.senderNick || data.senderStaffId || data.senderId || "?"} msg=${businessMsgId || messageId || "-"} preview=${JSON.stringify(preview)}`);
543
+ logger.debug(`Headers: ${JSON.stringify(res.headers || {})}`);
544
+ logger.debug(`字段: ${Object.keys(data).join(",")}`);
545
+ if (data.chatbotUserId || data.chatbotCorpId) {
546
+ const key = `${accountId}:${data.chatbotUserId || ""}:${data.chatbotCorpId || ""}`;
547
+ if (!globalThis.__dingtalkBotIdentityLogged) globalThis.__dingtalkBotIdentityLogged = /* @__PURE__ */ new Set();
548
+ const seen = globalThis.__dingtalkBotIdentityLogged;
549
+ if (!seen.has(key)) {
550
+ seen.add(key);
551
+ console.log(`[DingTalk:${accountId}] [BotIdentity] chatbotUserId=${data.chatbotUserId || "N/A"} chatbotCorpId=${data.chatbotCorpId || "N/A"}`);
552
+ }
553
+ }
551
554
  if (checkAndMarkDingtalkMessage(accountId, messageId, businessMsgId)) {
552
555
  processedCount++;
553
556
  ackMessage("duplicate");
554
- logger.warn(`⚠️ 检测到重复消息,跳过:protocol=${messageId || "-"} business=${businessMsgId || "-"} (${processedCount}/${receivedCount})`);
555
- logger.info(`========== 消息处理结束(重复) ==========\n`);
557
+ logger.warn(`⚠️ 重复消息已跳过 protocol=${messageId || "-"} business=${businessMsgId || "-"}`);
556
558
  return;
557
559
  }
558
- let contentPreview = "N/A";
559
- if (data.text?.content) contentPreview = data.text.content.length > 100 ? data.text.content.substring(0, 100) + "..." : data.text.content;
560
- else if (data.content) contentPreview = JSON.stringify(data.content).substring(0, 100) + "...";
561
- logger.info(`消息内容预览:${contentPreview}`);
562
- logger.info(`完整数据字段:${Object.keys(data).join(", ")}`);
563
- logger.info(`----- 消息详情结束 -----\n`);
564
- logger.info(`🚀 开始处理消息...`);
565
560
  await messageHandler({
566
561
  accountId,
567
562
  config: account.config,
@@ -574,16 +569,12 @@ async function monitorSingleAccount(opts) {
574
569
  ackMessage("enqueued");
575
570
  noteSocketAlive("after-enqueue");
576
571
  processedCount++;
577
- logger.info(`✅ 消息处理完成 (${processedCount}/${receivedCount})`);
578
- logger.info(`========== 消息处理结束(成功) ==========\n`);
572
+ logger.info(`✅ 入队完成 (${processedCount}/${receivedCount})`);
579
573
  } catch (error) {
580
574
  processedCount++;
581
- const errorMsg = `❌ 处理消息异常 (${processedCount}/${receivedCount}): ${error?.message || "未知错误"}`;
582
- const errorStack = error?.stack || "无堆栈信息";
583
- logger.error(errorMsg);
584
- logger.error(`错误堆栈:\n${errorStack}`);
585
- if (!acked) logger.warn(`⚠️ 未 ACK messageId=${messageId || "N/A"},等待钉钉重投(避免网关永久丢消息)`);
586
- logger.info(`========== 消息处理结束(失败) ==========\n`);
575
+ logger.error(`❌ 入站处理失败 (${processedCount}/${receivedCount}): ${error?.message || "未知错误"}`);
576
+ if (error?.stack) logger.debug(error.stack);
577
+ if (!acked) logger.warn(`⚠️ 未 ACK messageId=${messageId || "N/A"},等待钉钉重投`);
587
578
  } finally {
588
579
  markMessageProcessingEnd();
589
580
  }
@@ -23,7 +23,7 @@ var entry_bundled_default = defineBundledChannelEntry({
23
23
  exportName: "setDingtalkRuntime"
24
24
  },
25
25
  async registerFull(api) {
26
- const { registerGatewayMethods } = await import("./gateway-methods-BkI1gKaC.mjs");
26
+ const { registerGatewayMethods } = await import("./gateway-methods-DQ6TwOAr.mjs");
27
27
  registerGatewayMethods(api);
28
28
  }
29
29
  });