@wecom/wecom-openclaw-plugin 2026.4.2 → 2026.4.3-beta.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.
Files changed (215) hide show
  1. package/README.md +395 -10
  2. package/dist/index.d.ts +2 -4
  3. package/dist/index.js +58 -0
  4. package/dist/src/accounts.d.ts +57 -0
  5. package/dist/src/accounts.js +222 -0
  6. package/dist/src/agent/api-client.d.ts +94 -0
  7. package/dist/src/agent/api-client.js +292 -0
  8. package/dist/src/agent/handler.d.ts +63 -0
  9. package/dist/src/agent/handler.js +551 -0
  10. package/dist/src/agent/index.d.ts +5 -0
  11. package/dist/src/agent/index.js +5 -0
  12. package/dist/src/agent/webhook.d.ts +24 -0
  13. package/dist/src/agent/webhook.js +228 -0
  14. package/dist/src/agent/xml.d.ts +21 -0
  15. package/dist/src/agent/xml.js +41 -0
  16. package/dist/src/channel.js +579 -0
  17. package/dist/src/chat-queue.d.ts +31 -0
  18. package/dist/src/chat-queue.js +45 -0
  19. package/dist/{esm/types/src → src}/const.d.ts +46 -0
  20. package/dist/{esm/src → src}/const.js +77 -30
  21. package/dist/{esm/src → src}/dm-policy.js +6 -9
  22. package/dist/src/dynamic-agent.d.ts +37 -0
  23. package/dist/src/dynamic-agent.js +60 -0
  24. package/dist/src/dynamic-routing.d.ts +65 -0
  25. package/dist/src/dynamic-routing.js +58 -0
  26. package/dist/{esm/src → src}/group-policy.js +8 -22
  27. package/dist/src/http.d.ts +26 -0
  28. package/dist/src/http.js +93 -0
  29. package/dist/{esm/types/src → src}/interface.d.ts +0 -2
  30. package/dist/src/interface.js +4 -0
  31. package/dist/src/mcp/index.js +6 -0
  32. package/dist/{esm/src → src}/mcp/interceptors/biz-error.js +2 -6
  33. package/dist/{esm/src → src}/mcp/interceptors/index.js +6 -10
  34. package/dist/{esm/src → src}/mcp/interceptors/msg-media.js +6 -10
  35. package/dist/{esm/src → src}/mcp/interceptors/smartpage-create.js +2 -6
  36. package/dist/{esm/src → src}/mcp/interceptors/smartpage-export.js +2 -6
  37. package/dist/src/mcp/interceptors/types.js +7 -0
  38. package/dist/{esm/src → src}/mcp/schema.js +1 -4
  39. package/dist/{esm/src → src}/mcp/tool.js +4 -8
  40. package/dist/{esm/src → src}/mcp/transport.js +15 -16
  41. package/dist/{esm/src → src}/media-handler.js +6 -10
  42. package/dist/{esm/src → src}/media-uploader.js +64 -10
  43. package/dist/{esm/src → src}/message-parser.js +1 -4
  44. package/dist/{esm/types/src → src}/message-sender.d.ts +18 -0
  45. package/dist/{esm/src → src}/message-sender.js +45 -11
  46. package/dist/{esm/src → src}/monitor.js +145 -292
  47. package/dist/{esm/src → src}/onboarding.js +22 -26
  48. package/dist/{esm/types/src → src}/openclaw-compat.d.ts +49 -0
  49. package/dist/{esm/src → src}/openclaw-compat.js +133 -27
  50. package/dist/{esm/src → src}/reqid-store.js +4 -7
  51. package/dist/{esm/src → src}/runtime.js +2 -5
  52. package/dist/src/shared/command-auth.d.ts +23 -0
  53. package/dist/src/shared/command-auth.js +73 -0
  54. package/dist/src/shared/xml-parser.d.ts +46 -0
  55. package/dist/src/shared/xml-parser.js +210 -0
  56. package/dist/{esm/src → src}/state-dir-resolve.js +3 -7
  57. package/dist/{esm/src → src}/state-manager.js +84 -16
  58. package/dist/src/target.d.ts +35 -0
  59. package/dist/src/target.js +67 -0
  60. package/dist/src/template-card-manager.d.ts +55 -0
  61. package/dist/src/template-card-manager.js +205 -0
  62. package/dist/{esm/src → src}/template-card-parser.js +17 -8
  63. package/dist/{esm/src → src}/timeout.js +2 -5
  64. package/dist/src/types/account.d.ts +29 -0
  65. package/dist/src/types/account.js +4 -0
  66. package/dist/src/types/config.d.ts +91 -0
  67. package/dist/src/types/config.js +7 -0
  68. package/dist/src/types/constants.d.ts +42 -0
  69. package/dist/src/types/constants.js +42 -0
  70. package/dist/src/types/index.d.ts +7 -0
  71. package/dist/src/types/index.js +5 -0
  72. package/dist/src/types/message.d.ts +238 -0
  73. package/dist/src/types/message.js +5 -0
  74. package/dist/{esm/types/src → src}/utils.d.ts +41 -0
  75. package/dist/{esm/src → src}/utils.js +24 -10
  76. package/dist/src/version.d.ts +2 -0
  77. package/dist/src/version.js +24 -0
  78. package/dist/src/webhook/command-auth.d.ts +47 -0
  79. package/dist/src/webhook/command-auth.js +96 -0
  80. package/dist/src/webhook/gateway.d.ts +36 -0
  81. package/dist/src/webhook/gateway.js +235 -0
  82. package/dist/src/webhook/handler.d.ts +18 -0
  83. package/dist/src/webhook/handler.js +420 -0
  84. package/dist/src/webhook/helpers.d.ts +156 -0
  85. package/dist/src/webhook/helpers.js +833 -0
  86. package/dist/src/webhook/http.d.ts +26 -0
  87. package/dist/src/webhook/http.js +94 -0
  88. package/dist/src/webhook/index.d.ts +11 -0
  89. package/dist/src/webhook/index.js +14 -0
  90. package/dist/src/webhook/media.d.ts +29 -0
  91. package/dist/src/webhook/media.js +88 -0
  92. package/dist/src/webhook/monitor.d.ts +59 -0
  93. package/dist/src/webhook/monitor.js +1161 -0
  94. package/dist/src/webhook/state.d.ts +220 -0
  95. package/dist/src/webhook/state.js +491 -0
  96. package/dist/src/webhook/target.d.ts +41 -0
  97. package/dist/src/webhook/target.js +132 -0
  98. package/dist/src/webhook/types.d.ts +347 -0
  99. package/dist/src/webhook/types.js +33 -0
  100. package/dist/src/webhook/video-frame.d.ts +13 -0
  101. package/dist/src/webhook/video-frame.js +30 -0
  102. package/package.json +15 -7
  103. package/dist/cjs/index.js +0 -45
  104. package/dist/cjs/index.js.map +0 -1
  105. package/dist/cjs/src/channel.js +0 -339
  106. package/dist/cjs/src/channel.js.map +0 -1
  107. package/dist/cjs/src/const.js +0 -137
  108. package/dist/cjs/src/const.js.map +0 -1
  109. package/dist/cjs/src/dm-policy.js +0 -89
  110. package/dist/cjs/src/dm-policy.js.map +0 -1
  111. package/dist/cjs/src/group-policy.js +0 -133
  112. package/dist/cjs/src/group-policy.js.map +0 -1
  113. package/dist/cjs/src/mcp/interceptors/biz-error.js +0 -70
  114. package/dist/cjs/src/mcp/interceptors/biz-error.js.map +0 -1
  115. package/dist/cjs/src/mcp/interceptors/index.js +0 -73
  116. package/dist/cjs/src/mcp/interceptors/index.js.map +0 -1
  117. package/dist/cjs/src/mcp/interceptors/msg-media.js +0 -159
  118. package/dist/cjs/src/mcp/interceptors/msg-media.js.map +0 -1
  119. package/dist/cjs/src/mcp/interceptors/smartpage-create.js +0 -157
  120. package/dist/cjs/src/mcp/interceptors/smartpage-create.js.map +0 -1
  121. package/dist/cjs/src/mcp/interceptors/smartpage-export.js +0 -93
  122. package/dist/cjs/src/mcp/interceptors/smartpage-export.js.map +0 -1
  123. package/dist/cjs/src/mcp/schema.js +0 -104
  124. package/dist/cjs/src/mcp/schema.js.map +0 -1
  125. package/dist/cjs/src/mcp/tool.js +0 -208
  126. package/dist/cjs/src/mcp/tool.js.map +0 -1
  127. package/dist/cjs/src/mcp/transport.js +0 -417
  128. package/dist/cjs/src/mcp/transport.js.map +0 -1
  129. package/dist/cjs/src/media-handler.js +0 -129
  130. package/dist/cjs/src/media-handler.js.map +0 -1
  131. package/dist/cjs/src/media-uploader.js +0 -284
  132. package/dist/cjs/src/media-uploader.js.map +0 -1
  133. package/dist/cjs/src/message-parser.js +0 -134
  134. package/dist/cjs/src/message-parser.js.map +0 -1
  135. package/dist/cjs/src/message-sender.js +0 -89
  136. package/dist/cjs/src/message-sender.js.map +0 -1
  137. package/dist/cjs/src/monitor.js +0 -991
  138. package/dist/cjs/src/monitor.js.map +0 -1
  139. package/dist/cjs/src/onboarding.js +0 -182
  140. package/dist/cjs/src/onboarding.js.map +0 -1
  141. package/dist/cjs/src/openclaw-compat.js +0 -400
  142. package/dist/cjs/src/openclaw-compat.js.map +0 -1
  143. package/dist/cjs/src/reqid-store.js +0 -88
  144. package/dist/cjs/src/reqid-store.js.map +0 -1
  145. package/dist/cjs/src/runtime.js +0 -16
  146. package/dist/cjs/src/runtime.js.map +0 -1
  147. package/dist/cjs/src/state-dir-resolve.js +0 -35
  148. package/dist/cjs/src/state-dir-resolve.js.map +0 -1
  149. package/dist/cjs/src/state-manager.js +0 -158
  150. package/dist/cjs/src/state-manager.js.map +0 -1
  151. package/dist/cjs/src/template-card-parser.js +0 -641
  152. package/dist/cjs/src/template-card-parser.js.map +0 -1
  153. package/dist/cjs/src/timeout.js +0 -42
  154. package/dist/cjs/src/timeout.js.map +0 -1
  155. package/dist/cjs/src/utils.js +0 -60
  156. package/dist/cjs/src/utils.js.map +0 -1
  157. package/dist/cjs/src/version.js +0 -26
  158. package/dist/cjs/src/version.js.map +0 -1
  159. package/dist/esm/index.js +0 -41
  160. package/dist/esm/index.js.map +0 -1
  161. package/dist/esm/src/channel.js +0 -337
  162. package/dist/esm/src/channel.js.map +0 -1
  163. package/dist/esm/src/const.js.map +0 -1
  164. package/dist/esm/src/dm-policy.js.map +0 -1
  165. package/dist/esm/src/group-policy.js.map +0 -1
  166. package/dist/esm/src/mcp/interceptors/biz-error.js.map +0 -1
  167. package/dist/esm/src/mcp/interceptors/index.js.map +0 -1
  168. package/dist/esm/src/mcp/interceptors/msg-media.js.map +0 -1
  169. package/dist/esm/src/mcp/interceptors/smartpage-create.js.map +0 -1
  170. package/dist/esm/src/mcp/interceptors/smartpage-export.js.map +0 -1
  171. package/dist/esm/src/mcp/schema.js.map +0 -1
  172. package/dist/esm/src/mcp/tool.js.map +0 -1
  173. package/dist/esm/src/mcp/transport.js.map +0 -1
  174. package/dist/esm/src/media-handler.js.map +0 -1
  175. package/dist/esm/src/media-uploader.js.map +0 -1
  176. package/dist/esm/src/message-parser.js.map +0 -1
  177. package/dist/esm/src/message-sender.js.map +0 -1
  178. package/dist/esm/src/monitor.js.map +0 -1
  179. package/dist/esm/src/onboarding.js.map +0 -1
  180. package/dist/esm/src/openclaw-compat.js.map +0 -1
  181. package/dist/esm/src/reqid-store.js.map +0 -1
  182. package/dist/esm/src/runtime.js.map +0 -1
  183. package/dist/esm/src/state-dir-resolve.js.map +0 -1
  184. package/dist/esm/src/state-manager.js.map +0 -1
  185. package/dist/esm/src/template-card-parser.js.map +0 -1
  186. package/dist/esm/src/timeout.js.map +0 -1
  187. package/dist/esm/src/utils.js.map +0 -1
  188. package/dist/esm/src/version.js +0 -23
  189. package/dist/esm/src/version.js.map +0 -1
  190. package/dist/esm/types/index.d.ts +0 -9
  191. package/dist/esm/types/src/version.d.ts +0 -2
  192. /package/dist/{esm/types/src → src}/channel.d.ts +0 -0
  193. /package/dist/{esm/types/src → src}/dm-policy.d.ts +0 -0
  194. /package/dist/{esm/types/src → src}/group-policy.d.ts +0 -0
  195. /package/dist/{esm/types/src → src}/mcp/index.d.ts +0 -0
  196. /package/dist/{esm/types/src → src}/mcp/interceptors/biz-error.d.ts +0 -0
  197. /package/dist/{esm/types/src → src}/mcp/interceptors/index.d.ts +0 -0
  198. /package/dist/{esm/types/src → src}/mcp/interceptors/msg-media.d.ts +0 -0
  199. /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-create.d.ts +0 -0
  200. /package/dist/{esm/types/src → src}/mcp/interceptors/smartpage-export.d.ts +0 -0
  201. /package/dist/{esm/types/src → src}/mcp/interceptors/types.d.ts +0 -0
  202. /package/dist/{esm/types/src → src}/mcp/schema.d.ts +0 -0
  203. /package/dist/{esm/types/src → src}/mcp/tool.d.ts +0 -0
  204. /package/dist/{esm/types/src → src}/mcp/transport.d.ts +0 -0
  205. /package/dist/{esm/types/src → src}/media-handler.d.ts +0 -0
  206. /package/dist/{esm/types/src → src}/media-uploader.d.ts +0 -0
  207. /package/dist/{esm/types/src → src}/message-parser.d.ts +0 -0
  208. /package/dist/{esm/types/src → src}/monitor.d.ts +0 -0
  209. /package/dist/{esm/types/src → src}/onboarding.d.ts +0 -0
  210. /package/dist/{esm/types/src → src}/reqid-store.d.ts +0 -0
  211. /package/dist/{esm/types/src → src}/runtime.d.ts +0 -0
  212. /package/dist/{esm/types/src → src}/state-dir-resolve.d.ts +0 -0
  213. /package/dist/{esm/types/src → src}/state-manager.d.ts +0 -0
  214. /package/dist/{esm/types/src → src}/template-card-parser.d.ts +0 -0
  215. /package/dist/{esm/types/src → src}/timeout.d.ts +0 -0
