@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.
Files changed (54) hide show
  1. package/dist/index.js +60 -0
  2. package/dist/openclaw.plugin.json +19 -14
  3. package/dist/package.json +17 -5
  4. package/dist/src/channel.js +28 -1
  5. package/dist/src/runtime.js +34 -25
  6. package/dist/src/services/messaging/card.js +8 -0
  7. package/index.ts +66 -0
  8. package/openclaw.plugin.json +19 -14
  9. package/package.json +17 -5
  10. package/src/channel.ts +31 -1
  11. package/src/reply-dispatcher.ts +1 -0
  12. package/src/runtime.ts +40 -26
  13. package/src/services/messaging/card.ts +10 -2
  14. package/src/channel.js +0 -415
  15. package/src/config/accounts.js +0 -182
  16. package/src/config/schema.js +0 -135
  17. package/src/core/connection.js +0 -561
  18. package/src/core/message-handler.js +0 -1422
  19. package/src/core/provider.js +0 -59
  20. package/src/core/state.js +0 -49
  21. package/src/directory.js +0 -53
  22. package/src/docs.js +0 -209
  23. package/src/gateway-methods.js +0 -360
  24. package/src/onboarding.js +0 -337
  25. package/src/policy.js +0 -15
  26. package/src/probe.js +0 -144
  27. package/src/reply-dispatcher.js +0 -435
  28. package/src/runtime.js +0 -26
  29. package/src/sdk/helpers.js +0 -237
  30. package/src/sdk/types.js +0 -13
  31. package/src/secret-input.js +0 -13
  32. package/src/services/media/audio.js +0 -40
  33. package/src/services/media/chunk-upload.js +0 -211
  34. package/src/services/media/common.js +0 -120
  35. package/src/services/media/file.js +0 -54
  36. package/src/services/media/image.js +0 -59
  37. package/src/services/media/index.js +0 -9
  38. package/src/services/media/video.js +0 -133
  39. package/src/services/media.js +0 -889
  40. package/src/services/messaging/card.js +0 -234
  41. package/src/services/messaging/index.js +0 -8
  42. package/src/services/messaging/send.js +0 -85
  43. package/src/services/messaging.js +0 -680
  44. package/src/targets.js +0 -38
  45. package/src/types/index.js +0 -1
  46. package/src/utils/agent.js +0 -55
  47. package/src/utils/async.js +0 -40
  48. package/src/utils/constants.js +0 -24
  49. package/src/utils/http-client.js +0 -33
  50. package/src/utils/index.js +0 -7
  51. package/src/utils/logger.js +0 -76
  52. package/src/utils/session.js +0 -95
  53. package/src/utils/token.js +0 -71
  54. 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.3",
5
- "description": "\u706b\u4e00\u4e94\u5b9a\u5236\u7248\u9489\u9489 OpenClaw \u8fde\u63a5\u5668",
6
- "author": "\u706b\u4e00\u4e94\u4fe1\u606f\u79d1\u6280\u6709\u9650\u516c\u53f8",
7
- "main": "index.ts",
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": "\u9489\u9489\u4f01\u4e1a\u5185\u90e8\u673a\u5668\u4eba\uff0c\u4f7f\u7528 Stream \u6a21\u5f0f\uff0c\u65e0\u9700\u516c\u7f51 IP\uff0c\u652f\u6301 AI Card \u6d41\u5f0f\u54cd\u5e94\u3002",
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": "\u9489\u9489\u5e94\u7528\u7684 AppKey / Client ID",
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": "\u9489\u9489\u5e94\u7528\u7684 AppSecret / Client Secret",
466
+ "help": "钉钉应用的 AppSecret / Client Secret",
467
467
  "sensitive": true
468
468
  },
469
469
  "channels.dingtalk-connector.dmPolicy": {
470
470
  "label": "DM Policy",
471
- "help": "\u5355\u804a\u7b56\u7565\uff1aopen\uff08\u5f00\u653e\uff09\u3001pairing\uff08\u914d\u5bf9\uff09\u3001allowlist\uff08\u767d\u540d\u5355\uff09"
471
+ "help": "单聊策略:open(开放)、pairing(配对)、allowlist(白名单)"
472
472
  },
473
473
  "channels.dingtalk-connector.groupPolicy": {
474
474
  "label": "Group Policy",
475
- "help": "\u7fa4\u804a\u7b56\u7565\uff1aopen\uff08\u5f00\u653e\uff09\u3001allowlist\uff08\u767d\u540d\u5355\uff09\u3001disabled\uff08\u7981\u7528\uff09"
475
+ "help": "群聊策略:open(开放)、allowlist(白名单)、disabled(禁用)"
476
476
  },
477
477
  "channels.dingtalk-connector.requireMention": {
478
478
  "label": "Require @Mention",
479
- "help": "\u7fa4\u804a\u4e2d\u662f\u5426\u9700\u8981 @\u673a\u5668\u4eba \u624d\u54cd\u5e94"
479
+ "help": "群聊中是否需要 @机器人 才响应"
480
480
  },
481
481
  "channels.dingtalk-connector.debug": {
482
482
  "label": "Debug Mode",
483
- "help": "\u542f\u7528\u8c03\u8bd5\u65e5\u5fd7",
483
+ "help": "启用调试日志",
484
484
  "advanced": true
485
485
  },
486
486
  "channels.dingtalk-connector.endpoint": {
487
487
  "label": "Gateway Endpoint",
488
- "help": "\u81ea\u5b9a\u4e49 DWClient \u7f51\u5173\u5730\u5740\uff08\u9ad8\u7ea7\uff09",
488
+ "help": "自定义 DWClient 网关地址(高级)",
489
489
  "advanced": true
490
490
  },
