@dingtalk-real-ai/dingtalk-connector 0.8.3-beta.1 → 0.8.4
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 +4 -1
- package/README.md +32 -0
- package/index.ts +56 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +5 -2
- package/src/core/message-handler.ts +20 -2
- package/src/policy.ts +21 -8
- package/src/reply-dispatcher.ts +45 -50
- package/src/sdk/helpers.ts +1 -1
- package/src/services/media/audio.ts +1 -1
- package/src/services/media/chunk-upload.ts +18 -29
- package/src/services/media/common.ts +3 -3
- package/src/services/media/file.ts +1 -1
- package/src/services/media/image.ts +11 -3
- package/src/services/messaging.ts +1 -1
- package/src/types/index.ts +8 -1
- package/tsconfig.json +20 -0
package/CHANGELOG.md
CHANGED
|
@@ -5,9 +5,12 @@ 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.
|
|
8
|
+
## [0.8.4] - 2026-03-24
|
|
9
9
|
|
|
10
10
|
### 修复 / Fixes
|
|
11
|
+
- 🐛 **群聊消息处理崩溃** - 修复群聊时报错 `TypeError: Cannot read properties of undefined (reading 'config')` 导致 Agent 无法回复的问题。根因是 `src/policy.ts` 中 `resolveDingtalkGroupToolPolicy` 函数的参数签名与 OpenClaw SDK 的 `ChannelGroupContext` 接口不匹配,函数期望接收 `account: ResolvedDingtalkAccount`,但框架实际传入 `{ cfg, groupId, accountId, ... }`,导致 `account` 为 `undefined`。现已修正参数签名,内部通过 `resolveDingtalkAccount()` 正确获取账号信息。单聊不受影响。
|
|
12
|
+
**Group chat message processing crash** - Fixed `TypeError: Cannot read properties of undefined (reading 'config')` crash in group chats. Root cause: `resolveDingtalkGroupToolPolicy` in `src/policy.ts` had a parameter signature mismatch with the OpenClaw SDK's `ChannelGroupContext` interface. Fixed by correcting the parameter signature and resolving the account internally via `resolveDingtalkAccount()`. Direct messages were unaffected.
|
|
13
|
+
|
|
11
14
|
- 🐛 **兼容旧版 OpenClaw Gateway(createPluginRuntimeStore 缺失)** - 修复在旧版 OpenClaw Gateway 上加载插件时报错 `TypeError: (0 , _pluginSdk.createPluginRuntimeStore) is not a function` 的问题。根因是 `src/runtime.ts` 直接从 `openclaw/plugin-sdk` 导入 `createPluginRuntimeStore`,而该函数在旧版 SDK 中并不存在。现已替换为内联实现的 `createRuntimeStore`,功能完全等价,兼容所有版本的 OpenClaw
|
|
12
15
|
**Compatible with older OpenClaw Gateway (missing createPluginRuntimeStore)** - Fixed `TypeError: (0 , _pluginSdk.createPluginRuntimeStore) is not a function` when loading the plugin on older OpenClaw Gateway versions. Root cause: `src/runtime.ts` imported `createPluginRuntimeStore` from `openclaw/plugin-sdk`, which doesn't exist in older SDK versions. Replaced with an inline `createRuntimeStore` implementation that is fully equivalent and compatible with all OpenClaw versions
|
|
13
16
|
|
package/README.md
CHANGED
|
@@ -39,6 +39,38 @@
|
|
|
39
39
|
```
|
|
40
40
|
预期输出:`✓ Gateway is running on http://127.0.0.1:18789`
|
|
41
41
|
|
|
42
|
+
### ⚠️ 版本兼容性要求
|
|
43
|
+
|
|
44
|
+
**重要**:dingtalk-connector v0.8.4+ 需要 **OpenClaw SDK v2026.3.22 或更高版本**。
|
|
45
|
+
|
|
46
|
+
| dingtalk-connector 版本 | 最低 OpenClaw SDK 版本 | 说明 |
|
|
47
|
+
|------------------------|----------------------|------|
|
|
48
|
+
| v0.8.4+ | v2026.3.22+ | 使用新版 SDK API,支持更完善的路由和会话管理 |
|
|
49
|
+
| v0.8.3 及以下 | v2026.3.x | 兼容旧版 SDK |
|
|
50
|
+
|
|
51
|
+
**如何检查版本**:
|
|
52
|
+
```bash
|
|
53
|
+
# 检查 OpenClaw 版本
|
|
54
|
+
openclaw --version
|
|
55
|
+
|
|
56
|
+
# 检查插件版本
|
|
57
|
+
openclaw plugins list
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**如何升级**:
|
|
61
|
+
```bash
|
|
62
|
+
# 升级 OpenClaw 到最新版本
|
|
63
|
+
npm install -g openclaw@latest
|
|
64
|
+
|
|
65
|
+
# 或使用 yarn
|
|
66
|
+
yarn global add openclaw@latest
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**版本不兼容时的表现**:
|
|
70
|
+
- 插件加载时会显示详细的错误提示
|
|
71
|
+
- 提示信息会包含升级命令和降级方案
|
|
72
|
+
- 插件会自动停止加载,不影响其他插件
|
|
73
|
+
|
|
42
74
|
### 2. 钉钉企业账号
|
|
43
75
|
|
|
44
76
|
- 你需要一个钉钉企业账号来创建企业内部应用
|
package/index.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 钉钉企业内部机器人插件,使用 Stream 模式连接,支持 AI Card 流式响应。
|
|
5
5
|
* 已迁移到 OpenClaw SDK,支持多账号、安全策略等完整功能。
|
|
6
6
|
*
|
|
7
|
-
* Last updated: 2026-03-
|
|
7
|
+
* Last updated: 2026-03-24
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
/**
|
|
@@ -19,10 +19,65 @@ import { dingtalkPlugin } from "./src/channel.ts";
|
|
|
19
19
|
import { setDingtalkRuntime } from "./src/runtime.ts";
|
|
20
20
|
import { registerGatewayMethods } from "./src/gateway-methods.ts";
|
|
21
21
|
|
|
22
|
+
/**
|
|
23
|
+
* 检查 OpenClaw SDK 版本兼容性
|
|
24
|
+
* @param api OpenClaw Plugin API
|
|
25
|
+
* @returns true 表示版本兼容,false 表示版本过低
|
|
26
|
+
*/
|
|
27
|
+
function checkSdkVersion(api: OpenClawPluginApi): boolean {
|
|
28
|
+
try {
|
|
29
|
+
// 检查是否存在新版 SDK 的关键 API
|
|
30
|
+
// v0.8.4+ 需要 core.channel.routing.buildAgentSessionKey 方法
|
|
31
|
+
const hasNewRoutingApi = !!(api.runtime?.core?.channel?.routing?.buildAgentSessionKey);
|
|
32
|
+
|
|
33
|
+
if (!hasNewRoutingApi) {
|
|
34
|
+
console.error('\n' + '='.repeat(80));
|
|
35
|
+
console.error('❌ OpenClaw SDK 版本过低');
|
|
36
|
+
console.error('='.repeat(80));
|
|
37
|
+
console.error('');
|
|
38
|
+
console.error('dingtalk-connector v0.8.4+ 需要 OpenClaw SDK v2026.3.22 或更高版本。');
|
|
39
|
+
console.error('');
|
|
40
|
+
console.error('当前 OpenClaw SDK 版本过低,缺少必要的 API:');
|
|
41
|
+
console.error(' - core.channel.routing.buildAgentSessionKey');
|
|
42
|
+
console.error('');
|
|
43
|
+
console.error('请升级 OpenClaw 到最新版本:');
|
|
44
|
+
console.error('');
|
|
45
|
+
console.error(' npm install -g openclaw@latest');
|
|
46
|
+
console.error(' # 或');
|
|
47
|
+
console.error(' yarn global add openclaw@latest');
|
|
48
|
+
console.error('');
|
|
49
|
+
console.error('升级后重启 OpenClaw Gateway 即可。');
|
|
50
|
+
console.error('');
|
|
51
|
+
console.error('如果需要使用旧版 OpenClaw,请降级 dingtalk-connector:');
|
|
52
|
+
console.error('');
|
|
53
|
+
console.error(' npm install @dingtalk-real-ai/dingtalk-connector@0.8.3');
|
|
54
|
+
console.error('');
|
|
55
|
+
console.error('='.repeat(80));
|
|
56
|
+
console.error('');
|
|
57
|
+
return false;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return true;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.error('检查 OpenClaw SDK 版本时出错:', err);
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
22
67
|
export default function register(api: OpenClawPluginApi) {
|
|
68
|
+
// 版本兼容性检查
|
|
69
|
+
if (!checkSdkVersion(api)) {
|
|
70
|
+
console.error('[dingtalk-connector] 插件加载失败:OpenClaw SDK 版本不兼容');
|
|
71
|
+
console.error('[dingtalk-connector] 请按照上述提示升级 OpenClaw 或降级 dingtalk-connector');
|
|
72
|
+
// 不抛出异常,避免影响其他插件加载,但不注册任何功能
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
23
76
|
setDingtalkRuntime(api.runtime);
|
|
24
77
|
api.registerChannel({ plugin: dingtalkPlugin });
|
|
25
78
|
|
|
26
79
|
// 注册 Gateway Methods
|
|
27
80
|
registerGatewayMethods(api);
|
|
81
|
+
|
|
82
|
+
console.log('[dingtalk-connector] v0.8.4 已成功加载(需要 OpenClaw SDK v2026.3.22+)');
|
|
28
83
|
}
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dingtalk-real-ai/dingtalk-connector",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
4
4
|
"description": "DingTalk (钉钉) channel connector — Stream mode with AI Card streaming",
|
|
5
5
|
"main": "index.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -51,11 +51,14 @@
|
|
|
51
51
|
"fluent-ffmpeg": "^2.1.3",
|
|
52
52
|
"form-data": "^4.0.0",
|
|
53
53
|
"mammoth": "^1.8.0",
|
|
54
|
-
"openclaw": "^2026.3.
|
|
54
|
+
"openclaw": "^2026.3.22",
|
|
55
55
|
"pako": "^2.1.0",
|
|
56
56
|
"pdf-parse": "^1.1.1",
|
|
57
57
|
"zod": "^3.22.0"
|
|
58
58
|
},
|
|
59
|
+
"peerDependencies": {
|
|
60
|
+
"openclaw": ">=2026.3.22"
|
|
61
|
+
},
|
|
59
62
|
"devDependencies": {
|
|
60
63
|
"@types/node": "^20.19.37",
|
|
61
64
|
"@vitest/coverage-v8": "^2.0.0",
|
|
@@ -15,7 +15,25 @@
|
|
|
15
15
|
* - 钉钉 API 调用(accessToken、文件下载)
|
|
16
16
|
* - 与 OpenClaw 框架集成(bindings、runtime)
|
|
17
17
|
*/
|
|
18
|
-
|
|
18
|
+
// 类型定义
|
|
19
|
+
interface ClawdbotConfig {
|
|
20
|
+
[key: string]: any;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
interface RuntimeEnv {
|
|
24
|
+
log?: (...args: any[]) => void;
|
|
25
|
+
error?: (...args: any[]) => void;
|
|
26
|
+
warn?: (...args: any[]) => void;
|
|
27
|
+
debug?: (...args: any[]) => void;
|
|
28
|
+
info?: (...args: any[]) => void;
|
|
29
|
+
[key: string]: any;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface HistoryEntry {
|
|
33
|
+
role: string;
|
|
34
|
+
content: string;
|
|
35
|
+
[key: string]: any;
|
|
36
|
+
}
|
|
19
37
|
import type { ResolvedDingtalkAccount, DingtalkConfig } from "../types/index.ts";
|
|
20
38
|
import {
|
|
21
39
|
buildSessionContext,
|
|
@@ -1101,7 +1119,7 @@ export async function handleDingTalkMessageInternal(params: HandleMessageParams)
|
|
|
1101
1119
|
);
|
|
1102
1120
|
|
|
1103
1121
|
// ✅ 处理裸露的本地文件路径(绕过 OpenClaw SDK 的 bug)
|
|
1104
|
-
const { processRawMediaPaths } = await import('../services/media
|
|
1122
|
+
const { processRawMediaPaths } = await import('../services/media');
|
|
1105
1123
|
finalText = await processRawMediaPaths(
|
|
1106
1124
|
finalText,
|
|
1107
1125
|
config,
|
package/src/policy.ts
CHANGED
|
@@ -1,17 +1,30 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
// 类型定义
|
|
2
|
+
interface ClawdbotConfig {
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface ToolPolicy {
|
|
7
|
+
allow?: string[];
|
|
8
|
+
deny?: string[];
|
|
9
|
+
}
|
|
10
|
+
import { resolveDingtalkAccount } from "./config/accounts.ts";
|
|
3
11
|
|
|
4
12
|
export function resolveDingtalkGroupToolPolicy(params: {
|
|
5
|
-
|
|
6
|
-
groupId
|
|
13
|
+
cfg: ClawdbotConfig;
|
|
14
|
+
groupId?: string | null;
|
|
15
|
+
accountId?: string | null;
|
|
7
16
|
}): ToolPolicy | undefined {
|
|
8
|
-
const {
|
|
17
|
+
const { cfg, groupId, accountId } = params;
|
|
18
|
+
|
|
19
|
+
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
9
20
|
const dingtalkCfg = account.config;
|
|
10
21
|
|
|
11
22
|
// Check group-specific policy first
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
23
|
+
if (groupId) {
|
|
24
|
+
const groupConfig = dingtalkCfg?.groups?.[groupId];
|
|
25
|
+
if (groupConfig?.tools) {
|
|
26
|
+
return groupConfig.tools;
|
|
27
|
+
}
|
|
15
28
|
}
|
|
16
29
|
|
|
17
30
|
// Fall back to account-level default (allow all)
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -1,39 +1,49 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
1
|
+
// 类型定义
|
|
2
|
+
interface ClawdbotConfig {
|
|
3
|
+
[key: string]: any;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface RuntimeEnv {
|
|
7
|
+
log?: (...args: any[]) => void;
|
|
8
|
+
error?: (...args: any[]) => void;
|
|
9
|
+
warn?: (...args: any[]) => void;
|
|
10
|
+
debug?: (...args: any[]) => void;
|
|
11
|
+
info?: (...args: any[]) => void;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface ReplyPayload {
|
|
16
|
+
text?: string;
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// ✅ 动态导入 channel-runtime 模块
|
|
21
|
+
const channelRuntimeModule = await import("openclaw/plugin-sdk/channel-runtime") as any;
|
|
11
22
|
|
|
12
23
|
const {
|
|
13
24
|
createReplyPrefixOptions,
|
|
14
25
|
createTypingCallbacks,
|
|
15
26
|
logTypingFailure,
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
);
|
|
27
|
+
} = channelRuntimeModule;
|
|
28
|
+
|
|
19
29
|
import { resolveDingtalkAccount } from "./config/accounts.ts";
|
|
20
30
|
import { getDingtalkRuntime } from "./runtime.ts";
|
|
21
31
|
import type { DingtalkConfig } from "./types/index.ts";
|
|
22
32
|
import {
|
|
23
33
|
createAICardForTarget,
|
|
24
|
-
streamAICard,
|
|
25
34
|
finishAICard,
|
|
26
|
-
|
|
27
|
-
type AICardTarget,
|
|
35
|
+
streamAICard,
|
|
28
36
|
type AICardInstance,
|
|
29
|
-
|
|
37
|
+
type AICardTarget,
|
|
38
|
+
} from "./services/messaging/card.ts";
|
|
39
|
+
import { sendMessage } from "./services/messaging.ts";
|
|
40
|
+
import { getOapiAccessToken } from "./utils/token.ts";
|
|
30
41
|
import {
|
|
31
42
|
processLocalImages,
|
|
32
43
|
processVideoMarkers,
|
|
33
44
|
processAudioMarkers,
|
|
34
45
|
processFileMarkers,
|
|
35
46
|
} from "./services/media/index.ts";
|
|
36
|
-
import { getAccessToken, getOapiAccessToken } from "./utils/index.ts";
|
|
37
47
|
|
|
38
48
|
// ============ 新会话命令归一化 ============
|
|
39
49
|
|
|
@@ -192,16 +202,16 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
192
202
|
stop: async () => {
|
|
193
203
|
// 钉钉暂不支持打字指示器
|
|
194
204
|
},
|
|
195
|
-
onStartError: (err) =>
|
|
205
|
+
onStartError: (err: any) =>
|
|
196
206
|
logTypingFailure({
|
|
197
|
-
log: (message) => params.runtime.log?.(message),
|
|
207
|
+
log: (message: any) => params.runtime.log?.(message),
|
|
198
208
|
channel: "dingtalk-connector",
|
|
199
209
|
action: "start",
|
|
200
210
|
error: err,
|
|
201
211
|
}),
|
|
202
|
-
onStopError: (err) =>
|
|
212
|
+
onStopError: (err: any) =>
|
|
203
213
|
logTypingFailure({
|
|
204
|
-
log: (message) => params.runtime.log?.(message),
|
|
214
|
+
log: (message: any) => params.runtime.log?.(message),
|
|
205
215
|
channel: "dingtalk-connector",
|
|
206
216
|
action: "stop",
|
|
207
217
|
error: err,
|
|
@@ -217,7 +227,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
217
227
|
const chunkMode = core.channel.text.resolveChunkMode(cfg, "dingtalk-connector");
|
|
218
228
|
|
|
219
229
|
// 流式 AI Card 支持
|
|
220
|
-
const streamingEnabled = account.config?.streaming !== false;
|
|
230
|
+
const streamingEnabled = (account.config as any)?.streaming !== false;
|
|
221
231
|
let isCreatingCard = false; // ✅ 添加创建中标志,防止并发创建
|
|
222
232
|
|
|
223
233
|
const startStreaming = async () => {
|
|
@@ -243,7 +253,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
243
253
|
// 这样用户看到的是同一条消息从 ACK 文案更新为最终结果,而不是多出一条消息
|
|
244
254
|
if (preCreatedCard) {
|
|
245
255
|
log.info(`[DingTalk][startStreaming] 复用预创建 AI Card,cardInstanceId=${preCreatedCard.cardInstanceId}`);
|
|
246
|
-
currentCardTarget = preCreatedCard;
|
|
256
|
+
currentCardTarget = preCreatedCard as any;
|
|
247
257
|
accumulatedText = "";
|
|
248
258
|
return;
|
|
249
259
|
}
|
|
@@ -261,14 +271,9 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
261
271
|
const card = await createAICardForTarget(
|
|
262
272
|
account.config as DingtalkConfig,
|
|
263
273
|
target,
|
|
264
|
-
|
|
265
|
-
info: params.runtime.info,
|
|
266
|
-
error: params.runtime.error,
|
|
267
|
-
warn: params.runtime.warn,
|
|
268
|
-
debug: params.runtime.debug,
|
|
269
|
-
}
|
|
274
|
+
params.runtime as any
|
|
270
275
|
);
|
|
271
|
-
currentCardTarget = card;
|
|
276
|
+
currentCardTarget = card as any;
|
|
272
277
|
accumulatedText = "";
|
|
273
278
|
|
|
274
279
|
if (card) {
|
|
@@ -284,7 +289,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
284
289
|
}
|
|
285
290
|
};
|
|
286
291
|
|
|
287
|
-
const closeStreaming = async () => {
|
|
292
|
+
const closeStreaming: () => Promise<void> = async () => {
|
|
288
293
|
if (!currentCardTarget) {
|
|
289
294
|
log.info(`[DingTalk][closeStreaming] 无 AI Card,跳过关闭`);
|
|
290
295
|
return;
|
|
@@ -347,7 +352,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
347
352
|
|
|
348
353
|
// ✅ 处理裸露的本地文件路径(绕过 OpenClaw SDK 的 bug)
|
|
349
354
|
log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
|
|
350
|
-
const { processRawMediaPaths } = await import('./services/media
|
|
355
|
+
const { processRawMediaPaths } = await import('./services/media');
|
|
351
356
|
finalText = await processRawMediaPaths(
|
|
352
357
|
finalText,
|
|
353
358
|
account.config as DingtalkConfig,
|
|
@@ -362,14 +367,9 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
362
367
|
|
|
363
368
|
log.info(`[DingTalk][closeStreaming] 准备调用 finishAICard,文本长度=${finalText.length}`);
|
|
364
369
|
await finishAICard(
|
|
365
|
-
currentCardTarget as
|
|
370
|
+
currentCardTarget as any,
|
|
366
371
|
finalText,
|
|
367
|
-
|
|
368
|
-
info: params.runtime.info,
|
|
369
|
-
error: params.runtime.error,
|
|
370
|
-
warn: params.runtime.warn,
|
|
371
|
-
debug: params.runtime.debug,
|
|
372
|
-
}
|
|
372
|
+
params.runtime as any
|
|
373
373
|
);
|
|
374
374
|
log.info(`[DingTalk][closeStreaming] ✅ AI Card 关闭成功`);
|
|
375
375
|
} catch (error: any) {
|
|
@@ -429,7 +429,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
429
429
|
const oapiToken = await getOapiAccessToken(account.config as DingtalkConfig);
|
|
430
430
|
if (oapiToken) {
|
|
431
431
|
log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
|
|
432
|
-
const { processRawMediaPaths } = await import('./services/media
|
|
432
|
+
const { processRawMediaPaths } = await import('./services/media');
|
|
433
433
|
text = await processRawMediaPaths(
|
|
434
434
|
text,
|
|
435
435
|
account.config as DingtalkConfig,
|
|
@@ -479,10 +479,10 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
479
479
|
log.info(`[DingTalk][deliver] 流式更新 AI Card,累积文本长度=${accumulatedText.length}`);
|
|
480
480
|
try {
|
|
481
481
|
await streamAICard(
|
|
482
|
-
currentCardTarget as
|
|
482
|
+
currentCardTarget as any,
|
|
483
483
|
accumulatedText,
|
|
484
484
|
false,
|
|
485
|
-
params.runtime
|
|
485
|
+
params.runtime as any
|
|
486
486
|
);
|
|
487
487
|
} catch (streamErr: any) {
|
|
488
488
|
log.error(`[DingTalk][deliver] ❌ streamAICard 失败:${streamErr.message}`);
|
|
@@ -655,15 +655,10 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
655
655
|
|
|
656
656
|
try {
|
|
657
657
|
await streamAICard(
|
|
658
|
-
currentCardTarget as
|
|
658
|
+
currentCardTarget as any,
|
|
659
659
|
displayContent,
|
|
660
660
|
false,
|
|
661
|
-
|
|
662
|
-
info: params.runtime.info,
|
|
663
|
-
error: params.runtime.error,
|
|
664
|
-
warn: params.runtime.warn,
|
|
665
|
-
debug: params.runtime.debug,
|
|
666
|
-
}
|
|
661
|
+
params.runtime as any
|
|
667
662
|
);
|
|
668
663
|
lastUpdateTime = now;
|
|
669
664
|
log.debug(`[DingTalk][onPartialReply] ✅ AI Card 更新成功`);
|
package/src/sdk/helpers.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* 完全独立的辅助函数,不依赖任何外部 SDK。
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type { SecretInput, SecretInputRef } from "./types
|
|
7
|
+
import type { SecretInput, SecretInputRef } from "./types.ts";
|
|
8
8
|
|
|
9
9
|
// ============================================================================
|
|
10
10
|
// 账号 ID 处理
|
|
@@ -43,7 +43,7 @@ export async function processAudioMarkers(
|
|
|
43
43
|
continue;
|
|
44
44
|
}
|
|
45
45
|
const uploadResult = await uploadMediaToDingTalk(absPath, 'voice', oapiToken, 20 * 1024 * 1024, log);
|
|
46
|
-
result = result.replace(full, uploadResult ? `[音频已上传:${uploadResult
|
|
46
|
+
result = result.replace(full, uploadResult ? `[音频已上传:${uploadResult}]` : '⚠️ 音频上传失败');
|
|
47
47
|
} catch {
|
|
48
48
|
log?.warn?.(`${logPrefix} 解析音频标记失败:${match[1]}`);
|
|
49
49
|
result = result.replace(full, '');
|
|
@@ -67,21 +67,15 @@ export async function enableUploadTransaction(
|
|
|
67
67
|
form.append('file_name', fileName);
|
|
68
68
|
form.append('file_size', fileSize.toString());
|
|
69
69
|
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
},
|
|
80
|
-
headers: { 'Content-Type': 'application/octet-stream' },
|
|
81
|
-
timeout: 60_000,
|
|
82
|
-
maxBodyLength: Infinity,
|
|
83
|
-
},
|
|
84
|
-
);
|
|
70
|
+
const resp = await dingtalkOapiHttp.post<UploadTransactionResponse>(
|
|
71
|
+
`${DINGTALK_OAPI}/file/upload/transaction/enable`,
|
|
72
|
+
form,
|
|
73
|
+
{
|
|
74
|
+
params: { access_token: oapiToken },
|
|
75
|
+
headers: form.getHeaders(),
|
|
76
|
+
timeout: 60_000,
|
|
77
|
+
}
|
|
78
|
+
);
|
|
85
79
|
|
|
86
80
|
if (resp.data.errcode === 0) {
|
|
87
81
|
log.info(`事务开启成功,upload_id: ${resp.data.upload_id}`);
|
|
@@ -129,20 +123,15 @@ export async function uploadFileBlock(
|
|
|
129
123
|
contentType: 'application/octet-stream',
|
|
130
124
|
});
|
|
131
125
|
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
chunk_numbers: totalChunks,
|
|
142
|
-
},
|
|
143
|
-
timeout: 30_000,
|
|
144
|
-
},
|
|
145
|
-
);
|
|
126
|
+
const resp = await dingtalkOapiHttp.post<UploadBlockResponse>(
|
|
127
|
+
`${DINGTALK_OAPI}/file/upload/chunk`,
|
|
128
|
+
form,
|
|
129
|
+
{
|
|
130
|
+
params: { access_token: oapiToken },
|
|
131
|
+
headers: form.getHeaders(),
|
|
132
|
+
timeout: 60_000,
|
|
133
|
+
}
|
|
134
|
+
);
|
|
146
135
|
|
|
147
136
|
if (resp.data.errcode === 0) {
|
|
148
137
|
log.info(`块 ${chunkNumber} 上传成功`);
|
|
@@ -93,7 +93,7 @@ export async function uploadMediaToDingTalk(
|
|
|
93
93
|
if ((mediaType === 'video' || mediaType === 'file') && fileSize > CHUNK_CONFIG.SIZE_THRESHOLD) {
|
|
94
94
|
log?.info?.(`文件超过 20MB,使用分块上传:${absPath} (${fileSizeMB}MB)`);
|
|
95
95
|
try {
|
|
96
|
-
const { uploadLargeFileByChunks } = await import('./chunk-upload
|
|
96
|
+
const { uploadLargeFileByChunks } = await import('./chunk-upload');
|
|
97
97
|
const downloadCode = await uploadLargeFileByChunks(absPath, mediaType, oapiToken, debugEnabled);
|
|
98
98
|
if (downloadCode) {
|
|
99
99
|
log?.info?.(`分块上传成功:${absPath}, download_code: ${downloadCode}`);
|
|
@@ -126,10 +126,10 @@ export async function uploadMediaToDingTalk(
|
|
|
126
126
|
log?.info?.(`上传文件:${absPath} (${fileSizeMB}MB), uploadType=${uploadType}`);
|
|
127
127
|
const resp = await dingtalkUploadHttp.post(
|
|
128
128
|
`${DINGTALK_OAPI}/media/upload`,
|
|
129
|
-
|
|
129
|
+
form,
|
|
130
130
|
{
|
|
131
131
|
params: { access_token: oapiToken, type: mediaType },
|
|
132
|
-
headers:
|
|
132
|
+
headers: form.getHeaders(),
|
|
133
133
|
timeout: 60_000,
|
|
134
134
|
maxBodyLength: Infinity,
|
|
135
135
|
},
|
|
@@ -59,7 +59,7 @@ export async function processFileMarkers(
|
|
|
59
59
|
const fileData = JSON.parse(match[1]);
|
|
60
60
|
const absPath = toLocalPath(fileData.path);
|
|
61
61
|
const uploadResult = await uploadMediaToDingTalk(absPath, 'file', oapiToken, 20 * 1024 * 1024, log);
|
|
62
|
-
result = result.replace(full, uploadResult ? `[文件已上传:${uploadResult
|
|
62
|
+
result = result.replace(full, uploadResult ? `[文件已上传:${uploadResult}]` : '⚠️ 文件上传失败');
|
|
63
63
|
} catch {
|
|
64
64
|
log?.warn?.(`${logPrefix} 解析文件标记失败:${match[1]}`);
|
|
65
65
|
result = result.replace(full, '');
|
|
@@ -3,7 +3,15 @@
|
|
|
3
3
|
* 支持图片上传、本地路径处理
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
-
|
|
6
|
+
// 本地类型定义
|
|
7
|
+
interface Logger {
|
|
8
|
+
info?: (...args: any[]) => void;
|
|
9
|
+
warn?: (...args: any[]) => void;
|
|
10
|
+
error?: (...args: any[]) => void;
|
|
11
|
+
debug?: (...args: any[]) => void;
|
|
12
|
+
[key: string]: any;
|
|
13
|
+
}
|
|
14
|
+
|
|
7
15
|
import {
|
|
8
16
|
LOCAL_IMAGE_RE,
|
|
9
17
|
BARE_IMAGE_PATH_RE,
|
|
@@ -35,7 +43,7 @@ export async function processLocalImages(
|
|
|
35
43
|
const cleanPath = rawPath.replace(/\\ /g, ' ');
|
|
36
44
|
const uploadResult = await uploadMediaToDingTalk(cleanPath, 'image', oapiToken, 20 * 1024 * 1024, log);
|
|
37
45
|
if (uploadResult) {
|
|
38
|
-
result = result.replace(fullMatch, ``);
|
|
39
47
|
}
|
|
40
48
|
}
|
|
41
49
|
}
|
|
@@ -56,7 +64,7 @@ export async function processLocalImages(
|
|
|
56
64
|
log?.info?.(`[DingTalk][Media] 纯文本图片:"${fullMatch}" -> path="${rawPath}"`);
|
|
57
65
|
const uploadResult = await uploadMediaToDingTalk(rawPath, 'image', oapiToken, 20 * 1024 * 1024, log);
|
|
58
66
|
if (uploadResult) {
|
|
59
|
-
const replacement = ``;
|
|
60
68
|
result = result.slice(0, match.index!) + result.slice(match.index!).replace(fullMatch, replacement);
|
|
61
69
|
log?.info?.(`[DingTalk][Media] 替换纯文本路径为图片:${replacement}`);
|
|
62
70
|
}
|
|
@@ -697,7 +697,7 @@ export async function sendMediaToDingTalk(params: {
|
|
|
697
697
|
const videoMarker = `[DINGTALK_VIDEO]{"path":"${mediaUrl}"}[/DINGTALK_VIDEO]`;
|
|
698
698
|
|
|
699
699
|
// 直接处理视频标记(上传并发送视频消息)
|
|
700
|
-
const { processVideoMarkers } = await import("./media
|
|
700
|
+
const { processVideoMarkers } = await import("./media");
|
|
701
701
|
await processVideoMarkers(
|
|
702
702
|
videoMarker, // 只传入标记,不包含原始文本
|
|
703
703
|
"",
|
package/src/types/index.ts
CHANGED
package/tsconfig.json
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2022"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"allowSyntheticDefaultImports": true,
|
|
9
|
+
"strict": false,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"resolveJsonModule": true,
|
|
12
|
+
"allowJs": true,
|
|
13
|
+
"noEmit": true,
|
|
14
|
+
"allowImportingTsExtensions": true,
|
|
15
|
+
"types": ["node"],
|
|
16
|
+
"noImplicitAny": false
|
|
17
|
+
},
|
|
18
|
+
"include": ["src/**/*", "index.ts"],
|
|
19
|
+
"exclude": ["node_modules", "dist"]
|
|
20
|
+
}
|