@dingtalk-real-ai/dingtalk-connector 0.8.13-beta.0 → 0.8.13

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/CHANGELOG.md CHANGED
@@ -5,35 +5,41 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [0.8.13-beta.0] - 2026-04-06
8
+ ## [0.8.13] - 2026-04-08
9
9
 
10
10
  ### 修复 / Fixes
11
- - 🐛 **修复多账号场景下凭据未解析** - `sendText`/`sendMedia` 在多账号模式下 `clientId`/`clientSecret` 可能为 `SecretInput` 对象,导致 API 请求携带无效凭据。现已用 `resolveDingtalkAccount` 返回的已解析值覆盖原始 config
12
- **Fix unresolved credentials in sendText/sendMedia** - In multi-account mode, credentials could be `SecretInput` objects; now resolved values are used
11
+ - 🐛 **修复文件发送 mediaId 格式不一致导致静默失败** - `sendFileProactive` 在不同调用点传入 `cleanMediaId`(不带 `@`)和 `mediaId`(带 `@`),经实测钉钉 API 统一要求带 `@` 前缀。同时修复 catch 块吞掉异常导致外层误报成功
12
+ **Fix inconsistent mediaId format causing silent file send failure** - Unified to `@`-prefixed `mediaId`; fixed catch blocks swallowing errors
13
13
 
14
- - 🐛 **修复连接错误在 async 回调中无法传播** - `connection.ts` 错误处理使用 `throw` 抛出,但在 async 回调内无法被外层 Promise 捕获。改为 `reject(new Error(...))` 确保 400/401 等错误正确传播
15
- **Fix connection errors not propagating in async callback** - Changed `throw` to `reject()` inside async error handlers
14
+ - 🐛 **修复多账号场景下凭据未解析** - `sendText`/`sendMedia` 在多账号模式下 `clientId`/`clientSecret` 可能为 `SecretInput` 对象,现已用已解析值覆盖
15
+ **Fix unresolved credentials in multi-account mode** - Resolved values now override raw config
16
16
 
17
- - 🐛 **修复 QPS 限流后立即重试触发再次限流** - 收到 403 QpsLimit 后未更新 `lastUpdateTime`,导致节流检查立即放行。现已同步更新节流时间
18
- **Fix QPS rate limit immediate retry** - `lastUpdateTime` is now synced when skipping a QPS-limited update
17
+ - 🐛 **修复连接错误在 async 回调中无法传播** - 改为 `reject(new Error(...))` 确保 400/401 等错误正确传播
18
+ **Fix connection errors not propagating** - Changed `throw` to `reject()` inside async error handlers
19
19
 
20
- - 🐛 **修复 `resolveAllowFrom` 全局过滤误拦截群消息** - `allowFrom` 仅用于私聊白名单,群消息由 `groupAllowFrom` 在内部处理。将 `resolveAllowFrom` 改为返回空列表,禁用框架层全局过滤
21
- **Fix resolveAllowFrom global filter blocking group messages** - Returns `[]` to disable framework-level filtering; internal policy checks handle DM and group separately
20
+ - 🐛 **修复 QPS 限流后立即重试** - 收到 403 QpsLimit 后同步更新 `lastUpdateTime`
21
+ **Fix QPS rate limit immediate retry** - `lastUpdateTime` is now synced when skipping
22
+
23
+ - 🐛 **修复 `resolveAllowFrom` 全局过滤误拦截群消息** - 返回空列表禁用框架层全局过滤,群消息由内部 `groupAllowFrom` 处理
24
+ **Fix resolveAllowFrom blocking group messages** - Returns `[]` to disable framework-level filtering
22
25
 
23
26
  ### 新增 / Added
24
- - ✨ **消息路由支持 `group:`/`user:` 前缀** - `sendTextToDingTalk` 和 `sendMediaToDingTalk` 新增 `group:<id>` 和 `user:<id>` 格式解析,与 `gateway-methods.ts` 保持一致,兼容旧版裸 `cid` 前缀
25
- **Message routing supports `group:`/`user:` prefix targets** - Consistent with `gateway-methods.ts`, backward compatible with bare `cid` prefix
27
+ - ✨ **消息路由支持 `group:`/`user:` 前缀** - `sendTextToDingTalk` 和 `sendMediaToDingTalk` 新增前缀格式解析,兼容旧版裸 `cid` 前缀
28
+ **Message routing supports `group:`/`user:` prefix targets** - Backward compatible with bare `cid` prefix
26
29
 