491
491
  "channels.dingtalk-connector.accounts": {
492
492
  "label": "Accounts",
493
- "help": "\u591a\u8d26\u53f7\u914d\u7f6e\uff0c\u6bcf\u4e2a key \u662f\u8d26\u53f7 ID"
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.7",
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
- "channels": [
79
- "dingtalk-connector"
80
- ],
81
- "installDependencies": true
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",
@@ -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 {
@@ -1,26 +1,35 @@
1
- /**
2
- * 自实现的运行时存储工厂,避免依赖特定版本 openclaw 是否导出 createPluginRuntimeStore。
3
- * 旧版 openclaw 没有导出该函数,直接 import 会导致 TypeError,因此在此处内联实现。
4
- */
5
- function createRuntimeStore(errorMessage) {
6
- let runtimeValue = null;
7
- return {
8
- setRuntime: (next) => {
9
- runtimeValue = next;
10
- },
11
- clearRuntime: () => {
12
- runtimeValue = null;
13
- },
14
- tryGetRuntime: () => {
15
- return runtimeValue;
16
- },
17
- getRuntime: () => {
18
- if (runtimeValue === null) {
19
- throw new Error(errorMessage);
20
- }
21
- return runtimeValue;
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
  }
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "id": "@huo15/dingtalk-openclaw-connector",
3
3
  "name": "Huo15 DingTalk Connector Pro",
4
- "version": "1.0.3",
5
- "description": "\u706b\u4e00\u4e94\u5b9a\u5236\u7248\u9489\u9489 OpenClaw \u8fde\u63a5\u5668",
6
- "author": "\u706b\u4e00\u4e94\u4fe1\u606f\u79d1\u6280\u6709\u9650\u516c\u53f8",
7
- "main": "index.ts",
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": "\u9489\u9489\u4f01\u4e1a\u5185\u90e8\u673a\u5668\u4eba\uff0c\u4f7f\u7528 Stream \u6a21\u5f0f\uff0c\u65e0\u9700\u516c\u7f51 IP\uff0c\u652f\u6301 AI Card \u6d41\u5f0f\u54cd\u5e94\u3002",
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": "\u9489\u9489\u5e94\u7528\u7684 AppKey / Client ID",
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": "\u9489\u9489\u5e94\u7528\u7684 AppSecret / Client Secret",
466
+ "help": "钉钉应用的 AppSecret / Client Secret",
467
467
  "sensitive": true
468
468
  },
469
469
  "channels.dingtalk-connector.dmPolicy": {
470
470
  "label": "DM Policy",
471
- "help": "\u5355\u804a\u7b56\u7565\uff1aopen\uff08\u5f00\u653e\uff09\u3001pairing\uff08\u914d\u5bf9\uff09\u3001allowlist\uff08\u767d\u540d\u5355\uff09"
471
+ "help": "单聊策略:open(开放)、pairing(配对)、allowlist(白名单)"
472
472
  },
473
473
  "channels.dingtalk-connector.groupPolicy": {
474
474
  "label": "Group Policy",
475
- "help": "\u7fa4\u804a\u7b56\u7565\uff1aopen\uff08\u5f00\u653e\uff09\u3001allowlist\uff08\u767d\u540d\u5355\uff09\u3001disabled\uff08\u7981\u7528\uff09"
475
+ "help": "群聊策略:open(开放)、allowlist(白名单)、disabled(禁用)"
476
476
  },
477
477
  "channels.dingtalk-connector.requireMention": {
478
478
  "label": "Require @Mention",
479
- "help": "\u7fa4\u804a\u4e2d\u662f\u5426\u9700\u8981 @\u673a\u5668\u4eba \u624d\u54cd\u5e94"
479
+ "help": "群聊中是否需要 @机器人 才响应"
480
480
  },
481
481
  "channels.dingtalk-connector.debug": {
482
482
  "label": "Debug Mode",
483
- "help": "\u542f\u7528\u8c03\u8bd5\u65e5\u5fd7",
483
+ "help": "启用调试日志",
484
484
  "advanced": true
485
485
  },
486
486
  "channels.dingtalk-connector.endpoint": {
487
487
  "label": "Gateway Endpoint",
488
- "help": "\u81ea\u5b9a\u4e49 DWClient \u7f51\u5173\u5730\u5740\uff08\u9ad8\u7ea7\uff09",
488
+ "help": "自定义 DWClient 网关地址(高级)",
489
489
  "advanced": true
490
490
  },
491
491
  "channels.dingtalk-connector.accounts": {
492
492
  "label": "Accounts",
493
- "help": "\u591a\u8d26\u53f7\u914d\u7f6e\uff0c\u6bcf\u4e2a key \u662f\u8d26\u53f7 ID"
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.7",
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
- "channels": [
79
- "dingtalk-connector"
80
- ],
81
- "installDependencies": true
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 {
@@ -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
- * 自实现的运行时存储工厂,避免依赖特定版本 openclaw 是否导出 createPluginRuntimeStore。
5
- * 旧版 openclaw 没有导出该函数,直接 import 会导致 TypeError,因此在此处内联实现。
6
- */
7
- function createRuntimeStore<T>(errorMessage: string) {
8
- let runtimeValue: T | null = null;
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
- 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
- };
14
+ interface RuntimeSlot {
15
+ value: PluginRuntime | null;
27
16
  }
28
17
 
29
- const { setRuntime: setDingtalkRuntime, getRuntime: getDingtalkRuntime } =
30
- createRuntimeStore<PluginRuntime>("DingTalk runtime not initialized");
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 { getDingtalkRuntime, setDingtalkRuntime };
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);