@sunnoy/wecom 2.2.1 → 2.3.0

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/README.md CHANGED
@@ -180,7 +180,8 @@ npm test
180
180
  | `channels.wecom.secret` | string | 是 | 企业微信 AI 机器人 Secret |
181
181
  | `channels.wecom.websocketUrl` | string | 否 | WS 地址,默认 `wss://openws.work.weixin.qq.com` |
182
182
  | `channels.wecom.sendThinkingMessage` | boolean | 否 | 是否先发送 `<think></think>` 占位,默认 `true` |
183
- | `channels.wecom.welcomeMessage` | string | 否 | 进入会话欢迎语 |
183
+ | `channels.wecom.welcomeMessage` | string | 否 | 进入会话欢迎语(非空时固定使用该字符串) |
184
+ | `channels.wecom.welcomeMessagesFile` | string | 否 | 欢迎语列表文件路径。支持:`{ "messages": [ ... ] }` 或顶层数组;每条欢迎语可为**一行一个字符串的数组**(推荐,易读),或单条字符串(可含 `\\n`)。相对路径基于 OpenClaw 状态目录(`~/.openclaw` 或 `OPENCLAW_STATE_DIR`)。未设置 `welcomeMessage` 时从该文件随机选取;**修改文件后无需重启服务**(按 mtime 自动重读) |
184
185
  | `channels.wecom.adminUsers` | string[] | 否 | 管理员用户 ID,可绕过命令白名单 |
185
186
  | `channels.wecom.defaultAccount` | string | 否 | 多账号模式默认账号 |
186
187
 
package/index.js CHANGED
@@ -1,7 +1,8 @@
1
- import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
2
1
  import { logger } from "./logger.js";
3
2
  import { wecomChannelPlugin } from "./wecom/channel-plugin.js";
4
3
  import { createWeComMcpTool } from "./wecom/mcp-tool.js";
4
+ import { createImageStudioTool } from "./wecom/image-studio-tool.js";
5
+ import { resolveQwenImageToolsConfig, wecomPluginConfigSchema } from "./wecom/plugin-config.js";
5
6
  import { setOpenclawConfig, setRuntime } from "./wecom/state.js";
6
7
  import { buildReplyMediaGuidance } from "./wecom/ws-monitor.js";
7
8
  import { listAccountIds, resolveAccount } from "./wecom/accounts.js";