@@ -0,0 +1,579 @@
1
+ import { buildAccountScopedDmSecurityPolicy } from "./openclaw-compat.js";
2
+ import { formatPairingApproveHint, DEFAULT_ACCOUNT_ID } from './openclaw-compat.js';
3
+ import { getWeComRuntime } from "./runtime.js";
4
+ import { monitorWeComProvider } from "./monitor.js";
5
+ import { getWeComWebSocket } from "./state-manager.js";
6
+ import { wecomSetupWizard, wecomSetupAdapter } from "./onboarding.js";
7
+ import { listWeComAccountIds, resolveWeComAccountMulti, resolveDefaultWeComAccountId, hasMultiAccounts, } from "./accounts.js";
8
+ import { CHANNEL_ID, TEXT_CHUNK_LIMIT, WEBHOOK_PATHS } from "./const.js";
9
+ import { uploadAndSendMedia } from "./media-uploader.js";
10
+ import { registerAgentWebhookTarget, deregisterAgentWebhookTarget } from "./agent/webhook.js";
11
+ import { resolveWecomTarget } from "./target.js";
12
+ import { sendText as sendAgentText, sendMedia as sendAgentMedia, uploadMedia as uploadAgentMedia } from "./agent/api-client.js";
13
+ import { startWebhookGateway, stopWebhookGateway } from "./webhook/index.js";
14
+ /**
15
+ * 使用 SDK 的 sendMessage 主动发送企业微信消息
16
+ * 优先 Bot WebSocket,不可用时自动回退到 Agent HTTP API
17
+ */
18
+ async function sendWeComMessage({ to, content, accountId, cfg, }) {
19
+ const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
20
+ // 从 to 中提取目标(格式是 "${CHANNEL_ID}:xxx" 或直接是目标字符串)
21
+ const channelPrefix = new RegExp(`^${CHANNEL_ID}:`, "i");
22
+ const chatId = to.replace(channelPrefix, "");
23
+ // ── 尝试 Bot WebSocket ──
24
+ const wsClient = getWeComWebSocket(resolvedAccountId);
25
+ if (wsClient?.isConnected) {
26
+ const result = await wsClient.sendMessage(chatId, {
27
+ msgtype: 'markdown',
28
+ markdown: { content },
29
+ });
30
+ const messageId = result?.headers?.req_id ?? `wecom-${Date.now()}`;
31
+ return { channel: CHANNEL_ID, messageId, chatId };
32
+ }
33
+ // ── 回退到 Agent HTTP API ──
34
+ if (!cfg) {
35
+ throw new Error(`WSClient not connected for account ${resolvedAccountId} and no config available for Agent fallback`);
36
+ }
37
+ const account = resolveWeComAccountMulti({ cfg, accountId: resolvedAccountId });
38
+ const agent = account.agent;
39
+ if (!agent?.configured) {
40
+ throw new Error(`WSClient not connected for account ${resolvedAccountId} and Agent mode is not configured. ` +
41
+ `Please configure either Bot (botId + secret) or Agent (corpId + corpSecret + agentId) for this account.`);
42
+ }
43
+ const target = resolveWecomTarget(chatId);
44
+ if (!target) {
45
+ throw new Error(`Cannot resolve outbound target from "${to}"`);
46
+ }
47
+ console.log(`[wecom-outbound] Bot WS unavailable, sending via Agent HTTP API to ${JSON.stringify(target)} (accountId=${resolvedAccountId})`);
48
+ await sendAgentText({
49
+ agent,
50
+ toUser: target.touser,
51
+ toParty: target.toparty,
52
+ toTag: target.totag,
53
+ chatId: target.chatid,
54
+ text: content,
55
+ });
56
+ return {
57
+ channel: CHANNEL_ID,
58
+ messageId: `agent-${Date.now()}`,
59
+ chatId,
60
+ };
61
+ }
62
+ // 企业微信频道元数据
63
+ const meta = {
64
+ id: CHANNEL_ID,
65
+ label: "企业微信",
66
+ selectionLabel: "企业微信 (WeCom)",
67
+ detailLabel: "企业微信智能机器人",
68
+ docsPath: `/channels/${CHANNEL_ID}`,
69
+ docsLabel: CHANNEL_ID,
70
+ blurb: "企业微信智能机器人接入插件",
71
+ systemImage: "message.fill",
72
+ };
73
+ export const wecomPlugin = {
74
+ id: CHANNEL_ID,
75
+ meta: {
76
+ ...meta,
77
+ quickstartAllowFrom: true,
78
+ },
79
+ pairing: {
80
+ idLabel: "wecomUserId",
81
+ normalizeAllowEntry: (entry) => entry.replace(new RegExp(`^(${CHANNEL_ID}|user):`, "i"), "").trim(),
82
+ notifyApproval: async ({ cfg, id }) => {
83
+ // sendWeComMessage({
84
+ // to: id,
85
+ // content: " pairing approved",
86
+ // accountId: cfg.accountId,
87
+ // });
88
+ // Pairing approved for user
89
+ },
90
+ },
91
+ setupWizard: wecomSetupWizard,
92
+ setup: wecomSetupAdapter,
93
+ capabilities: {
94
+ chatTypes: ["direct", "group"],
95
+ reactions: false,
96
+ threads: false,
97
+ media: true,
98
+ nativeCommands: false,
99
+ blockStreaming: true,
100
+ },
101
+ reload: { configPrefixes: [`channels.${CHANNEL_ID}`] },
102
+ config: {
103
+ // 多账号:列出所有账户 ID
104
+ listAccountIds: (cfg) => listWeComAccountIds(cfg),
105
+ // 多账号:按 accountId 解析账户配置
106
+ resolveAccount: (cfg, accountId) => resolveWeComAccountMulti({ cfg, accountId }),
107
+ // 多账号:获取默认账户 ID
108
+ defaultAccountId: (cfg) => resolveDefaultWeComAccountId(cfg),
109
+ // 多账号:设置账户启用状态
110
+ setAccountEnabled: ({ cfg, accountId, enabled }) => {
111
+ if (!hasMultiAccounts(cfg)) {
112
+ // 单账号模式:设置顶层 enabled
113
+ const wecomConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
114
+ return {
115
+ ...cfg,
116
+ channels: {
117
+ ...cfg.channels,
118
+ [CHANNEL_ID]: {
119
+ ...wecomConfig,
120
+ enabled,
121
+ },
122
+ },
123
+ };
124
+ }
125
+ // 多账号模式:设置 accounts[accountId].enabled
126
+ const wecomConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
127
+ return {
128
+ ...cfg,
129
+ channels: {
130
+ ...cfg.channels,
131
+ [CHANNEL_ID]: {
132
+ ...wecomConfig,
133
+ accounts: {
134
+ ...wecomConfig.accounts,
135
+ [accountId]: {
136
+ ...wecomConfig.accounts?.[accountId],
137
+ enabled,
138
+ },
139
+ },
140
+ },
141
+ },
142
+ };
143
+ },
144
+ // 多账号:删除账户
145
+ deleteAccount: ({ cfg, accountId }) => {
146
+ if (!hasMultiAccounts(cfg)) {
147
+ // 单账号模式:删除整个 wecom 配置
148
+ const next = { ...cfg };
149
+ const nextChannels = { ...cfg.channels };
150
+ delete nextChannels[CHANNEL_ID];
151
+ if (Object.keys(nextChannels).length > 0) {
152
+ next.channels = nextChannels;
153
+ }
154
+ else {
155
+ delete next.channels;
156
+ }
157
+ return next;
158
+ }
159
+ // 删除指定账号
160
+ const wecomConfig = cfg.channels?.[CHANNEL_ID];
161
+ const accounts = { ...wecomConfig?.accounts };
162
+ delete accounts[accountId];
163
+ return {
164
+ ...cfg,
165
+ channels: {
166
+ ...cfg.channels,
167
+ [CHANNEL_ID]: {
168
+ ...wecomConfig,
169
+ accounts: Object.keys(accounts).length > 0 ? accounts : undefined,
170
+ },
171
+ },
172
+ };
173
+ },
174
+ // 检查是否已配置(Bot / Agent / botWebhook 凭证之一即可)
175
+ isConfigured: (account) => Boolean(account.botId?.trim() && account.secret?.trim()) ||
176
+ Boolean(account.agent?.configured) ||
177
+ Boolean(account.token?.trim() && account.encodingAESKey?.trim()),
178
+ // 描述账户信息
179
+ describeAccount: (account) => ({
180
+ accountId: account.accountId,
181
+ name: account.name,
182
+ enabled: account.enabled,
183
+ configured: Boolean(account.botId?.trim() && account.secret?.trim()) || Boolean(account.agent?.configured),
184
+ botId: account.botId,
185
+ websocketUrl: account.websocketUrl,
186
+ agentConfigured: Boolean(account.agent?.configured),
187
+ }),
188
+ // 解析允许来源列表(多账号:按 accountId 解析)
189
+ resolveAllowFrom: ({ cfg, accountId }) => {
190
+ const account = resolveWeComAccountMulti({ cfg, accountId });
191
+ return (account.config.allowFrom ?? []).map((entry) => String(entry));
192
+ },
193
+ // 格式化允许来源列表
194
+ formatAllowFrom: ({ allowFrom }) => allowFrom
195
+ .map((entry) => String(entry).trim())
196
+ .filter(Boolean),
197
+ },
198
+ security: {
199
+ resolveDmPolicy: ({ cfg, accountId, account }) => {
200
+ const result = buildAccountScopedDmSecurityPolicy({
201
+ cfg,
202
+ channelKey: CHANNEL_ID,
203
+ accountId,
204
+ fallbackAccountId: account.accountId,
205
+ policy: account.config.dmPolicy,
206
+ allowFrom: account.config.allowFrom ?? [],
207
+ defaultPolicy: "open",
208
+ policyPathSuffix: "dmPolicy",
209
+ approveHint: formatPairingApproveHint(CHANNEL_ID),
210
+ normalizeEntry: (raw) => raw.replace(new RegExp(`^${CHANNEL_ID}:`, "i"), "").trim(),
211
+ });
212
+ return result;
213
+ },
214
+ collectWarnings: ({ cfg, accountId }) => {
215
+ const account = resolveWeComAccountMulti({ cfg, accountId });
216
+ const warnings = [];
217
+ // 动态构造配置路径(区分单账号 / 多账号)
218
+ const isMulti = hasMultiAccounts(cfg);
219
+ const basePath = isMulti && accountId
220
+ ? `channels.${CHANNEL_ID}.accounts.${accountId}.`
221
+ : `channels.${CHANNEL_ID}.`;
222
+ // DM 策略警告
223
+ const dmPolicy = account.config.dmPolicy ?? "open";
224
+ if (dmPolicy === "open") {
225
+ const hasWildcard = (account.config.allowFrom ?? []).some((entry) => String(entry).trim() === "*");
226
+ if (!hasWildcard) {
227
+ warnings.push(`- 企业微信[${account.accountId}]私信:dmPolicy="open" 但 allowFrom 未包含 "*"。任何人都可以发消息,但允许列表为空可能导致意外行为。建议设置 ${basePath}allowFrom=["*"] 或使用 dmPolicy="pairing"。`);
228
+ }
229
+ }
230
+ // 群组策略警告
231
+ const defaultGroupPolicy = cfg.channels?.defaults?.groupPolicy;
232
+ const groupPolicy = account.config.groupPolicy ?? defaultGroupPolicy ?? "open";
233
+ if (groupPolicy === "open") {
234
+ warnings.push(`- 企业微信[${account.accountId}]群组:groupPolicy="open" 允许所有群组中的成员触发。设置 ${basePath}groupPolicy="allowlist" + ${basePath}groupAllowFrom 来限制群组。`);
235
+ }
236
+ return warnings;
237
+ },
238
+ },
239
+ messaging: {
240
+ normalizeTarget: (target) => {
241
+ const trimmed = target.trim();
242
+ if (!trimmed)
243
+ return undefined;
244
+ return trimmed;
245
+ },
246
+ targetResolver: {
247
+ looksLikeId: (id) => {
248
+ const trimmed = id?.trim();
249
+ return Boolean(trimmed);
250
+ },
251
+ hint: "<userId|groupId>",
252
+ },
253
+ },
254
+ directory: {
255
+ self: async () => null,
256
+ listPeers: async () => [],
257
+ listGroups: async () => [],
258
+ },
259
+ outbound: {
260
+ deliveryMode: "gateway",
261
+ chunker: (text, limit) => getWeComRuntime().channel.text.chunkMarkdownText(text, limit),
262
+ textChunkLimit: TEXT_CHUNK_LIMIT,
263
+ sendText: async ({ to, text, accountId, cfg }) => {
264
+ return sendWeComMessage({ to, content: text, accountId: accountId ?? undefined, cfg });
265
+ },
266
+ sendMedia: async ({ to, text, mediaUrl, mediaLocalRoots, accountId, cfg }) => {
267
+ const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
268
+ const channelPrefix = new RegExp(`^${CHANNEL_ID}:`, "i");
269
+ const chatId = to.replace(channelPrefix, "");
270
+ // 如果没有 mediaUrl,fallback 为纯文本
271
+ if (!mediaUrl) {
272
+ return sendWeComMessage({ to, content: text || "", accountId: resolvedAccountId, cfg });
273
+ }
274
+ // ── 尝试 Bot WebSocket ──
275
+ const wsClient = getWeComWebSocket(resolvedAccountId);
276
+ if (wsClient?.isConnected) {
277
+ const result = await uploadAndSendMedia({
278
+ wsClient,
279
+ mediaUrl,
280
+ chatId,
281
+ mediaLocalRoots,
282
+ });
283
+ if (result.rejected) {
284
+ return sendWeComMessage({ to, content: `⚠️ ${result.rejectReason}`, accountId: resolvedAccountId, cfg });
285
+ }
286
+ if (!result.ok) {
287
+ const fallbackContent = text
288
+ ? `${text}\n📎 ${mediaUrl}`
289
+ : `📎 ${mediaUrl}`;
290
+ return sendWeComMessage({ to, content: fallbackContent, accountId: resolvedAccountId, cfg });
291
+ }
292
+ if (text) {
293
+ await sendWeComMessage({ to, content: text, accountId: resolvedAccountId, cfg });
294
+ }
295
+ if (result.downgradeNote) {
296
+ await sendWeComMessage({ to, content: `ℹ️ ${result.downgradeNote}`, accountId: resolvedAccountId, cfg });
297
+ }
298
+ return {
299
+ channel: CHANNEL_ID,
300
+ messageId: result.messageId,
301
+ chatId,
302
+ };
303
+ }
304
+ // ── 回退到 Agent HTTP API ──
305
+ if (!cfg) {
306
+ throw new Error(`WSClient not connected for account ${resolvedAccountId} and no config available for Agent fallback`);
307
+ }
308
+ const account = resolveWeComAccountMulti({ cfg, accountId: resolvedAccountId });
309
+ const agent = account.agent;
310
+ if (!agent?.configured) {
311
+ throw new Error(`WSClient not connected for account ${resolvedAccountId} and Agent mode is not configured. ` +
312
+ `Please configure either Bot (botId + secret) or Agent (corpId + corpSecret + agentId).`);
313
+ }
314
+ // Agent 模式:文本 fallback(Agent HTTP API 不支持直接发 mediaUrl,需先上传)
315
+ const target = resolveWecomTarget(chatId);
316
+ if (!target) {
317
+ throw new Error(`Cannot resolve outbound target from "${to}"`);
318
+ }
319
+ console.log(`[wecom-outbound] Bot WS unavailable, sending media via Agent HTTP API to ${JSON.stringify(target)}`);
320
+ // 尝试下载并上传媒体到企微
321
+ try {
322
+ const mediaResponse = await fetch(mediaUrl);
323
+ if (mediaResponse.ok) {
324
+ const buffer = Buffer.from(await mediaResponse.arrayBuffer());
325
+ const filename = mediaUrl.split('/').pop() || 'file.bin';
326
+ const mediaId = await uploadAgentMedia({
327
+ agent,
328
+ type: 'file',
329
+ buffer,
330
+ filename,
331
+ });
332
+ await sendAgentMedia({
333
+ agent,
334
+ toUser: target.touser,
335
+ toParty: target.toparty,
336
+ toTag: target.totag,
337
+ chatId: target.chatid,
338
+ mediaId,
339
+ mediaType: 'file',
340
+ });
341
+ if (text) {
342
+ await sendAgentText({ agent, toUser: target.touser, toParty: target.toparty, toTag: target.totag, chatId: target.chatid, text });
343
+ }
344
+ return { channel: CHANNEL_ID, messageId: `agent-media-${Date.now()}`, chatId };
345
+ }
346
+ }
347
+ catch (err) {
348
+ console.warn(`[wecom-outbound] Agent media upload failed, falling back to text:`, err);
349
+ }
350
+ // 媒体上传失败,降级为文本 + URL
351
+ const fallbackContent = text ? `${text}\n📎 ${mediaUrl}` : `📎 ${mediaUrl}`;
352
+ await sendAgentText({ agent, toUser: target.touser, toParty: target.toparty, toTag: target.totag, chatId: target.chatid, text: fallbackContent });
353
+ return { channel: CHANNEL_ID, messageId: `agent-${Date.now()}`, chatId };
354
+ },
355
+ },
356
+ status: {
357
+ defaultRuntime: {
358
+ accountId: DEFAULT_ACCOUNT_ID,
359
+ running: false,
360
+ lastStartAt: null,
361
+ lastStopAt: null,
362
+ lastError: null,
363
+ },
364
+ collectStatusIssues: (accounts) => accounts.flatMap((entry) => {
365
+ const accountId = String(entry.accountId ?? DEFAULT_ACCOUNT_ID);
366
+ const enabled = entry.enabled !== false;
367
+ const configured = entry.configured === true;
368
+ if (!enabled) {
369
+ return [];
370
+ }
371
+ const issues = [];
372
+ if (!configured) {
373
+ issues.push({
374
+ channel: CHANNEL_ID,
375
+ accountId,
376
+ kind: "config",
377
+ message: "企业微信机器人 ID 或 Secret 未配置",
378
+ fix: "Run: openclaw channels add wecom --bot-id <id> --secret <secret>",
379
+ });
380
+ }
381
+ return issues;
382
+ }),
383
+ buildChannelSummary: ({ snapshot }) => ({
384
+ configured: snapshot.configured ?? false,
385
+ running: snapshot.running ?? false,
386
+ lastStartAt: snapshot.lastStartAt ?? null,
387
+ lastStopAt: snapshot.lastStopAt ?? null,
388
+ lastError: snapshot.lastError ?? null,
389
+ }),
390
+ probeAccount: async () => {
391
+ return { ok: true, status: 200 };
392
+ },
393
+ buildAccountSnapshot: ({ account, runtime }) => {
394
+ const configured = Boolean(account.botId?.trim() &&
395
+ account.secret?.trim()) || Boolean(account.agent?.configured);
396
+ return {
397
+ accountId: account.accountId,
398
+ name: account.name,
399
+ enabled: account.enabled,
400
+ configured,
401
+ running: runtime?.running ?? false,
402
+ lastStartAt: runtime?.lastStartAt ?? null,
403
+ lastStopAt: runtime?.lastStopAt ?? null,
404
+ lastError: runtime?.lastError ?? null,
405
+ };
406
+ },
407
+ },
408
+ gateway: {
409
+ startAccount: async (ctx) => {
410
+ // 多账号:按 accountId 解析账号配置
411
+ const account = resolveWeComAccountMulti({ cfg: ctx.cfg, accountId: ctx.accountId });
412
+ // 读取连接模式(默认 websocket)
413
+ const connectionMode = account.config.connectionMode ?? "websocket";
414
+ ctx.log?.info(`starting wecom[${ctx.accountId}] (name: ${account.name}, mode: ${connectionMode})`);
415
+ // ── Agent target 注册 ──────────────────────────────────────────
416
+ const agent = account.agent;
417
+ if (agent?.configured) {
418
+ const isMulti = hasMultiAccounts(ctx.cfg);
419
+ const defaultId = resolveDefaultWeComAccountId(ctx.cfg);
420
+ const isDefault = ctx.accountId === defaultId;
421
+ const paths = isMulti
422
+ ? [
423
+ `${WEBHOOK_PATHS.AGENT_PLUGIN}/${ctx.accountId}`,
424
+ `${WEBHOOK_PATHS.AGENT}/${ctx.accountId}`,
425
+ // 默认账号额外注册 /default 别名路径
426
+ ...(isDefault && ctx.accountId !== DEFAULT_ACCOUNT_ID
427
+ ? [
428
+ `${WEBHOOK_PATHS.AGENT_PLUGIN}/${DEFAULT_ACCOUNT_ID}`,
429
+ `${WEBHOOK_PATHS.AGENT}/${DEFAULT_ACCOUNT_ID}`,
430
+ ]
431
+ : []),
432
+ WEBHOOK_PATHS.AGENT_PLUGIN,
433
+ WEBHOOK_PATHS.AGENT,
434
+ ]
435
+ : [
436
+ // 单账号模式:同时注册 /default 路径以支持显式指定
437
+ WEBHOOK_PATHS.AGENT_PLUGIN,
438
+ WEBHOOK_PATHS.AGENT,
439
+ `${WEBHOOK_PATHS.AGENT_PLUGIN}/${DEFAULT_ACCOUNT_ID}`,
440
+ `${WEBHOOK_PATHS.AGENT}/${DEFAULT_ACCOUNT_ID}`,
441
+ ];
442
+ for (const p of paths) {
443
+ registerAgentWebhookTarget({
444
+ agent,
445
+ config: ctx.cfg,
446
+ runtime: {
447
+ log: ctx.log?.info ? (msg) => ctx.log.info(msg) : undefined,
448
+ error: ctx.log?.error ? (msg) => ctx.log.error(msg) : undefined,
449
+ },
450
+ path: p,
451
+ });
452
+ }
453
+ ctx.log?.info(`[${ctx.accountId}] wecom agent webhook registered at ${paths.join(", ")}`);
454
+ // 账号生命周期结束时清理
455
+ ctx.abortSignal.addEventListener("abort", () => {
456
+ deregisterAgentWebhookTarget(agent.accountId);
457
+ }, { once: true });
458
+ }
459
+ // ── Bot WebSocket 监听(需要 botId + secret)──────────────────
460
+ const hasBotCredentials = Boolean(account.botId?.trim() && account.secret?.trim());
461
+ if (hasBotCredentials) {
462
+ return monitorWeComProvider({
463
+ account,
464
+ config: ctx.cfg,
465
+ runtime: ctx.runtime,
466
+ abortSignal: ctx.abortSignal,
467
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- SDK 类型签名在不同版本间存在差异
468
+ setStatus: ctx.setStatus,
469
+ });
470
+ }
471
+ else if (connectionMode === "webhook") {
472
+ // ── Webhook 模式 ──────────────────────────────────────────────
473
+ const webhookAccount = {
474
+ ...account,
475
+ connectionMode: "webhook",
476
+ token: account.config.token ?? "",
477
+ encodingAESKey: account.config.encodingAESKey ?? "",
478
+ receiveId: account.config.receiveId ?? "",
479
+ welcomeText: account.config.welcomeText,
480
+ };
481
+ const gatewayCtx = {
482
+ account: webhookAccount,
483
+ config: ctx.cfg,
484
+ runtime: ctx.runtime,
485
+ abortSignal: ctx.abortSignal,
486
+ setStatus: ctx.setStatus,
487
+ log: ctx.log,
488
+ accountId: ctx.accountId,
489
+ };
490
+ startWebhookGateway(gatewayCtx);
491
+ // 等待 abortSignal 停止后清理
492
+ await new Promise((resolve) => {
493
+ if (ctx.abortSignal.aborted) {
494
+ stopWebhookGateway(gatewayCtx);
495
+ resolve();
496
+ return;
497
+ }
498
+ ctx.abortSignal.addEventListener("abort", () => {
499
+ stopWebhookGateway(gatewayCtx);
500
+ resolve();
501
+ }, { once: true });
502
+ });
503
+ return;
504
+ }
505
+ // Agent-only:无 Bot,等待 abort 信号
506
+ return new Promise((resolve) => {
507
+ ctx.abortSignal.addEventListener("abort", () => resolve(), { once: true });
508
+ });
509
+ },
510
+ logoutAccount: async ({ cfg, accountId }) => {
511
+ const resolvedAccountId = accountId ?? DEFAULT_ACCOUNT_ID;
512
+ const isMulti = hasMultiAccounts(cfg);
513
+ let nextCfg = { ...cfg };
514
+ let cleared = false;
515
+ let changed = false;
516
+ if (!isMulti) {
517
+ // 单账号模式:删除顶层 botId/secret
518
+ const wecomConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
519
+ const nextWecom = { ...wecomConfig };
520
+ if (nextWecom.botId || nextWecom.secret) {
521
+ delete nextWecom.botId;
522
+ delete nextWecom.secret;
523
+ cleared = true;
524
+ changed = true;
525
+ }
526
+ if (changed) {
527
+ if (Object.keys(nextWecom).length > 0) {
528
+ nextCfg.channels = { ...nextCfg.channels, [CHANNEL_ID]: nextWecom };
529
+ }
530
+ else {
531
+ const nextChannels = { ...nextCfg.channels };
532
+ delete nextChannels[CHANNEL_ID];
533
+ if (Object.keys(nextChannels).length > 0) {
534
+ nextCfg.channels = nextChannels;
535
+ }
536
+ else {
537
+ delete nextCfg.channels;
538
+ }
539
+ }
540
+ }
541
+ }
542
+ else {
543
+ // 多账号模式:删除指定账号的 botId/secret
544
+ const wecomConfig = (cfg.channels?.[CHANNEL_ID] ?? {});
545
+ const accountCfg = wecomConfig.accounts?.[resolvedAccountId];
546
+ if (accountCfg?.botId || accountCfg?.secret) {
547
+ const nextAccount = { ...accountCfg };
548
+ delete nextAccount.botId;
549
+ delete nextAccount.secret;
550
+ cleared = true;
551
+ changed = true;
552
+ const nextAccounts = { ...wecomConfig.accounts };
553
+ if (Object.keys(nextAccount).length > 0) {
554
+ nextAccounts[resolvedAccountId] = nextAccount;
555
+ }
556
+ else {
557
+ delete nextAccounts[resolvedAccountId];
558
+ }
559
+ nextCfg = {
560
+ ...cfg,
561
+ channels: {
562
+ ...cfg.channels,
563
+ [CHANNEL_ID]: {
564
+ ...wecomConfig,
565
+ accounts: Object.keys(nextAccounts).length > 0 ? nextAccounts : undefined,
566
+ },
567
+ },
568
+ };
569
+ }
570
+ }
571
+ if (changed) {
572
+ await getWeComRuntime().config.writeConfigFile(nextCfg);
573
+ }
574
+ const resolved = resolveWeComAccountMulti({ cfg: changed ? nextCfg : cfg, accountId: resolvedAccountId });
575
+ const loggedOut = !resolved.botId && !resolved.secret;
576
+ return { cleared, envToken: false, loggedOut };
577
+ },
578
+ },
579
+ };
@@ -0,0 +1,31 @@
1
+ type QueueStatus = "queued" | "immediate";
2
+ /**
3
+ * 构建队列键
4
+ *
5
+ * 使用 accountId + chatId 作为维度,确保同一会话中的消息串行处理。
6
+ * 不同会话之间互不影响,可以并行处理。
7
+ */
8
+ export declare function buildQueueKey(accountId: string, chatId: string): string;
9
+ /**
10
+ * 检查指定会话是否有正在处理的任务
11
+ */
12
+ export declare function hasActiveTask(key: string): boolean;
13
+ /**
14
+ * 将任务加入串行队列
15
+ *
16
+ * 如果队列中已有任务(status="queued"),新任务会排队等待;
17
+ * 如果队列为空(status="immediate"),任务立即执行。
18
+ *
19
+ * 即使前一个任务失败,后续任务仍会继续执行(.then(task, task))。
20
+ */
21
+ export declare function enqueueWeComChatTask(params: {
22
+ accountId: string;
23
+ chatId: string;
24
+ task: () => Promise<void>;
25
+ }): {
26
+ status: QueueStatus;
27
+ promise: Promise<void>;
28
+ };
29
+ /** @internal 测试专用:重置所有队列状态 */
30
+ export declare function _resetChatQueueState(): void;
31
+ export {};
@@ -0,0 +1,45 @@
1
+ const chatQueues = new Map();
2
+ /**
3
+ * 构建队列键
4
+ *
5
+ * 使用 accountId + chatId 作为维度,确保同一会话中的消息串行处理。
6
+ * 不同会话之间互不影响,可以并行处理。
7
+ */
8
+ export function buildQueueKey(accountId, chatId) {
9
+ return `${accountId}:${chatId}`;
10
+ }
11
+ /**
12
+ * 检查指定会话是否有正在处理的任务
13
+ */
14
+ export function hasActiveTask(key) {
15
+ return chatQueues.has(key);
16
+ }
17
+ /**
18
+ * 将任务加入串行队列
19
+ *
20
+ * 如果队列中已有任务(status="queued"),新任务会排队等待;
21
+ * 如果队列为空(status="immediate"),任务立即执行。
22
+ *
23
+ * 即使前一个任务失败,后续任务仍会继续执行(.then(task, task))。
24
+ */
25
+ export function enqueueWeComChatTask(params) {
26
+ const { accountId, chatId, task } = params;
27
+ const key = buildQueueKey(accountId, chatId);
28
+ const prev = chatQueues.get(key) ?? Promise.resolve();
29
+ const status = chatQueues.has(key) ? "queued" : "immediate";
30
+ // continue queue even if previous task failed
31
+ const next = prev.then(task, task);
32
+ chatQueues.set(key, next);
33
+ const cleanup = () => {
34
+ // 只有当前任务仍是队列末尾时才清理,避免误删后续任务
35
+ if (chatQueues.get(key) === next) {
36
+ chatQueues.delete(key);
37
+ }
38
+ };
39
+ next.then(cleanup, cleanup);
40
+ return { status, promise: next };
41
+ }
42
+ /** @internal 测试专用:重置所有队列状态 */
43
+ export function _resetChatQueueState() {
44
+ chatQueues.clear();
45
+ }