@dingtalk-real-ai/dingtalk-connector 0.8.3 → 0.8.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/CHANGELOG.md +12 -0
- 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 +47 -44
- package/src/runtime.ts +27 -2
- 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,6 +5,18 @@ 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.5] - 2026-03-24
|
|
9
|
+
|
|
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
|
+
|
|
14
|
+
- 🐛 **兼容旧版 OpenClaw Gateway(createPluginRuntimeStore 缺失)** - 修复在旧版 OpenClaw Gateway 上加载插件时报错 `TypeError: (0 , _pluginSdk.createPluginRuntimeStore) is not a function` 的问题。根因是 `src/runtime.ts` 直接从 `openclaw/plugin-sdk` 导入 `createPluginRuntimeStore`,而该函数在旧版 SDK 中并不存在。现已替换为内联实现的 `createRuntimeStore`,功能完全等价,兼容所有版本的 OpenClaw
|
|
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
|
|
16
|
+
|
|
17
|
+
- 🐛 **openclaw 依赖版本约束放宽** - 将 `package.json` 中的 `"openclaw": "^2026.3.0"` 改为 `"openclaw": "*"`,避免版本约束导致安装失败或与用户已安装版本冲突
|
|
18
|
+
**Relaxed openclaw dependency version constraint** - Changed `"openclaw": "^2026.3.0"` to `"openclaw": "*"` in `package.json` to avoid installation failures or conflicts with the user's installed version
|
|
19
|
+
|
|
8
20
|
## [0.8.3] - 2026-03-24
|
|
9
21
|
|
|
10
22
|
### 修复 / Fixes
|
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.5",
|
|
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,31 +1,49 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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;
|
|
22
|
+
|
|
23
|
+
const {
|
|
7
24
|
createReplyPrefixOptions,
|
|
8
25
|
createTypingCallbacks,
|
|
9
26
|
logTypingFailure,
|
|
10
|
-
}
|
|
27
|
+
} = channelRuntimeModule;
|
|
28
|
+
|
|
11
29
|
import { resolveDingtalkAccount } from "./config/accounts.ts";
|
|
12
30
|
import { getDingtalkRuntime } from "./runtime.ts";
|
|
13
31
|
import type { DingtalkConfig } from "./types/index.ts";
|
|
14
32
|
import {
|
|
15
33
|
createAICardForTarget,
|
|
16
|
-
streamAICard,
|
|
17
34
|
finishAICard,
|
|
18
|
-
|
|
19
|
-
type AICardTarget,
|
|
35
|
+
streamAICard,
|
|
20
36
|
type AICardInstance,
|
|
21
|
-
|
|
37
|
+
type AICardTarget,
|
|
38
|
+
} from "./services/messaging/card.ts";
|
|
39
|
+
import { sendMessage } from "./services/messaging.ts";
|
|
40
|
+
import { getOapiAccessToken } from "./utils/token.ts";
|
|
22
41
|
import {
|
|
23
42
|
processLocalImages,
|
|
24
43
|
processVideoMarkers,
|
|
25
44
|
processAudioMarkers,
|
|
26
45
|
processFileMarkers,
|
|
27
46
|
} from "./services/media/index.ts";
|
|
28
|
-
import { getAccessToken, getOapiAccessToken } from "./utils/index.ts";
|
|
29
47
|
|
|
30
48
|
// ============ 新会话命令归一化 ============
|
|
31
49
|
|
|
@@ -184,16 +202,16 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
184
202
|
stop: async () => {
|
|
185
203
|
// 钉钉暂不支持打字指示器
|
|
186
204
|
},
|
|
187
|
-
onStartError: (err) =>
|
|
205
|
+
onStartError: (err: any) =>
|
|
188
206
|
logTypingFailure({
|
|
189
|
-
log: (message) => params.runtime.log?.(message),
|
|
207
|
+
log: (message: any) => params.runtime.log?.(message),
|
|
190
208
|
channel: "dingtalk-connector",
|
|
191
209
|
action: "start",
|
|
192
210
|
error: err,
|
|
193
211
|
}),
|
|
194
|
-
onStopError: (err) =>
|
|
212
|
+
onStopError: (err: any) =>
|
|
195
213
|
logTypingFailure({
|
|
196
|
-
log: (message) => params.runtime.log?.(message),
|
|
214
|
+
log: (message: any) => params.runtime.log?.(message),
|
|
197
215
|
channel: "dingtalk-connector",
|
|
198
216
|
action: "stop",
|
|
199
217
|
error: err,
|
|
@@ -209,7 +227,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
209
227
|
const chunkMode = core.channel.text.resolveChunkMode(cfg, "dingtalk-connector");
|
|
210
228
|
|
|
211
229
|
// 流式 AI Card 支持
|
|
212
|
-
const streamingEnabled = account.config?.streaming !== false;
|
|
230
|
+
const streamingEnabled = (account.config as any)?.streaming !== false;
|
|
213
231
|
let isCreatingCard = false; // ✅ 添加创建中标志,防止并发创建
|
|
214
232
|
|
|
215
233
|
const startStreaming = async () => {
|
|
@@ -235,7 +253,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
235
253
|
// 这样用户看到的是同一条消息从 ACK 文案更新为最终结果,而不是多出一条消息
|
|
236
254
|
if (preCreatedCard) {
|
|
237
255
|
log.info(`[DingTalk][startStreaming] 复用预创建 AI Card,cardInstanceId=${preCreatedCard.cardInstanceId}`);
|
|
238
|
-
currentCardTarget = preCreatedCard;
|
|
256
|
+
currentCardTarget = preCreatedCard as any;
|
|
239
257
|
accumulatedText = "";
|
|
240
258
|
return;
|
|
241
259
|
}
|
|
@@ -253,14 +271,9 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
253
271
|
const card = await createAICardForTarget(
|
|
254
272
|
account.config as DingtalkConfig,
|
|
255
273
|
target,
|
|
256
|
-
|
|
257
|
-
info: params.runtime.info,
|
|
258
|
-
error: params.runtime.error,
|
|
259
|
-
warn: params.runtime.warn,
|
|
260
|
-
debug: params.runtime.debug,
|
|
261
|
-
}
|
|
274
|
+
params.runtime as any
|
|
262
275
|
);
|
|
263
|
-
currentCardTarget = card;
|
|
276
|
+
currentCardTarget = card as any;
|
|
264
277
|
accumulatedText = "";
|
|
265
278
|
|
|
266
279
|
if (card) {
|
|
@@ -276,7 +289,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
276
289
|
}
|
|
277
290
|
};
|
|
278
291
|
|
|
279
|
-
const closeStreaming = async () => {
|
|
292
|
+
const closeStreaming: () => Promise<void> = async () => {
|
|
280
293
|
if (!currentCardTarget) {
|
|
281
294
|
log.info(`[DingTalk][closeStreaming] 无 AI Card,跳过关闭`);
|
|
282
295
|
return;
|
|
@@ -339,7 +352,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
339
352
|
|
|
340
353
|
// ✅ 处理裸露的本地文件路径(绕过 OpenClaw SDK 的 bug)
|
|
341
354
|
log.info(`[DingTalk][closeStreaming] 准备调用 processRawMediaPaths`);
|
|
342
|
-
const { processRawMediaPaths } = await import('./services/media
|
|
355
|
+
const { processRawMediaPaths } = await import('./services/media');
|
|
343
356
|
finalText = await processRawMediaPaths(
|
|
344
357
|
finalText,
|
|
345
358
|
account.config as DingtalkConfig,
|
|
@@ -354,14 +367,9 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
354
367
|
|
|
355
368
|
log.info(`[DingTalk][closeStreaming] 准备调用 finishAICard,文本长度=${finalText.length}`);
|
|
356
369
|
await finishAICard(
|
|
357
|
-
currentCardTarget as
|
|
370
|
+
currentCardTarget as any,
|
|
358
371
|
finalText,
|
|
359
|
-
|
|
360
|
-
info: params.runtime.info,
|
|
361
|
-
error: params.runtime.error,
|
|
362
|
-
warn: params.runtime.warn,
|
|
363
|
-
debug: params.runtime.debug,
|
|
364
|
-
}
|
|
372
|
+
params.runtime as any
|
|
365
373
|
);
|
|
366
374
|
log.info(`[DingTalk][closeStreaming] ✅ AI Card 关闭成功`);
|
|
367
375
|
} catch (error: any) {
|
|
@@ -421,7 +429,7 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
421
429
|
const oapiToken = await getOapiAccessToken(account.config as DingtalkConfig);
|
|
422
430
|
if (oapiToken) {
|
|
423
431
|
log.info(`[DingTalk][deliver] 检测到 final 响应,准备处理裸露文件路径`);
|
|
424
|
-
const { processRawMediaPaths } = await import('./services/media
|
|
432
|
+
const { processRawMediaPaths } = await import('./services/media');
|
|
425
433
|
text = await processRawMediaPaths(
|
|
426
434
|
text,
|
|
427
435
|
account.config as DingtalkConfig,
|
|
@@ -471,10 +479,10 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
471
479
|
log.info(`[DingTalk][deliver] 流式更新 AI Card,累积文本长度=${accumulatedText.length}`);
|
|
472
480
|
try {
|
|
473
481
|
await streamAICard(
|
|
474
|
-
currentCardTarget as
|
|
482
|
+
currentCardTarget as any,
|
|
475
483
|
accumulatedText,
|
|
476
484
|
false,
|
|
477
|
-
params.runtime
|
|
485
|
+
params.runtime as any
|
|
478
486
|
);
|
|
479
487
|
} catch (streamErr: any) {
|
|
480
488
|
log.error(`[DingTalk][deliver] ❌ streamAICard 失败:${streamErr.message}`);
|
|
@@ -647,15 +655,10 @@ export function createDingtalkReplyDispatcher(params: CreateDingtalkReplyDispatc
|
|
|
647
655
|
|
|
648
656
|
try {
|
|
649
657
|
await streamAICard(
|
|
650
|
-
currentCardTarget as
|
|
658
|
+
currentCardTarget as any,
|
|
651
659
|
displayContent,
|
|
652
660
|
false,
|
|
653
|
-
|
|
654
|
-
info: params.runtime.info,
|
|
655
|
-
error: params.runtime.error,
|
|
656
|
-
warn: params.runtime.warn,
|
|
657
|
-
debug: params.runtime.debug,
|
|
658
|
-
}
|
|
661
|
+
params.runtime as any
|
|
659
662
|
);
|
|
660
663
|
lastUpdateTime = now;
|
|
661
664
|
log.debug(`[DingTalk][onPartialReply] ✅ AI Card 更新成功`);
|
package/src/runtime.ts
CHANGED
|
@@ -1,7 +1,32 @@
|
|
|
1
|
-
import { createPluginRuntimeStore } from "openclaw/plugin-sdk";
|
|
2
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
3
2
|
|
|
3
|
+
/**
|
|
4
|
+
* 自实现的运行时存储工厂,避免依赖特定版本 openclaw 是否导出 createPluginRuntimeStore。
|
|
5
|
+
* 旧版 openclaw 没有导出该函数,直接 import 会导致 TypeError,因此在此处内联实现。
|
|
6
|
+
*/
|
|
7
|
+
function createRuntimeStore<T>(errorMessage: string) {
|
|
8
|
+
let runtimeValue: T | null = null;
|
|
9
|
+
|
|
10
|
+
return {
|
|
11
|
+
setRuntime: (next: T): void => {
|
|
12
|
+
runtimeValue = next;
|
|
13
|
+
},
|
|
14
|
+
clearRuntime: (): void => {
|
|
15
|
+
runtimeValue = null;
|
|
16
|
+
},
|
|
17
|
+
tryGetRuntime: (): T | null => {
|
|
18
|
+
return runtimeValue;
|
|
19
|
+
},
|
|
20
|
+
getRuntime: (): T => {
|
|
21
|
+
if (runtimeValue === null) {
|
|
22
|
+
throw new Error(errorMessage);
|
|
23
|
+
}
|
|
24
|
+
return runtimeValue;
|
|
25
|
+
},
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
4
29
|
const { setRuntime: setDingtalkRuntime, getRuntime: getDingtalkRuntime } =
|
|
5
|
-
|
|
30
|
+
createRuntimeStore<PluginRuntime>("DingTalk runtime not initialized");
|
|
6
31
|
|
|
7
32
|
export { getDingtalkRuntime, setDingtalkRuntime };
|
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
|
+
}
|