@@ -11,7 +12,7 @@ const plugin = {
11
12
  id: "wecom",
12
13
  name: "Enterprise WeChat",
13
14
  description: "Enterprise WeChat AI Bot channel plugin for OpenClaw",
14
- configSchema: emptyPluginConfigSchema(),
15
+ configSchema: wecomPluginConfigSchema,
15
16
  register(api) {
16
17
  logger.info("Registering WeCom WS plugin");
17
18
  setRuntime(api.runtime);
@@ -19,6 +20,14 @@ const plugin = {
19
20
  api.registerChannel({ plugin: wecomChannelPlugin });
20
21
  api.registerTool(createWeComMcpTool(), { name: "wecom_mcp" });
21
22
 
23
+ const qwenImageToolsConfig = resolveQwenImageToolsConfig(api.pluginConfig);
24
+ if (qwenImageToolsConfig.enabled) {
25
+ api.registerTool(createImageStudioTool(qwenImageToolsConfig), { name: "image_studio" });
26
+ logger.info(
27
+ `[image_studio] Registered with provider "${qwenImageToolsConfig.provider}" using qwen(generate=${qwenImageToolsConfig.models.qwen.generate}, edit=${qwenImageToolsConfig.models.qwen.edit}) wan(generate=${qwenImageToolsConfig.models.wan.generate}, edit=${qwenImageToolsConfig.models.wan.edit})`,
28
+ );
29
+ }
30
+
22
31
  // Register HTTP callback endpoints for all accounts that have callback config
23
32
  for (const accountId of listAccountIds(api.config)) {
24
33
  const account = resolveAccount(api.config, accountId);
@@ -8,7 +8,143 @@
8
8
  "configSchema": {
9
9
  "type": "object",
10
10
  "additionalProperties": true,
11
- "properties": {}
11
+ "properties": {
12
+ "qwenImageTools": {
13
+ "type": "object",
14
+ "additionalProperties": false,
15
+ "properties": {
16
+ "enabled": {
17
+ "type": "boolean"
18
+ },
19
+ "provider": {
20
+ "type": "string"
21
+ },
22
+ "route": {
23
+ "type": "string",
24
+ "enum": [
25
+ "auto",
26
+ "qwen",
27
+ "wan"
28
+ ]
29
+ },
30
+ "endpoint": {
31
+ "type": "string"
32
+ },
33
+ "endpoints": {
34
+ "type": "object",
35
+ "additionalProperties": false,
36
+ "properties": {
37
+ "qwen": {
38
+ "type": "string"
39
+ },
40
+ "wan": {
41
+ "type": "string"
42
+ },
43
+ "task": {
44
+ "type": "string"
45
+ }
46
+ }
47
+ },
48
+ "timeoutMs": {
49
+ "type": "integer",
50
+ "minimum": 1000
51
+ },
52
+ "models": {
53
+ "type": "object",
54
+ "additionalProperties": false,
55
+ "properties": {
56
+ "generate": {
57
+ "type": "string"
58
+ },
59
+ "edit": {
60
+ "type": "string"
61
+ },
62
+ "qwen": {
63
+ "type": "object",
64
+ "additionalProperties": false,
65
+ "properties": {
66
+ "generate": {
67
+ "type": "string"
68
+ },
69
+ "edit": {
70
+ "type": "string"
71
+ }
72
+ }
73
+ },
74
+ "wan": {
75
+ "type": "object",
76
+ "additionalProperties": false,
77
+ "properties": {
78
+ "generate": {
79
+ "type": "string"
80
+ },
81
+ "edit": {
82
+ "type": "string"
83
+ }
84
+ }
85
+ }
86
+ }
87
+ },
88
+ "defaults": {
89
+ "type": "object",
90
+ "additionalProperties": false,
91
+ "properties": {
92
+ "size": {
93
+ "type": "string"
94
+ },
95
+ "aspect": {
96
+ "type": "string",
97
+ "enum": [
98
+ "landscape",
99
+ "square",
100
+ "portrait"
101
+ ]
102
+ },
103
+ "n": {
104
+ "type": "integer",
105
+ "minimum": 1
106
+ },
107
+ "watermark": {
108
+ "type": "boolean"
109
+ },
110
+ "promptExtend": {
111
+ "type": "boolean"
112
+ },
113
+ "qwen": {
114
+ "type": "object",
115
+ "additionalProperties": false,
116
+ "properties": {
117
+ "landscape": {
118
+ "type": "string"
119
+ },
120
+ "square": {
121
+ "type": "string"
122
+ },
123
+ "portrait": {
124
+ "type": "string"
125
+ }
126
+ }
127
+ },
128
+ "wan": {
129
+ "type": "object",
130
+ "additionalProperties": false,
131
+ "properties": {
132
+ "landscape": {
133
+ "type": "string"
134
+ },
135
+ "square": {
136
+ "type": "string"
137
+ },
138
+ "portrait": {
139
+ "type": "string"
140
+ }
141
+ }
142
+ }
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
12
148
  },
13
149
  "channels": [
14
150
  "wecom"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sunnoy/wecom",
3
- "version": "2.2.1",
3
+ "version": "2.3.0",
4
4
  "description": "Enterprise WeChat AI Bot channel plugin for OpenClaw",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -8,7 +8,6 @@
8
8
  "index.js",
9
9
  "wecom",
10
10
  "dynamic-agent.js",
11
-
12
11
  "logger.js",
13
12
  "README.md",
14
13
  "LICENSE",
@@ -60,7 +59,7 @@
60
59
  "author": "",
61
60
  "license": "ISC",
62
61
  "dependencies": {
63
- "@wecom/aibot-node-sdk": "^1.0.2",
62
+ "@wecom/aibot-node-sdk": "^1.0.3",
64
63
  "file-type": "^21.3.0"
65
64
  }
66
65
  }
package/wecom/accounts.js CHANGED
@@ -9,6 +9,7 @@ const RESERVED_KEYS = new Set([
9
9
  "websocketUrl",
10
10
  "sendThinkingMessage",
11
11
  "welcomeMessage",
12
+ "welcomeMessagesFile",
12
13
  "allowFrom",
13
14
  "dmPolicy",
14
15
  "groupPolicy",
@@ -32,6 +33,7 @@ const SHARED_MULTI_ACCOUNT_KEYS = new Set([
32
33
  "websocketUrl",
33
34
  "sendThinkingMessage",
34
35
  "welcomeMessage",
36
+ "welcomeMessagesFile",
35
37
  "allowFrom",
36
38
  "dmPolicy",
37
39
  "groupPolicy",
@@ -321,10 +321,6 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
321
321
  ? generateAgentId(peerKind, peerId, account.accountId)
322
322
  : null;
323
323
 
324
- if (dynamicAgentId) {
325
- await ensureDynamicAgentListed(dynamicAgentId, account.config.workspaceTemplate);
326
- }
327
-
328
324
  const route = core.routing.resolveAgentRoute({
329
325
  cfg: config,
330
326
  channel: CHANNEL_ID,
@@ -337,9 +333,17 @@ async function processCallbackMessage({ parsedMsg, account, config, runtime }) {
337
333
  config.bindings.some(
338
334
  (b) => b.match?.channel === CHANNEL_ID && b.match?.accountId === account.accountId,
339
335
  );
336
+
340
337
  if (dynamicAgentId && !hasExplicitBinding) {
338
+ const routeAgentId = route.agentId;
339
+ // Use the account's configured agentId as the base for property inheritance
340
+ // (model, subagents, tools). route.agentId may resolve to "main" when
341
+ // there is no explicit binding, but the account's agentId points to the
342
+ // actual parent agent whose properties the dynamic agent should inherit.
343
+ const baseAgentId = account.config.agentId || routeAgentId;
344
+ await ensureDynamicAgentListed(dynamicAgentId, account.config.workspaceTemplate, baseAgentId);
345
+ route.sessionKey = route.sessionKey.replace(`agent:${routeAgentId}:`, `agent:${dynamicAgentId}:`);
341
346
  route.agentId = dynamicAgentId;
342
- route.sessionKey = `agent:${dynamicAgentId}:${peerKind}:${peerId}`;
343
347
  }
344
348
 
345
349
  // Build a body object that mirrors the WS frame.body structure expected by
@@ -36,6 +36,7 @@ import {
36
36
  } from "./constants.js";
37
37
  import { uploadAndSendMedia } from "./media-uploader.js";
38
38
  import { getExtendedMediaLocalRoots } from "./openclaw-compat.js";
39
+ import { extractParentAgentId } from "./parent-resolver.js";
39
40
  import { sendWsMessage, startWsMonitor } from "./ws-monitor.js";
40
41
  import { getWsClient } from "./ws-state.js";
41
42
 
@@ -262,6 +263,7 @@ export const wecomChannelPlugin = {
262
263
  websocketUrl: { type: "string" },
263
264
  sendThinkingMessage: { type: "boolean" },
264
265
  welcomeMessage: { type: "string" },
266
+ welcomeMessagesFile: { type: "string" },
265
267
  dmPolicy: { enum: ["pairing", "allowlist", "open", "disabled"] },
266
268
  allowFrom: { type: "array", items: { type: "string" } },
267
269
  groupPolicy: { enum: ["open", "allowlist", "disabled"] },
@@ -291,6 +293,10 @@ export const wecomChannelPlugin = {
291
293
  secret: { label: "Secret", sensitive: true },
292
294
  websocketUrl: { label: "WebSocket URL", placeholder: DEFAULT_WS_URL },
293
295
  welcomeMessage: { label: "Welcome Message" },
296
+ welcomeMessagesFile: {
297
+ label: "Welcome Messages File",
298
+ placeholder: "welcome-messages.json",
299
+ },
294
300
  "agent.corpSecret": { sensitive: true, label: "Application Secret" },
295
301
  "agent.replyFormat": { label: "Reply Format", placeholder: "text" },
296
302
  "agent.callback.token": { label: "Callback Token" },
@@ -404,6 +410,7 @@ export const wecomChannelPlugin = {
404
410
  try {
405
411
  if (!target.toParty && !target.toTag) {
406
412
  const wsTarget = target.chatId || target.toUser || to;
413
+ logger.debug(`[wecom] sendText: trying WS accountId=${resolvedAccountId} target=${wsTarget}`);
407
414
  return await sendWsMessage({
408
415
  to: wsTarget,
409
416
  content: text,
@@ -411,9 +418,17 @@ export const wecomChannelPlugin = {
411
418
  });
412
419
  }
413
420
  } catch (error) {
414
- logger.warn(`[wecom] WS sendText failed, falling back to Agent API: ${error.message}`);
421
+ logger.warn(`[wecom] WS sendText failed (accountId=${resolvedAccountId}), falling back: ${error.message}`);
422
+ }
423
+
424
+ // Webhook fallback for accounts without Agent API (e.g. WS bot mode)
425
+ const account = resolveAccount(cfg, resolvedAccountId);
426
+ if (!account?.agentCredentials && account?.config?.webhooks?.default) {
427
+ logger.debug(`[wecom] sendText: Agent API unavailable, using webhook fallback accountId=${resolvedAccountId}`);
428
+ return sendViaWebhook({ cfg, accountId: resolvedAccountId, webhookName: "default", text });
415
429
  }
416
430
 
431
+ logger.debug(`[wecom] sendText: trying Agent API accountId=${resolvedAccountId}`);
417
432
  return sendViaAgent({
418
433
  cfg,
419
434
  accountId: resolvedAccountId,
@@ -631,6 +646,55 @@ export const wecomChannelPlugin = {
631
646
  };
632
647
  },
633
648
  },
649
+ hooks: {
650
+ /**
651
+ * Ensure announce delivery uses a valid WeCom channel accountId.
652
+ *
653
+ * When a dynamic agent (e.g. wecom-yoyo-dm-xxx) spawns a sub-agent,
654
+ * the announce delivery may reference the dynamic agent ID as accountId.
655
+ * This hook resolves it to the actual WeCom account (e.g. yoyo) so the
656
+ * outbound sendText can find valid WS/Agent API credentials.
657
+ */
658
+ subagent_delivery_target: async (event, ctx) => {
659
+ const origin = event.requesterOrigin;
660
+ if (!origin?.channel || origin.channel !== CHANNEL_ID) return;
661
+
662
+ const cfg = ctx?.cfg ?? getOpenclawConfig();
663
+
664
+ // Check whether current accountId already resolves to a valid account
665
+ const currentAccount = resolveAccount(cfg, origin.accountId);
666
+ if (currentAccount?.enabled) return;
667
+
668
+ // Try to extract the base account from a dynamic agent ID
669
+ const baseId = extractParentAgentId(origin.accountId);
670
+ if (baseId && baseId !== origin.accountId) {
671
+ const baseAccount = resolveAccount(cfg, baseId);
672
+ if (baseAccount?.enabled) {
673
+ logger.info(`[wecom] subagent_delivery_target: ${origin.accountId} → ${baseId}`);
674
+ return { origin: { ...origin, accountId: baseId } };
675
+ }
676
+ }
677
+
678
+ // Fallback to default account
679
+ const defaultId = resolveDefaultAccountId(cfg);
680
+ if (defaultId && defaultId !== origin.accountId) {
681
+ logger.info(`[wecom] subagent_delivery_target: fallback → ${defaultId}`);
682
+ return { origin: { ...origin, accountId: defaultId } };
683
+ }
684
+ },
685
+
686
+ subagent_spawned: async (event) => {
687
+ logger.info(
688
+ `[wecom] subagent spawned: child=${event.childSessionKey} requester=${event.requesterSessionKey}`,
689
+ );
690
+ },
691
+
692
+ subagent_ended: async (event) => {
693
+ logger.info(
694
+ `[wecom] subagent ended: target=${event.targetSessionKey} reason=${event.reason} outcome=${event.outcome}`,
695
+ );
696
+ },
697
+ },
634
698
  };
635
699
 
636
700
  export const wecomChannelPluginTesting = {};
@@ -47,7 +47,8 @@ export const DEFAULT_WELCOME_MESSAGES = [
47
47
  "/compact 压缩对话",
48
48
  "/help 帮助",
49
49
  "/status 查看状态",
50
- "/reasoning stream 打开思考动画",
50
+ "你可以让我生成和编辑图片了",
51
+ "你可以用语音跟我对话",
51
52
  ].join("\n"),
52
53
  [
53
54
  "终于唤醒我啦,我已经准备就绪!😄",
@@ -57,7 +58,8 @@ export const DEFAULT_WELCOME_MESSAGES = [
57
58
  "/compact 压缩对话",
58
59
  "/help 帮助",
59
60
  "/status 查看状态",
60
- "/reasoning stream 打开思考动画",
61
+ "你可以让我生成和编辑图片了",
62
+ "你可以用语音跟我对话",
61
63
  ].join("\n"),
62
64
  [
63
65
  "欢迎回来,准备开始今天的工作吧!✨",
@@ -67,7 +69,8 @@ export const DEFAULT_WELCOME_MESSAGES = [
67
69
  "/compact 压缩对话",
68
70
  "/help 帮助",
69
71
  "/status 查看状态",
70
- "/reasoning stream 打开思考动画",
72
+ "你可以让我生成和编辑图片了",
73
+ "你可以用语音跟我对话",
71
74
  ].join("\n"),
72
75
  [
73
76
  "嗨,我已经在线!🤖",
@@ -77,7 +80,8 @@ export const DEFAULT_WELCOME_MESSAGES = [
77
80
  "/compact 压缩对话",
78
81
  "/help 帮助",
79
82
  "/status 查看状态",
80
- "/reasoning stream 打开思考动画",
83
+ "你可以让我生成和编辑图片了",
84
+ "你可以用语音跟我对话",
81
85
  ].join("\n"),
82
86
  [
83
87
  "今天也一起高效开工吧!🚀",
@@ -87,7 +91,8 @@ export const DEFAULT_WELCOME_MESSAGES = [
87
91
  "/compact 压缩对话",
88
92
  "/help 帮助",
89
93
  "/status 查看状态",
90
- "/reasoning stream 打开思考动画",
94
+ "你可以让我生成和编辑图片了",
95
+ "你可以用语音跟我对话",
91
96
  ].join("\n"),
92
97
  [
93
98
  "叮咚,你的数字助手已就位!🎉",
@@ -97,7 +102,8 @@ export const DEFAULT_WELCOME_MESSAGES = [
97
102
  "/compact 压缩对话",
98
103
  "/help 帮助",
99
104
  "/status 查看状态",
100
- "/reasoning stream 打开思考动画",
105
+ "你可以让我生成和编辑图片了",
106
+ "你可以用语音跟我对话",
101
107
  ].join("\n"),
102
108
  [
103
109
  "灵感加载完成,随时可以开聊!💡",
@@ -107,7 +113,8 @@ export const DEFAULT_WELCOME_MESSAGES = [
107
113
  "/compact 压缩对话",
108
114
  "/help 帮助",
109
115
  "/status 查看状态",
110
- "/reasoning stream 打开思考动画",
116
+ "你可以让我生成和编辑图片了",
117
+ "你可以用语音跟我对话",
111
118
  ].join("\n"),
112
119
  ];
113
120
  export const DEFAULT_WELCOME_MESSAGE = DEFAULT_WELCOME_MESSAGES[0];