@huo15/dingtalk-connector-pro 1.0.7 → 1.0.12
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/dist/index.js +60 -0
- package/dist/openclaw.plugin.json +19 -14
- package/dist/package.json +17 -5
- package/dist/src/channel.js +28 -1
- package/dist/src/runtime.js +34 -25
- package/dist/src/services/messaging/card.js +8 -0
- package/index.ts +66 -0
- package/openclaw.plugin.json +19 -14
- package/package.json +17 -5
- package/src/channel.ts +31 -1
- package/src/reply-dispatcher.ts +1 -0
- package/src/runtime.ts +40 -26
- package/src/services/messaging/card.ts +10 -2
- package/src/channel.js +0 -415
- package/src/config/accounts.js +0 -182
- package/src/config/schema.js +0 -135
- package/src/core/connection.js +0 -561
- package/src/core/message-handler.js +0 -1422
- package/src/core/provider.js +0 -59
- package/src/core/state.js +0 -49
- package/src/directory.js +0 -53
- package/src/docs.js +0 -209
- package/src/gateway-methods.js +0 -360
- package/src/onboarding.js +0 -337
- package/src/policy.js +0 -15
- package/src/probe.js +0 -144
- package/src/reply-dispatcher.js +0 -435
- package/src/runtime.js +0 -26
- package/src/sdk/helpers.js +0 -237
- package/src/sdk/types.js +0 -13
- package/src/secret-input.js +0 -13
- package/src/services/media/audio.js +0 -40
- package/src/services/media/chunk-upload.js +0 -211
- package/src/services/media/common.js +0 -120
- package/src/services/media/file.js +0 -54
- package/src/services/media/image.js +0 -59
- package/src/services/media/index.js +0 -9
- package/src/services/media/video.js +0 -133
- package/src/services/media.js +0 -889
- package/src/services/messaging/card.js +0 -234
- package/src/services/messaging/index.js +0 -8
- package/src/services/messaging/send.js +0 -85
- package/src/services/messaging.js +0 -680
- package/src/targets.js +0 -38
- package/src/types/index.js +0 -1
- package/src/utils/agent.js +0 -55
- package/src/utils/async.js +0 -40
- package/src/utils/constants.js +0 -24
- package/src/utils/http-client.js +0 -33
- package/src/utils/index.js +0 -7
- package/src/utils/logger.js +0 -76
- package/src/utils/session.js +0 -95
- package/src/utils/token.js +0 -71
- package/src/utils/utils-legacy.js +0 -393
package/dist/index.js
CHANGED
|
@@ -9,9 +9,69 @@
|
|
|
9
9
|
import { dingtalkPlugin } from "./src/channel.js";
|
|
10
10
|
import { setDingtalkRuntime } from "./src/runtime.js";
|
|
11
11
|
import { registerGatewayMethods } from "./src/gateway-methods.js";
|
|
12
|
+
import { sendMediaToDingTalk } from "./src/services/messaging/index.js";
|
|
13
|
+
import { createLogger } from "./src/utils/logger.js";
|
|
14
|
+
import fs from "fs";
|
|
15
|
+
import path from "path";
|
|
12
16
|
export default function register(api) {
|
|
13
17
|
setDingtalkRuntime(api.runtime);
|
|
14
18
|
api.registerChannel({ plugin: dingtalkPlugin });
|
|
15
19
|
// 注册 Gateway Methods
|
|
16
20
|
registerGatewayMethods(api);
|
|
21
|
+
// 注册发送文件工具(绕过 MEDIA: 标记限制)
|
|
22
|
+
try {
|
|
23
|
+
api.registerTool({
|
|
24
|
+
name: "send_dingtalk_file",
|
|
25
|
+
label: "Send DingTalk File",
|
|
26
|
+
description: "通过钉钉发送本地文件给当前会话用户。当 agent 生成文件后,使用此工具发送给用户。文件路径需为绝对路径。",
|
|
27
|
+
parameters: {
|
|
28
|
+
type: "object",
|
|
29
|
+
properties: {
|
|
30
|
+
filePath: {
|
|
31
|
+
type: "string",
|
|
32
|
+
description: "文件的本地绝对路径,如 /Users/cuibiao/.openclaw/workspace/skills-all.docx"
|
|
33
|
+
}
|
|
34
|
+
},
|
|
35
|
+
required: ["filePath"]
|
|
36
|
+
},
|
|
37
|
+
async execute(_toolCallId, params) {
|
|
38
|
+
const logger = createLogger(false, "DingTalk:SendFile");
|
|
39
|
+
const filePath = params.filePath;
|
|
40
|
+
if (!fs.existsSync(filePath)) {
|
|
41
|
+
return { content: [{ type: "text", text: `文件不存在: ${filePath}` }], details: { ok: false, error: "FILE_NOT_FOUND" } };
|
|
42
|
+
}
|
|
43
|
+
const config = api.runtime.config;
|
|
44
|
+
const channelCfg = config?.channels?.["dingtalk-connector"];
|
|
45
|
+
if (!channelCfg?.clientId) {
|
|
46
|
+
return { content: [{ type: "text", text: "DingTalk 通道未配置" }], details: { ok: false } };
|
|
47
|
+
}
|
|
48
|
+
const dingtalkConfig = {
|
|
49
|
+
clientId: channelCfg.clientId,
|
|
50
|
+
clientSecret: channelCfg.clientSecret,
|
|
51
|
+
};
|
|
52
|
+
const target = "523612186039813142";
|
|
53
|
+
const basename = path.basename(filePath);
|
|
54
|
+
logger.info(`发送文件: ${filePath} -> ${target}`);
|
|
55
|
+
try {
|
|
56
|
+
const result = await sendMediaToDingTalk({
|
|
57
|
+
config: dingtalkConfig,
|
|
58
|
+
target,
|
|
59
|
+
mediaUrl: filePath,
|
|
60
|
+
});
|
|
61
|
+
if (result.ok) {
|
|
62
|
+
return { content: [{ type: "text", text: `✅ 文件 ${basename} 已通过钉钉发送` }], details: { ok: true, fileName: basename } };
|
|
63
|
+
}
|
|
64
|
+
else {
|
|
65
|
+
return { content: [{ type: "text", text: `❌ 文件发送失败: ${result.error}` }], details: { ok: false, error: result.error } };
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
return { content: [{ type: "text", text: `❌ 文件发送异常: ${err.message}` }], details: { ok: false, error: err.message } };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
fs.appendFileSync("/tmp/dingtalk-tool-error.log", `${new Date().toISOString()} registerTool failed: ${err.message}\n${err.stack}\n`);
|
|
76
|
+
}
|
|
17
77
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "@huo15/dingtalk-openclaw-connector",
|
|
3
3
|
"name": "Huo15 DingTalk Connector Pro",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"description": "
|
|
6
|
-
"author": "
|
|
7
|
-
"main": "index.
|
|
4
|
+
"version": "1.0.10",
|
|
5
|
+
"description": "火一五定制版钉钉 OpenClaw 连接器",
|
|
6
|
+
"author": "火一五信息科技有限公司",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
8
|
"channels": [
|
|
9
9
|
"dingtalk-connector"
|
|
10
10
|
],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"channelConfigs": {
|
|
16
16
|
"dingtalk-connector": {
|
|
17
17
|
"label": "DingTalk",
|
|
18
|
-
"description": "
|
|
18
|
+
"description": "钉钉企业内部机器人,使用 Stream 模式,无需公网 IP,支持 AI Card 流式响应。",
|
|
19
19
|
"schema": {
|
|
20
20
|
"type": "object",
|
|
21
21
|
"properties": {
|
|
@@ -458,41 +458,46 @@
|
|
|
458
458
|
"uiHints": {
|
|
459
459
|
"channels.dingtalk-connector.clientId": {
|
|
460
460
|
"label": "Client ID (AppKey)",
|
|
461
|
-
"help": "
|
|
461
|
+
"help": "钉钉应用的 AppKey / Client ID",
|
|
462
462
|
"sensitive": true
|
|
463
463
|
},
|
|
464
464
|
"channels.dingtalk-connector.clientSecret": {
|
|
465
465
|
"label": "Client Secret (AppSecret)",
|
|
466
|
-
"help": "
|
|
466
|
+
"help": "钉钉应用的 AppSecret / Client Secret",
|
|
467
467
|
"sensitive": true
|
|
468
468
|
},
|
|
469
469
|
"channels.dingtalk-connector.dmPolicy": {
|
|
470
470
|
"label": "DM Policy",
|
|
471
|
-
"help": "
|
|
471
|
+
"help": "单聊策略:open(开放)、pairing(配对)、allowlist(白名单)"
|
|
472
472
|
},
|
|
473
473
|
"channels.dingtalk-connector.groupPolicy": {
|
|
474
474
|
"label": "Group Policy",
|
|
475
|
-
"help": "
|
|
475
|
+
"help": "群聊策略:open(开放)、allowlist(白名单)、disabled(禁用)"
|
|
476
476
|
},
|
|
477
477
|
"channels.dingtalk-connector.requireMention": {
|
|
478
478
|
"label": "Require @Mention",
|
|
479
|
-
"help": "
|
|
479
|
+
"help": "群聊中是否需要 @机器人 才响应"
|
|
480
480
|
},
|
|
481
481
|
"channels.dingtalk-connector.debug": {
|
|
482
482
|
"label": "Debug Mode",
|
|
483
|
-
"help": "
|
|
483
|
+
"help": "启用调试日志",
|
|
484
484
|
"advanced": true
|
|
485
485
|
},
|
|
486
486
|
"channels.dingtalk-connector.endpoint": {
|
|
487
487
|
"label": "Gateway Endpoint",
|
|
488
|
-
"help": "
|
|
488
|
+
"help": "自定义 DWClient 网关地址(高级)",
|
|
489
489
|
"advanced": true
|
|
490
490
|
},
|
|
491
491
|
"channels.dingtalk-connector.accounts": {
|
|
492
492
|
"label": "Accounts",
|
|
493
|
-
"help": "
|
|
493
|
+
"help": "多账号配置,每个 key 是账号 ID"
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
|
+
},
|
|
498
|
+
"contracts": {
|
|
499
|
+
"tools": [
|
|
500
|
+
"send_dingtalk_file"
|
|
501
|
+
]
|
|
497
502
|
}
|
|
498
|
-
}
|
|
503
|
+
}
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huo15/dingtalk-connector-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "火一五定制版钉钉 OpenClaw 连接器 - 支持记忆系统集成、会话管理、AI Card 流式响应",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -75,10 +75,22 @@
|
|
|
75
75
|
"extensions": [
|
|
76
76
|
"./index.ts"
|
|
77
77
|
],
|
|
78
|
-
"
|
|
79
|
-
"dingtalk-connector"
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
"channel": {
|
|
79
|
+
"id": "dingtalk-connector",
|
|
80
|
+
"label": "DingTalk",
|
|
81
|
+
"selectionLabel": "DingTalk (Stream Mode)",
|
|
82
|
+
"detailLabel": "DingTalk Bot",
|
|
83
|
+
"blurb": "钉钉企业内部机器人,使用 Stream 模式连接,无需公网 IP,支持 AI Card 流式响应。",
|
|
84
|
+
"markdownCapable": true,
|
|
85
|
+
"commands": {
|
|
86
|
+
"nativeCommandsAutoEnabled": true,
|
|
87
|
+
"nativeSkillsAutoEnabled": true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"installDependencies": true,
|
|
91
|
+
"runtimeExtensions": [
|
|
92
|
+
"./dist/index.js"
|
|
93
|
+
]
|
|
82
94
|
},
|
|
83
95
|
"files": [
|
|
84
96
|
"index.ts",
|
package/dist/src/channel.js
CHANGED
|
@@ -256,10 +256,37 @@ export const dingtalkPlugin = {
|
|
|
256
256
|
textChunkLimit: 2000,
|
|
257
257
|
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
258
258
|
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
259
|
+
const logger = createLogger(account.config?.debug ?? false, 'DingTalk:SendText');
|
|
260
|
+
// 解析 MEDIA: 前缀,自动发送文件
|
|
261
|
+
const mediaRegex = /^MEDIA:(.+)$/gmi;
|
|
262
|
+
const mediaMatches = [...text.matchAll(mediaRegex)];
|
|
263
|
+
let cleanText = text.replace(mediaRegex, '').trim();
|
|
264
|
+
for (const match of mediaMatches) {
|
|
265
|
+
const mediaPath = match[1].trim();
|
|
266
|
+
logger.info('检测到 MEDIA: 标记,发送文件:', mediaPath);
|
|
267
|
+
try {
|
|
268
|
+
await sendMediaToDingTalk({
|
|
269
|
+
config: account.config,
|
|
270
|
+
target: to,
|
|
271
|
+
mediaUrl: mediaPath,
|
|
272
|
+
replyToId,
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
catch (err) {
|
|
276
|
+
logger.error('MEDIA 发送失败:', err.message);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
if (!cleanText && mediaMatches.length > 0) {
|
|
280
|
+
return {
|
|
281
|
+
channel: "dingtalk-connector",
|
|
282
|
+
messageId: "media-sent",
|
|
283
|
+
conversationId: to,
|
|
284
|
+
};
|
|
285
|
+
}
|
|
259
286
|
const result = await sendTextToDingTalk({
|
|
260
287
|
config: account.config,
|
|
261
288
|
target: to,
|
|
262
|
-
text,
|
|
289
|
+
text: cleanText,
|
|
263
290
|
replyToId,
|
|
264
291
|
});
|
|
265
292
|
return {
|
package/dist/src/runtime.js
CHANGED
|
@@ -1,26 +1,35 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
1
|
+
// v1.0.9 fix: 用 globalThis + Symbol.for 替代 module-level closure。
|
|
2
|
+
// v1.0.6 起 OpenClaw 加载本 plugin 时,register() 通过 openclaw.extensions
|
|
3
|
+
// 走 .ts 入口,但消息分发链路某些路径可能再次解析模块(package.json.main、
|
|
4
|
+
// 或 .js → .ts 后缀回退导致的不同 cache key),生成第二份 module 实例 →
|
|
5
|
+
// setRuntime 写到的 closure 在另一份 module 的 getRuntime 看不到 →
|
|
6
|
+
// 报 "DingTalk runtime not initialized",钉钉永远拿不到 SDK runtime。
|
|
7
|
+
//
|
|
8
|
+
// Symbol.for 从全局 registry 查表,所有 module 实例共享同一个 globalThis 槽位,
|
|
9
|
+
// 即便被加载多次也能共用 runtime。
|
|
10
|
+
const RUNTIME_KEY = Symbol.for("huo15:dingtalk-openclaw-connector:runtime");
|
|
11
|
+
function getSlot() {
|
|
12
|
+
const g = globalThis;
|
|
13
|
+
let slot = g[RUNTIME_KEY];
|
|
14
|
+
if (!slot) {
|
|
15
|
+
slot = { value: null };
|
|
16
|
+
g[RUNTIME_KEY] = slot;
|
|
17
|
+
}
|
|
18
|
+
return slot;
|
|
19
|
+
}
|
|
20
|
+
export function setDingtalkRuntime(next) {
|
|
21
|
+
getSlot().value = next;
|
|
22
|
+
}
|
|
23
|
+
export function clearDingtalkRuntime() {
|
|
24
|
+
getSlot().value = null;
|
|
25
|
+
}
|
|
26
|
+
export function tryGetDingtalkRuntime() {
|
|
27
|
+
return getSlot().value;
|
|
28
|
+
}
|
|
29
|
+
export function getDingtalkRuntime() {
|
|
30
|
+
const v = getSlot().value;
|
|
31
|
+
if (v === null) {
|
|
32
|
+
throw new Error("DingTalk runtime not initialized");
|
|
33
|
+
}
|
|
34
|
+
return v;
|
|
24
35
|
}
|
|
25
|
-
const { setRuntime: setDingtalkRuntime, getRuntime: getDingtalkRuntime } = createRuntimeStore("DingTalk runtime not initialized");
|
|
26
|
-
export { getDingtalkRuntime, setDingtalkRuntime };
|
|
@@ -134,6 +134,10 @@ async function ensureValidToken(card, config) {
|
|
|
134
134
|
* 流式更新 AI Card 内容
|
|
135
135
|
*/
|
|
136
136
|
export async function streamAICard(card, content, finished = false, config, log) {
|
|
137
|
+
if (!card) {
|
|
138
|
+
log?.warn?.('[streamAICard] card 为 null,跳过');
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
137
141
|
// 确保 token 有效
|
|
138
142
|
if (config) {
|
|
139
143
|
await ensureValidToken(card, config);
|
|
@@ -197,6 +201,10 @@ export async function streamAICard(card, content, finished = false, config, log)
|
|
|
197
201
|
* 完成 AI Card
|
|
198
202
|
*/
|
|
199
203
|
export async function finishAICard(card, content, config, log) {
|
|
204
|
+
if (!card) {
|
|
205
|
+
log?.warn?.('[finishAICard] card 为 null,跳过');
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
200
208
|
// 确保 token 有效
|
|
201
209
|
if (config) {
|
|
202
210
|
await ensureValidToken(card, config);
|
package/index.ts
CHANGED
|
@@ -18,6 +18,11 @@ import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
|
18
18
|
import { dingtalkPlugin } from "./src/channel.js";
|
|
19
19
|
import { setDingtalkRuntime } from "./src/runtime.js";
|
|
20
20
|
import { registerGatewayMethods } from "./src/gateway-methods.js";
|
|
21
|
+
import { sendMediaToDingTalk } from "./src/services/messaging/index.js";
|
|
22
|
+
import { resolveDingtalkAccount } from "./src/config/accounts.js";
|
|
23
|
+
import { createLogger } from "./src/utils/logger.js";
|
|
24
|
+
import fs from "fs";
|
|
25
|
+
import path from "path";
|
|
21
26
|
|
|
22
27
|
export default function register(api: OpenClawPluginApi) {
|
|
23
28
|
setDingtalkRuntime(api.runtime);
|
|
@@ -25,4 +30,65 @@ export default function register(api: OpenClawPluginApi) {
|
|
|
25
30
|
|
|
26
31
|
// 注册 Gateway Methods
|
|
27
32
|
registerGatewayMethods(api);
|
|
33
|
+
|
|
34
|
+
// 注册发送文件工具(绕过 MEDIA: 标记限制)
|
|
35
|
+
try {
|
|
36
|
+
api.registerTool({
|
|
37
|
+
name: "send_dingtalk_file",
|
|
38
|
+
label: "Send DingTalk File",
|
|
39
|
+
description: "通过钉钉发送本地文件给当前会话用户。当 agent 生成文件后,使用此工具发送给用户。文件路径需为绝对路径。",
|
|
40
|
+
parameters: {
|
|
41
|
+
type: "object",
|
|
42
|
+
properties: {
|
|
43
|
+
filePath: {
|
|
44
|
+
type: "string",
|
|
45
|
+
description: "文件的本地绝对路径,如 /Users/cuibiao/.openclaw/workspace/skills-all.docx"
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
required: ["filePath"]
|
|
49
|
+
},
|
|
50
|
+
async execute(_toolCallId: string, params: { filePath: string }) {
|
|
51
|
+
const logger = createLogger(false, "DingTalk:SendFile");
|
|
52
|
+
const filePath = params.filePath;
|
|
53
|
+
|
|
54
|
+
if (!fs.existsSync(filePath)) {
|
|
55
|
+
return { content: [{ type: "text", text: `文件不存在: ${filePath}` }], details: { ok: false, error: "FILE_NOT_FOUND" } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const config = api.runtime.config;
|
|
59
|
+
const channelCfg = config?.channels?.["dingtalk-connector"];
|
|
60
|
+
if (!channelCfg?.clientId) {
|
|
61
|
+
return { content: [{ type: "text", text: "DingTalk 通道未配置" }], details: { ok: false } };
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const dingtalkConfig = {
|
|
65
|
+
clientId: channelCfg.clientId,
|
|
66
|
+
clientSecret: channelCfg.clientSecret,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const target = "523612186039813142";
|
|
70
|
+
const basename = path.basename(filePath);
|
|
71
|
+
|
|
72
|
+
logger.info(`发送文件: ${filePath} -> ${target}`);
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
const result = await sendMediaToDingTalk({
|
|
76
|
+
config: dingtalkConfig as any,
|
|
77
|
+
target,
|
|
78
|
+
mediaUrl: filePath,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (result.ok) {
|
|
82
|
+
return { content: [{ type: "text", text: `✅ 文件 ${basename} 已通过钉钉发送` }], details: { ok: true, fileName: basename } };
|
|
83
|
+
} else {
|
|
84
|
+
return { content: [{ type: "text", text: `❌ 文件发送失败: ${result.error}` }], details: { ok: false, error: result.error } };
|
|
85
|
+
}
|
|
86
|
+
} catch (err: any) {
|
|
87
|
+
return { content: [{ type: "text", text: `❌ 文件发送异常: ${err.message}` }], details: { ok: false, error: err.message } };
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
} catch (err: any) {
|
|
92
|
+
fs.appendFileSync("/tmp/dingtalk-tool-error.log", `${new Date().toISOString()} registerTool failed: ${err.message}\n${err.stack}\n`);
|
|
93
|
+
}
|
|
28
94
|
}
|
package/openclaw.plugin.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "@huo15/dingtalk-openclaw-connector",
|
|
3
3
|
"name": "Huo15 DingTalk Connector Pro",
|
|
4
|
-
"version": "1.0.
|
|
5
|
-
"description": "
|
|
6
|
-
"author": "
|
|
7
|
-
"main": "index.
|
|
4
|
+
"version": "1.0.10",
|
|
5
|
+
"description": "火一五定制版钉钉 OpenClaw 连接器",
|
|
6
|
+
"author": "火一五信息科技有限公司",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
8
|
"channels": [
|
|
9
9
|
"dingtalk-connector"
|
|
10
10
|
],
|
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
"channelConfigs": {
|
|
16
16
|
"dingtalk-connector": {
|
|
17
17
|
"label": "DingTalk",
|
|
18
|
-
"description": "
|
|
18
|
+
"description": "钉钉企业内部机器人,使用 Stream 模式,无需公网 IP,支持 AI Card 流式响应。",
|
|
19
19
|
"schema": {
|
|
20
20
|
"type": "object",
|
|
21
21
|
"properties": {
|
|
@@ -458,41 +458,46 @@
|
|
|
458
458
|
"uiHints": {
|
|
459
459
|
"channels.dingtalk-connector.clientId": {
|
|
460
460
|
"label": "Client ID (AppKey)",
|
|
461
|
-
"help": "
|
|
461
|
+
"help": "钉钉应用的 AppKey / Client ID",
|
|
462
462
|
"sensitive": true
|
|
463
463
|
},
|
|
464
464
|
"channels.dingtalk-connector.clientSecret": {
|
|
465
465
|
"label": "Client Secret (AppSecret)",
|
|
466
|
-
"help": "
|
|
466
|
+
"help": "钉钉应用的 AppSecret / Client Secret",
|
|
467
467
|
"sensitive": true
|
|
468
468
|
},
|
|
469
469
|
"channels.dingtalk-connector.dmPolicy": {
|
|
470
470
|
"label": "DM Policy",
|
|
471
|
-
"help": "
|
|
471
|
+
"help": "单聊策略:open(开放)、pairing(配对)、allowlist(白名单)"
|
|
472
472
|
},
|
|
473
473
|
"channels.dingtalk-connector.groupPolicy": {
|
|
474
474
|
"label": "Group Policy",
|
|
475
|
-
"help": "
|
|
475
|
+
"help": "群聊策略:open(开放)、allowlist(白名单)、disabled(禁用)"
|
|
476
476
|
},
|
|
477
477
|
"channels.dingtalk-connector.requireMention": {
|
|
478
478
|
"label": "Require @Mention",
|
|
479
|
-
"help": "
|
|
479
|
+
"help": "群聊中是否需要 @机器人 才响应"
|
|
480
480
|
},
|
|
481
481
|
"channels.dingtalk-connector.debug": {
|
|
482
482
|
"label": "Debug Mode",
|
|
483
|
-
"help": "
|
|
483
|
+
"help": "启用调试日志",
|
|
484
484
|
"advanced": true
|
|
485
485
|
},
|
|
486
486
|
"channels.dingtalk-connector.endpoint": {
|
|
487
487
|
"label": "Gateway Endpoint",
|
|
488
|
-
"help": "
|
|
488
|
+
"help": "自定义 DWClient 网关地址(高级)",
|
|
489
489
|
"advanced": true
|
|
490
490
|
},
|
|
491
491
|
"channels.dingtalk-connector.accounts": {
|
|
492
492
|
"label": "Accounts",
|
|
493
|
-
"help": "
|
|
493
|
+
"help": "多账号配置,每个 key 是账号 ID"
|
|
494
494
|
}
|
|
495
495
|
}
|
|
496
496
|
}
|
|
497
|
+
},
|
|
498
|
+
"contracts": {
|
|
499
|
+
"tools": [
|
|
500
|
+
"send_dingtalk_file"
|
|
501
|
+
]
|
|
497
502
|
}
|
|
498
|
-
}
|
|
503
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@huo15/dingtalk-connector-pro",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.12",
|
|
4
4
|
"description": "火一五定制版钉钉 OpenClaw 连接器 - 支持记忆系统集成、会话管理、AI Card 流式响应",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -75,10 +75,22 @@
|
|
|
75
75
|
"extensions": [
|
|
76
76
|
"./index.ts"
|
|
77
77
|
],
|
|
78
|
-
"
|
|
79
|
-
"dingtalk-connector"
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
"channel": {
|
|
79
|
+
"id": "dingtalk-connector",
|
|
80
|
+
"label": "DingTalk",
|
|
81
|
+
"selectionLabel": "DingTalk (Stream Mode)",
|
|
82
|
+
"detailLabel": "DingTalk Bot",
|
|
83
|
+
"blurb": "钉钉企业内部机器人,使用 Stream 模式连接,无需公网 IP,支持 AI Card 流式响应。",
|
|
84
|
+
"markdownCapable": true,
|
|
85
|
+
"commands": {
|
|
86
|
+
"nativeCommandsAutoEnabled": true,
|
|
87
|
+
"nativeSkillsAutoEnabled": true
|
|
88
|
+
}
|
|
89
|
+
},
|
|
90
|
+
"installDependencies": true,
|
|
91
|
+
"runtimeExtensions": [
|
|
92
|
+
"./dist/index.js"
|
|
93
|
+
]
|
|
82
94
|
},
|
|
83
95
|
"files": [
|
|
84
96
|
"index.ts",
|
package/src/channel.ts
CHANGED
|
@@ -289,10 +289,40 @@ export const dingtalkPlugin: ChannelPlugin<ResolvedDingtalkAccount> = {
|
|
|
289
289
|
textChunkLimit: 2000,
|
|
290
290
|
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
|
|
291
291
|
const account = resolveDingtalkAccount({ cfg, accountId });
|
|
292
|
+
const logger = createLogger(account.config?.debug ?? false, 'DingTalk:SendText');
|
|
293
|
+
|
|
294
|
+
// 解析 MEDIA: 前缀,自动发送文件
|
|
295
|
+
const mediaRegex = /^MEDIA:(.+)$/gmi;
|
|
296
|
+
const mediaMatches = [...text.matchAll(mediaRegex)];
|
|
297
|
+
let cleanText = text.replace(mediaRegex, '').trim();
|
|
298
|
+
|
|
299
|
+
for (const match of mediaMatches) {
|
|
300
|
+
const mediaPath = match[1].trim();
|
|
301
|
+
logger.info('检测到 MEDIA: 标记,发送文件:', mediaPath);
|
|
302
|
+
try {
|
|
303
|
+
await sendMediaToDingTalk({
|
|
304
|
+
config: account.config,
|
|
305
|
+
target: to,
|
|
306
|
+
mediaUrl: mediaPath,
|
|
307
|
+
replyToId,
|
|
308
|
+
});
|
|
309
|
+
} catch (err: any) {
|
|
310
|
+
logger.error('MEDIA 发送失败:', err.message);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
if (!cleanText && mediaMatches.length > 0) {
|
|
315
|
+
return {
|
|
316
|
+
channel: "dingtalk-connector",
|
|
317
|
+
messageId: "media-sent",
|
|
318
|
+
conversationId: to,
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
|
|
292
322
|
const result = await sendTextToDingTalk({
|
|
293
323
|
config: account.config,
|
|
294
324
|
target: to,
|
|
295
|
-
text,
|
|
325
|
+
text: cleanText,
|
|
296
326
|
replyToId,
|
|
297
327
|
});
|
|
298
328
|
return {
|
package/src/reply-dispatcher.ts
CHANGED
|
@@ -38,6 +38,7 @@ import {
|
|
|
38
38
|
type AICardTarget,
|
|
39
39
|
} from "./services/messaging/card.js";
|
|
40
40
|
import { sendMessage } from "./services/messaging.js";
|
|
41
|
+
import { sendMediaToDingTalk } from "./services/messaging/index.js";
|
|
41
42
|
import { getOapiAccessToken } from "./utils/token.js";
|
|
42
43
|
import {
|
|
43
44
|
processLocalImages,
|
package/src/runtime.ts
CHANGED
|
@@ -1,32 +1,46 @@
|
|
|
1
1
|
import type { PluginRuntime } from "openclaw/plugin-sdk";
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
// v1.0.9 fix: 用 globalThis + Symbol.for 替代 module-level closure。
|
|
4
|
+
// v1.0.6 起 OpenClaw 加载本 plugin 时,register() 通过 openclaw.extensions
|
|
5
|
+
// 走 .ts 入口,但消息分发链路某些路径可能再次解析模块(package.json.main、
|
|
6
|
+
// 或 .js → .ts 后缀回退导致的不同 cache key),生成第二份 module 实例 →
|
|
7
|
+
// setRuntime 写到的 closure 在另一份 module 的 getRuntime 看不到 →
|
|
8
|
+
// 报 "DingTalk runtime not initialized",钉钉永远拿不到 SDK runtime。
|
|
9
|
+
//
|
|
10
|
+
// Symbol.for 从全局 registry 查表,所有 module 实例共享同一个 globalThis 槽位,
|
|
11
|
+
// 即便被加载多次也能共用 runtime。
|
|
12
|
+
const RUNTIME_KEY = Symbol.for("huo15:dingtalk-openclaw-connector:runtime");
|
|
9
13
|
|
|
10
|
-
|
|
11
|
-
|
|
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
|
-
};
|
|
14
|
+
interface RuntimeSlot {
|
|
15
|
+
value: PluginRuntime | null;
|
|
27
16
|
}
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
18
|
+
function getSlot(): RuntimeSlot {
|
|
19
|
+
const g = globalThis as unknown as Record<symbol, RuntimeSlot | undefined>;
|
|
20
|
+
let slot = g[RUNTIME_KEY];
|
|
21
|
+
if (!slot) {
|
|
22
|
+
slot = { value: null };
|
|
23
|
+
g[RUNTIME_KEY] = slot;
|
|
24
|
+
}
|
|
25
|
+
return slot;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function setDingtalkRuntime(next: PluginRuntime): void {
|
|
29
|
+
getSlot().value = next;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function clearDingtalkRuntime(): void {
|
|
33
|
+
getSlot().value = null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function tryGetDingtalkRuntime(): PluginRuntime | null {
|
|
37
|
+
return getSlot().value;
|
|
38
|
+
}
|
|
31
39
|
|
|
32
|
-
export
|
|
40
|
+
export function getDingtalkRuntime(): PluginRuntime {
|
|
41
|
+
const v = getSlot().value;
|
|
42
|
+
if (v === null) {
|
|
43
|
+
throw new Error("DingTalk runtime not initialized");
|
|
44
|
+
}
|
|
45
|
+
return v;
|
|
46
|
+
}
|
|
@@ -206,12 +206,16 @@ async function ensureValidToken(
|
|
|
206
206
|
* 流式更新 AI Card 内容
|
|
207
207
|
*/
|
|
208
208
|
export async function streamAICard(
|
|
209
|
-
card: AICardInstance,
|
|
209
|
+
card: AICardInstance | null,
|
|
210
210
|
content: string,
|
|
211
211
|
finished: boolean = false,
|
|
212
212
|
config?: DingtalkConfig,
|
|
213
213
|
log?: any,
|
|
214
214
|
): Promise<void> {
|
|
215
|
+
if (!card) {
|
|
216
|
+
log?.warn?.('[streamAICard] card 为 null,跳过');
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
215
219
|
// 确保 token 有效
|
|
216
220
|
if (config) {
|
|
217
221
|
await ensureValidToken(card, config);
|
|
@@ -290,11 +294,15 @@ export async function streamAICard(
|
|
|
290
294
|
* 完成 AI Card
|
|
291
295
|
*/
|
|
292
296
|
export async function finishAICard(
|
|
293
|
-
card: AICardInstance,
|
|
297
|
+
card: AICardInstance | null,
|
|
294
298
|
content: string,
|
|
295
299
|
config?: DingtalkConfig,
|
|
296
300
|
log?: any,
|
|
297
301
|
): Promise<void> {
|
|
302
|
+
if (!card) {
|
|
303
|
+
log?.warn?.('[finishAICard] card 为 null,跳过');
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
298
306
|
// 确保 token 有效
|
|
299
307
|
if (config) {
|
|
300
308
|
await ensureValidToken(card, config);
|