27
30
  ### 改进 / Improvements
28
- - ✅ **兼容 pdf-parse v1v2 API** - `parsePdfFile` 自动检测 pdf-parse 导出格式,v2.x 使用 `PDFParse` 类,v1.x 使用 `default` 函数
29
- **Support both pdf-parse v1 and v2 API** - Auto-detects export format: v2.x class API or v1.x function API
31
+ - ✅ **兼容 pdf-parse v1/v2 API** - 自动检测导出格式,v2.x 使用 class API,v1.x 使用函数 API
32
+ **Support both pdf-parse v1 and v2 API** - Auto-detects export format
33
+
34
+ - ✅ **`enableMediaUpload`/`systemPrompt` 移至共享配置** - 支持多账号独立配置
35
+ **Move `enableMediaUpload`/`systemPrompt` to shared config** - Per-account configurable
30
36
 
31
- - ✅ **`enableMediaUpload`/`systemPrompt` 移至共享配置** - 这两个字段现在支持多账号模式下每个账号独立配置
32
- **Move `enableMediaUpload`/`systemPrompt` to shared config** - Now configurable per-account in multi-account mode
37
+ - ✅ **新增出站路由测试** - 覆盖 `group:`/`user:` 前缀解析和消息路由场景
38
+ **Add outbound routing tests** - Covers prefix parsing and routing scenarios
33
39
 
34
40
  ### 文档 / Documentation
35
41
  - 📝 **优化 README 安装验证说明** - 移除版本号硬编码,新增插件未加载时的警示提示
36
- **Improve README plugin verification instructions** - Removed hardcoded version, added warning when plugin is not loaded
42
+ **Improve README plugin verification instructions** - Removed hardcoded version, added warning
37
43
 
38
44
  ## [0.8.12] - 2026-04-01
39
45
 
package/README.en.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img alt="DingTalk" src="docs/images/dingtalk.svg" width="72" height="72" />
2
+ <img alt="DingTalk" src="https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/dingtalk.svg" width="72" height="72" />
3
3
  <h1>Official DingTalk OpenClaw Connector</h1>
4
4
  <p>Connect DingTalk bots to OpenClaw Gateway with AI Card streaming and session management</p>
5
5
 
@@ -135,13 +135,13 @@ You should see output similar to `✓ DingTalk Channel (vX.X.X) - loaded`.
135
135
  1. Go to [DingTalk Open Platform](https://open-dev.dingtalk.com/)
136
136
  2. Click **"Application Development"**
137
137
 
138
- ![Create Application](docs/images/image-1.png)
138
+ ![Create Application](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-1.png)
139
139
 
140
140
  #### 2.2 Add Bot Capability
141
141
 
142
142
  1. On the application details page, use the “one-click OpenClaw bot app” flow
143
143
 
144
- ![Create OpenClaw bot app](docs/images/image-2.png)
144
+ ![Create OpenClaw bot app](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-2.png)
145
145
 
146
146
  #### 2.3 Get Credentials
147
147
 
@@ -149,9 +149,9 @@ You should see output similar to `✓ DingTalk Channel (vX.X.X) - loaded`.
149
149
  2. Copy your **AppKey** (Client ID)
150
150
  3. Copy your **AppSecret** (Client Secret)
151
151
 
152
- ![Finish creation](docs/images/image-3.png)
152
+ ![Finish creation](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-3.png)
153
153
 
154
- ![Get credentials](docs/images/image-4.png)
154
+ ![Get credentials](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-4.png)
155
155
 
156
156
  > ⚠️ **Important**: Client ID and Client Secret are your bot’s unique credentials. Store them safely.
157
157
 
@@ -281,6 +281,29 @@ Both session routing/message policy options (including `pmpolicy` and `groupPoli
281
281
 
282
282
  ---
283
283
 
284
+ ### Invalid Config: additional properties
285
+
286
+ **Symptoms**:
287
+
288
+ ```
289
+ Problem:
290
+ - channels.dingtalk-connector: invalid config: must NOT have additional properties
291
+ ```
292
+
293
+ **Cause**: Your config file contains deprecated or renamed fields that are no longer recognized.
294
+
295
+ **Solution**: Open `openclaw.config.yaml` and remove any unsupported fields under `channels.dingtalk-connector`. Known fields to remove:
296
+
297
+ | Old Field | Notes |
298
+ |-----------|-------|
299
+ | `gatewayPassword` | Deprecated legacy field |
300
+ | `gatewayToken` | Deprecated legacy field |
301
+ | `dmHistoryLimit` | Removed in v0.8.9 (never implemented) |
302
+
303
+ The error message will indicate the exact field name. Remove it and restart.
304
+
305
+ ---
306
+
284
307
  ### HTTP 401 Error
285
308
 
286
309
  **Symptoms**: Error message shows "401 Unauthorized"
package/README.md CHANGED
@@ -1,5 +1,5 @@
1
1
  <div align="center">
2
- <img alt="DingTalk" src="docs/images/dingtalk.svg" width="72" height="72" />
2
+ <img alt="DingTalk" src="https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/dingtalk.svg" width="72" height="72" />
3
3
  <h1>钉钉 OpenClaw 官方连接器</h1>
4
4
  <p>将钉钉机器人连接到 OpenClaw Gateway,支持 AI Card 流式响应和会话管理</p>
5
5
 
@@ -167,13 +167,13 @@ openclaw plugins list
167
167
  1. 访问 [钉钉开放平台](https://open-dev.dingtalk.com/)
168
168
  2. 点击 **"应用开发"**
169
169
 
170
- ![创建应用](docs/images/image-1.png)
170
+ ![创建应用](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-1.png)
171
171
 
172
172
  #### 3.2 添加机器人能力
173
173
 
174
174
  1. 在应用详情页,点击 一键创建OpenClaw机器人应用
175
175
 
176
- ![创建OpenClaw机器人应用](docs/images/image-2.png)
176
+ ![创建OpenClaw机器人应用](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-2.png)
177
177
 
178
178
  #### 3.3 获取凭证
179
179
 
@@ -181,9 +181,9 @@ openclaw plugins list
181
181
  2. 复制你的 **AppKey**(Client ID)
182
182
  3. 复制你的 **AppSecret**(Client Secret)
183
183
 
184
- ![完成创建](docs/images/image-3.png)
184
+ ![完成创建](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-3.png)
185
185
 
186
- ![获取凭证](docs/images/image-4.png)
186
+ ![获取凭证](https://raw.githubusercontent.com/DingTalk-Real-AI/dingtalk-openclaw-connector/main/docs/images/image-4.png)
187
187
 
188
188
  > ⚠️ **重要**:Client ID和 Client Secret是机器人的唯一凭证。请合理保存。
189
189
 
@@ -314,6 +314,29 @@ openclaw logs --follow
314
314
 
315
315
  ---
316
316
 
317
+ ### 配置字段不合法(additional properties)
318
+
319
+ **症状**:
320
+
321
+ ```
322
+ Problem:
323
+ - channels.dingtalk-connector: invalid config: must NOT have additional properties
324
+ ```
325
+
326
+ **原因**:配置文件中包含已废弃或已重命名的字段,连接器不再识别。
327
+
328
+ **解决方案**:打开 `openclaw.config.yaml`,删除 `channels.dingtalk-connector` 下不再支持的字段。已知需要删除的旧字段:
329
+
330
+ | 旧字段 | 说明 |
331
+ |--------|------|
332
+ | `gatewayPassword` | 早期版本字段,已废弃 |
333
+ | `gatewayToken` | 早期版本字段,已废弃 |
334
+ | `dmHistoryLimit` | v0.8.9 移除(未实现) |
335
+
336
+ 错误信息会指出具体的字段名,删除后重启即可。
337
+
338
+ ---
339
+
317
340
  ### HTTP 401 错误
318
341
 
319
342
  **症状**:错误信息显示 "401 Unauthorized"
@@ -0,0 +1,62 @@
1
+ # Release Notes - v0.8.13
2
+
3
+ ## 🎉 新版本亮点 / Highlights
4
+
5
+ 本次更新聚焦**媒体发送可靠性**和**多账号稳定性**:修复了文件发送时 mediaId 格式不一致导致静默失败的问题,修复了多账号凭据解析错误、连接错误无法传播、QPS 限流重试等多个关键 Bug,同时新增 `group:`/`user:` 前缀路由支持。
6
+
7
+ This release focuses on **media sending reliability** and **multi-account stability**: fixes silent file sending failures caused by inconsistent mediaId format, resolves multi-account credential resolution errors, connection error propagation, QPS throttle retry issues, and adds `group:`/`user:` prefix routing support.
8
+
9
+ ## 🐛 修复 / Fixes
10
+
11
+ - **修复文件发送时 mediaId 格式不一致导致静默失败 / Fix inconsistent mediaId format causing silent file send failure**
12
+ `sendFileProactive` 在不同调用点传入了 `cleanMediaId`(不带 `@`)和 `mediaId`(带 `@`),经实测钉钉 API 统一要求带 `@` 前缀的 `mediaId`。同时修复了 `sendFileMessage`/`sendFileProactive` 的 catch 块吞掉异常导致外层误报成功的问题。
13
+ `sendFileProactive` was called with `cleanMediaId` (without `@`) in some places and `mediaId` (with `@`) in others. Testing confirmed DingTalk API requires the `@`-prefixed `mediaId`. Also fixed catch blocks swallowing errors, causing false success reports.
14
+
15
+ - **修复多账号场景下凭据未解析 / Fix unresolved credentials in multi-account mode**
16
+ `sendText`/`sendMedia` 在多账号模式下 `clientId`/`clientSecret` 可能为 `SecretInput` 对象,导致 API 请求携带无效凭据。现已用 `resolveDingtalkAccount` 返回的已解析值覆盖。
17
+ In multi-account mode, credentials could be `SecretInput` objects; now resolved values are used.
18
+
19
+ - **修复连接错误在 async 回调中无法传播 / Fix connection errors not propagating**
20
+ `connection.ts` 错误处理使用 `throw` 抛出,但在 async 回调内无法被外层 Promise 捕获。改为 `reject(new Error(...))` 确保 400/401 等错误正确传播。
21
+ Changed `throw` to `reject()` inside async error handlers for proper error propagation.
22
+
23
+ - **修复 QPS 限流后立即重试 / Fix QPS rate limit immediate retry**
24
+ 收到 403 QpsLimit 后未更新 `lastUpdateTime`,导致节流检查立即放行。现已同步更新。
25
+ `lastUpdateTime` is now synced when skipping a QPS-limited update.
26
+
27
+ - **修复 `resolveAllowFrom` 全局过滤误拦截群消息 / Fix resolveAllowFrom blocking group messages**
28
+ `allowFrom` 仅用于私聊白名单,群消息由 `groupAllowFrom` 在内部处理。将 `resolveAllowFrom` 改为返回空列表,禁用框架层全局过滤。
29
+ Returns `[]` to disable framework-level filtering; internal policy checks handle DM and group separately.
30
+
31
+ ## ✨ 功能改进 / Improvements
32
+
33
+ - **消息路由支持 `group:`/`user:` 前缀 / Message routing supports `group:`/`user:` prefix targets**
34
+ `sendTextToDingTalk` 和 `sendMediaToDingTalk` 新增 `group:<id>` 和 `user:<id>` 格式解析,兼容旧版裸 `cid` 前缀。
35
+ Now supports `group:<id>` and `user:<id>` prefixed targets, backward compatible with bare `cid` prefix.
36
+
37
+ - **兼容 pdf-parse v1/v2 API、共享配置字段扩展 / pdf-parse v1/v2 compatibility & shared config fields**
38
+ `parsePdfFile` 自动检测 pdf-parse 导出格式;`enableMediaUpload`/`systemPrompt` 移至共享配置,支持多账号独立配置。
39
+ Auto-detects pdf-parse export format; `enableMediaUpload`/`systemPrompt` now configurable per-account.
40
+
41
+ - **新增出站路由测试 / Add outbound routing tests**
42
+ 新增 `outbound-routing.test.ts`,覆盖 `group:`/`user:` 前缀解析和消息路由场景。
43
+ Added `outbound-routing.test.ts` covering prefix parsing and message routing scenarios.
44
+
45
+ ## 📥 安装升级 / Installation & Upgrade
46
+
47
+ ```bash
48
+ openclaw plugins install @dingtalk-real-ai/dingtalk-connector
49
+ openclaw plugins update dingtalk-connector
50
+ openclaw plugins install https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector.git
51
+ ```
52
+
53
+ ## 🔗 相关链接 / Related Links
54
+
55
+ - [完整变更日志 / Full Changelog](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/CHANGELOG.md)
56
+ - [使用文档 / Documentation](https://github.com/DingTalk-Real-AI/dingtalk-openclaw-connector/blob/main/README.md)
57
+
58
+ ---
59
+
60
+ **发布日期 / Release Date**:2026-04-08
61
+ **版本号 / Version**:v0.8.13
62
+ **兼容性 / Compatibility**:OpenClaw Gateway 0.4.0+
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "id": "dingtalk-connector",
3
3
  "name": "DingTalk Channel",
4
- "version": "0.8.13-beta.0",
4
+ "version": "0.8.13",
5
5
  "description": "DingTalk (钉钉) messaging channel via Stream mode with AI Card streaming",
6
6
  "author": "DingTalk Real Team",
7
7
  "main": "index.ts",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dingtalk-real-ai/dingtalk-connector",
3
- "version": "0.8.13-beta.0",
3
+ "version": "0.8.13",
4
4
  "description": "DingTalk (钉钉) channel connector — Stream mode with AI Card streaming",
5
5
  "main": "index.ts",
6
6
  "type": "module",
@@ -55,7 +55,7 @@
55
55
  "access": "public"
56
56
  },
57
57
  "dependencies": {
58
- "axios": "1.6.0",
58
+ "axios": "1.14.0",
59
59
  "dingtalk-stream": "2.1.4",
60
60
  "form-data": "^4.0.0",
61
61
  "zod": "^4.3.6"
@@ -49,7 +49,7 @@ import {
49
49
  processLocalImages,
50
50
  processVideoMarkers,
51
51
  processAudioMarkers,
52
- processFileMarkers
52
+ uploadAndReplaceFileMarkers
53
53
  } from "../services/media/index.ts";
54
54
  import { sendProactive, type AICardTarget } from "../services/messaging/index.ts";
55
55
  import { createAICardForTarget, streamAICard, type AICardInstance } from "../services/messaging/card.ts";
@@ -701,7 +701,7 @@ export async function downloadMediaByCode(
701
701
 
702
702
  const resp = await dingtalkHttp.post(
703
703
  `${DINGTALK_API}/v1.0/robot/messageFiles/download`,
704
- { downloadCode, robotCode: config.clientId },
704
+ { downloadCode, robotCode: String(config.clientId) },
705
705
  {
706
706
  headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
707
707
  timeout: 30_000,
@@ -733,7 +733,7 @@ export async function getFileDownloadUrl(
733
733
 
734
734
  const resp = await dingtalkHttp.post(
735
735
  `${DINGTALK_API}/v1.0/robot/messageFiles/download`,
736
- { downloadCode, robotCode: config.clientId },
736
+ { downloadCode, robotCode: String(config.clientId) },
737
737
  {
738
738
  headers: { 'x-acs-dingtalk-access-token': token, 'Content-Type': 'application/json' },
739
739
  timeout: 30_000,
@@ -1513,7 +1513,7 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
1513
1513
  true, // ✅ 使用主动 API 模式
1514
1514
  mediaTarget
1515
1515
  );
1516
- finalText = await processFileMarkers(
1516
+ finalText = await uploadAndReplaceFileMarkers(
1517
1517
  finalText,
1518
1518
  '',
1519
1519
  config,
@@ -43,7 +43,7 @@ import {
43
43
  processLocalImages,
44
44
  processVideoMarkers,
45
45
  processAudioMarkers,
46
- processFileMarkers,
46
+ uploadAndReplaceFileMarkers,
47
47
  } from "./services/media/index.ts";
48
48
 
49
49
 
@@ -314,7 +314,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
314
314
  true, // ✅ 使用主动 API 模式
315
315
  target
316
316
  );
317
- finalText = await processFileMarkers(
317
+ finalText = await uploadAndReplaceFileMarkers(
318
318
  finalText,
319
319
  '',
320
320
  account.config as DingtalkConfig,
@@ -29,9 +29,14 @@ export async function parseDocumentFile(filePath: string, log?: any): Promise<st
29
29
  }
30
30
 
31
31
  /**
32
- * 提取文件标记并发送文件消息
32
+ * 提取文件标记,上传文件到钉钉,并用文本替换标记。
33
+ *
34
+ * 注意:此函数只做「上传 + 文本替换」,不会发送独立的文件消息。
35
+ * 如果需要上传后再发送独立文件消息,请使用 media.ts 中的 processFileMarkers。
36
+ *
37
+ * 调用方:reply-dispatcher.ts、message-handler.ts(通过 media/index.ts 导入)
33
38
  */
34
- export async function processFileMarkers(
39
+ export async function uploadAndReplaceFileMarkers(
35
40
  content: string,
36
41
  sessionWebhook: string,
37
42
  config: DingtalkConfig,
@@ -607,7 +607,12 @@ export interface FileInfo {
607
607
  }
608
608
 
609
609
  /**
610
- * 提取文件标记并发送文件消息
610
+ * 提取文件标记,上传文件到钉钉,并发送独立的文件消息(webhook 或 proactive API)。
611
+ *
612
+ * 注意:此函数既做「上传」也做「发送」,是完整版的文件处理流程。
613
+ * 与 media/file.ts 中的 uploadAndReplaceFileMarkers 不同,后者只做上传+文本替换。
614
+ *
615
+ * 调用方:messaging.ts(直接 import media.ts)
611
616
  */
612
617
  export async function processFileMarkers(
613
618
  content: string,
@@ -672,11 +677,11 @@ export async function processFileMarkers(
672
677
  continue;
673
678
  }
674
679
 
675
- // 发送文件消息
680
+ // 发送文件消息(钉钉 API 统一要求带 @ 前缀的 mediaId)
676
681
  if (useProactiveApi && target) {
677
- await sendFileProactive(config, target, fileInfo, uploadResult.cleanMediaId, log);
682
+ await sendFileProactive(config, target, fileInfo, uploadResult.mediaId, log);
678
683
  } else {
679
- await sendFileMessage(config, sessionWebhook, fileInfo, uploadResult.downloadUrl, log);
684
+ await sendFileMessage(config, sessionWebhook, fileInfo, uploadResult.mediaId, log);
680
685
  }
681
686
  statusMessages.push(`✅ 文件已发送: ${fileName}`);
682
687
  log?.info?.(`${logPrefix} 文件处理完成: ${fileName}`);
@@ -771,7 +776,7 @@ export async function sendVideoProactive(
771
776
  };
772
777
 
773
778
  const body: any = {
774
- robotCode: config.clientId,
779
+ robotCode: String(config.clientId),
775
780
  msgKey: 'sampleVideo',
776
781
  msgParam: JSON.stringify(msgParam),
777
782
  };
@@ -873,7 +878,7 @@ export async function sendAudioProactive(
873
878
  };
874
879
 
875
880
  const body: any = {
876
- robotCode: config.clientId,
881
+ robotCode: String(config.clientId),
877
882
  msgKey: 'sampleAudio',
878
883
  msgParam: JSON.stringify(msgParam),
879
884
  };
@@ -942,6 +947,7 @@ async function sendFileMessage(
942
947
  }
943
948
  } catch (err: any) {
944
949
  log?.error?.(`发送文件消息异常: ${fileInfo.fileName}, 错误: ${err.message}`);
950
+ throw err;
945
951
  }
946
952
  }
947
953
 
@@ -960,14 +966,16 @@ export async function sendFileProactive(
960
966
  const { DINGTALK_API } = await import('../utils/index.ts');
961
967
 
962
968
  // 钉钉普通消息 API 的文件消息格式
969
+ const resolvedFileName = fileInfo.fileName || path.basename(fileInfo.path);
970
+ const resolvedFileType = fileInfo.fileType || resolvedFileName.split('.').pop() || 'file';
963
971
  const msgParam = {
964
972
  mediaId: mediaId,
965
- fileName: fileInfo.fileName,
966
- fileType: fileInfo.fileType,
973
+ fileName: resolvedFileName,
974
+ fileType: resolvedFileType,
967
975
  };
968
976
 
969
977
  const body: any = {
970
- robotCode: config.clientId,
978
+ robotCode: String(config.clientId),
971
979
  msgKey: 'sampleFile',
972
980
  msgParam: JSON.stringify(msgParam),
973
981
  };
@@ -994,6 +1002,7 @@ export async function sendFileProactive(
994
1002
  }
995
1003
  } catch (err: any) {
996
1004
  log?.error?.(`File[Proactive] 发送文件消息失败: ${fileInfo.fileName}, 错误: ${err.message}`);
1005
+ throw err;
997
1006
  }
998
1007
  }
999
1008
 
@@ -1109,9 +1118,7 @@ export async function processRawMediaPaths(
1109
1118
  };
1110
1119
 
1111
1120
  if (target) {
1112
- // 文件消息使用下载链接
1113
- // 文件消息使用媒体文件ID(cleanMediaId)通过主动API发送
1114
- await sendFileProactive(config, target, fileInfo, uploadResult.cleanMediaId, log);
1121
+ await sendFileProactive(config, target, fileInfo, uploadResult.mediaId, log);
1115
1122
  }
1116
1123
  statusMessages.push(`✅ 文件已发送: ${fileName}`);
1117
1124
  }
@@ -303,7 +303,7 @@ export async function finishAICard(
303
303
  `[DingTalk][AICard] 开始 finish,最终内容长度=${fixedContent.length}`,
304
304
  );
305
305
 
306
- await streamAICard(card, fixedContent, true, log);
306
+ await streamAICard(card, fixedContent, true, config, log);
307
307
 
308
308
  const body = {
309
309
  outTrackId: card.cardInstanceId,
@@ -229,7 +229,7 @@ export async function sendNormalToUser(
229
229
  try {
230
230
  const token = await getAccessToken(config);
231
231
  const body = {
232
- robotCode: config.clientId,
232
+ robotCode: String(config.clientId),
233
233
  userIds: userIdArray,
234
234
  msgKey: payload.msgKey,
235
235
  msgParam: JSON.stringify(payload.msgParam),
@@ -306,7 +306,7 @@ export async function sendNormalToGroup(
306
306
  try {
307
307
  const token = await getAccessToken(config);
308
308
  const body = {
309
- robotCode: config.clientId,
309
+ robotCode: String(config.clientId),
310
310
  openConversationId,
311
311
  msgKey: payload.msgKey,
312
312
  msgParam: JSON.stringify(payload.msgParam),
@@ -439,7 +439,7 @@ export async function sendAICardInternal(
439
439
  }
440
440
 
441
441
  // 7. 使用 finishAICard 设置内容
442
- await finishAICard(card, processedContent, log);
442
+ await finishAICard(card, processedContent, config, log);
443
443
 
444
444
  log?.info?.(
445
445
  `AI Card 发送成功: ${targetDesc}, cardInstanceId=${card.cardInstanceId}`,
@@ -767,8 +767,9 @@ export async function sendMediaToDingTalk(params: {
767
767
  // 获取文件扩展名作为 fileType
768
768
  const fileType = ext || "file";
769
769
 
770
- // 构建文件信息
770
+ // 构建文件信息(path 字段用于 sendFileProactive 中 fileName 的 fallback)
771
771
  const fileInfo = {
772
+ path: mediaUrl,
772
773
  fileName: fileName,
773
774
  fileType: fileType,
774
775
  };
@@ -904,7 +905,7 @@ async function sendProactiveInternal(
904
905
  try {
905
906
  const card = await createAICardForTarget(config, target, externalLog);
906
907
  if (card) {
907
- await finishAICard(card, content, externalLog);
908
+ await finishAICard(card, content, config, externalLog);
908
909
  return {
909
910
  ok: true,
910
911
  cardInstanceId: card.cardInstanceId,
@@ -956,7 +957,7 @@ async function sendProactiveInternal(
956
957
  }
957
958
 
958
959
  const body: any = {
959
- robotCode: config.clientId,
960
+ robotCode: String(config.clientId),
960
961
  msgKey: payload.msgKey,
961
962
  msgParam: JSON.stringify(payload.msgParam),
962
963
  };
@@ -2,6 +2,7 @@
2
2
  * HTTP 客户端配置模块
3
3
  *
4
4
  * 提供统一的 axios 实例,用于钉钉 API 请求。
5
+ * 所有钉钉 API 调用必须使用此模块导出的实例,禁止直接使用 axios 或 fetch。
5
6
  *
6
7
  * 使用方式:
7
8
  * ```typescript
@@ -13,7 +14,7 @@
13
14
 
14
15
  import axios, { type AxiosInstance } from 'axios';
15
16
 
16
- /** 钉钉专用 HTTP 客户端(30 秒超时) */
17
+ /** 钉钉新版 API 专用 HTTP 客户端(30 秒超时,用于 api.dingtalk.com) */
17
18
  export const dingtalkHttp: AxiosInstance = axios.create({
18
19
  timeout: 30000,
19
20
  headers: {