@dingxiang-me/openclaw-wechat 1.7.2 → 2.0.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 (59) hide show
  1. package/CHANGELOG.md +151 -0
  2. package/README.en.md +379 -11
  3. package/README.md +620 -12
  4. package/docs/channels/wecom.md +181 -3
  5. package/openclaw.plugin.json +148 -5
  6. package/package.json +9 -5
  7. package/src/core/delivery-router.js +2 -0
  8. package/src/core/stream-manager.js +13 -2
  9. package/src/core.js +96 -6
  10. package/src/wecom/account-config-core.js +2 -0
  11. package/src/wecom/account-config.js +12 -3
  12. package/src/wecom/agent-context.js +7 -1
  13. package/src/wecom/agent-dispatch-executor.js +13 -1
  14. package/src/wecom/agent-dispatch-fallback.js +23 -0
  15. package/src/wecom/agent-inbound-dispatch.js +1 -1
  16. package/src/wecom/agent-inbound-processor.js +33 -2
  17. package/src/wecom/agent-late-reply-runtime.js +31 -1
  18. package/src/wecom/agent-runtime-context.js +3 -0
  19. package/src/wecom/agent-webhook-handler.js +5 -0
  20. package/src/wecom/api-client-core.js +1 -1
  21. package/src/wecom/bot-context.js +7 -1
  22. package/src/wecom/bot-dispatch-fallback.js +34 -3
  23. package/src/wecom/bot-dispatch-handlers.js +47 -4
  24. package/src/wecom/bot-inbound-dispatch-runtime.js +10 -0
  25. package/src/wecom/bot-inbound-executor-helpers.js +11 -4
  26. package/src/wecom/bot-inbound-executor.js +34 -0
  27. package/src/wecom/bot-long-connection-manager.js +971 -0
  28. package/src/wecom/bot-reply-runtime.js +36 -6
  29. package/src/wecom/bot-runtime-context.js +3 -0
  30. package/src/wecom/bot-state-store.js +4 -5
  31. package/src/wecom/bot-webhook-dispatch.js +5 -0
  32. package/src/wecom/bot-webhook-handler.js +5 -0
  33. package/src/wecom/callback-health-diagnostics.js +86 -0
  34. package/src/wecom/channel-config-schema.js +242 -0
  35. package/src/wecom/channel-plugin.js +162 -4
  36. package/src/wecom/channel-status-state.js +150 -0
  37. package/src/wecom/command-handlers.js +6 -0
  38. package/src/wecom/command-status-text.js +32 -3
  39. package/src/wecom/doc-client.js +537 -0
  40. package/src/wecom/doc-schema.js +380 -0
  41. package/src/wecom/doc-tool.js +833 -0
  42. package/src/wecom/outbound-active-stream.js +17 -10
  43. package/src/wecom/outbound-delivery.js +49 -0
  44. package/src/wecom/plugin-account-policy-services.js +4 -1
  45. package/src/wecom/plugin-composition.js +2 -0
  46. package/src/wecom/plugin-constants.js +1 -1
  47. package/src/wecom/plugin-delivery-inbound-services.js +4 -0
  48. package/src/wecom/plugin-processing-deps.js +5 -0
  49. package/src/wecom/plugin-route-runtime-deps.js +2 -0
  50. package/src/wecom/plugin-services.js +37 -0
  51. package/src/wecom/register-runtime.js +20 -1
  52. package/src/wecom/request-parsers.js +1 -0
  53. package/src/wecom/route-registration.js +4 -1
  54. package/src/wecom/session-reset.js +168 -0
  55. package/src/wecom/text-format.js +22 -5
  56. package/src/wecom/text-inbound-scheduler.js +1 -1
  57. package/src/wecom/thinking-parser.js +74 -0
  58. package/src/wecom/voice-transcription-process.js +80 -8
  59. package/src/wecom/voice-transcription.js +11 -0
@@ -4,6 +4,13 @@ function assertFunction(name, value) {
4
4
  }
5
5
  }
6
6
 
7
+ function isTimeoutLikeReason(reason) {
8
+ return String(reason?.message || reason || "")
9
+ .trim()
10
+ .toLowerCase()
11
+ .includes("timed out");
12
+ }
13
+
7
14
  export function createWecomBotDispatchState() {
8
15
  return {
9
16
  blockText: "",
@@ -49,6 +56,7 @@ export function createWecomBotLateReplyRuntime({
49
56
  safeDeliverReply,
50
57
  runLateReplyWatcher,
51
58
  activeWatchers,
59
+ clearSessionStoreEntry = null,
52
60
  now = () => Date.now(),
53
61
  randomToken = () => Math.random().toString(36).slice(2, 8),
54
62
  } = {}) {
@@ -64,6 +72,24 @@ export function createWecomBotLateReplyRuntime({
64
72
 
65
73
  let lateReplyWatcherPromise = null;
66
74
 
75
+ const autoResetTimedOutSession = async (reason) => {
76
+ if (typeof clearSessionStoreEntry !== "function" || !isTimeoutLikeReason(reason)) return false;
77
+ try {
78
+ const result = await clearSessionStoreEntry({
79
+ storePath,
80
+ sessionKey: sessionId,
81
+ logger,
82
+ });
83
+ logger?.info?.(
84
+ `wecom(bot): auto-reset timed out session=${sessionId} cleared=${result?.cleared === true ? "yes" : "no"}`,
85
+ );
86
+ return result?.cleared === true;
87
+ } catch (err) {
88
+ logger?.warn?.(`wecom(bot): failed to auto-reset timed out session=${sessionId}: ${String(err?.message || err)}`);
89
+ return false;
90
+ }
91
+ };
92
+
67
93
  const readTranscriptFallbackResult = async ({
68
94
  runtimeStorePath = storePath,
69
95
  runtimeSessionId = sessionId,
@@ -128,12 +154,16 @@ export function createWecomBotLateReplyRuntime({
128
154
  if (dispatchState.streamFinished) return;
129
155
  const reasonText = String(watchErr?.message || watchErr || "");
130
156
  const isTimeout = reasonText.includes("timed out");
131
- await safeDeliverReply(
132
- isTimeout
133
- ? "抱歉,当前模型请求超时或网络不稳定,请稍后重试。"
134
- : `抱歉,当前模型请求超时或网络不稳定,请稍后重试。\n故障信息: ${reasonText.slice(0, 160)}`,
135
- isTimeout ? "late-timeout-fallback" : "late-watcher-error",
136
- );
157
+ try {
158
+ await safeDeliverReply(
159
+ isTimeout
160
+ ? "抱歉,当前模型请求超时或网络不稳定,请稍后重试。"
161
+ : `抱歉,当前模型请求超时或网络不稳定,请稍后重试。\n故障信息: ${reasonText.slice(0, 160)}`,
162
+ isTimeout ? "late-timeout-fallback" : "late-watcher-error",
163
+ );
164
+ } finally {
165
+ await autoResetTimedOutSession(reasonText);
166
+ }
137
167
  },
138
168
  }).finally(() => {
139
169
  lateReplyWatcherPromise = null;
@@ -16,6 +16,7 @@ export async function prepareWecomBotRuntimeContext({
16
16
  msgId = "",
17
17
  messageText = "",
18
18
  commandBody = "",
19
+ commandAuthorized = false,
19
20
  originalContent = "",
20
21
  fromAddress = "",
21
22
  groupChatPolicy = {},
@@ -82,6 +83,8 @@ export async function prepareWecomBotRuntimeContext({
82
83
  messageText,
83
84
  originalContent,
84
85
  commandBody,
86
+ commandAuthorized,
87
+ commandSource: commandAuthorized ? "text" : "",
85
88
  fromAddress,
86
89
  sessionId,
87
90
  accountId,
@@ -84,13 +84,13 @@ export function createWecomBotStateStore({
84
84
  return stream;
85
85
  }
86
86
 
87
- function updateStream(streamId, content, { append = false, finished = false, msgItem } = {}) {
88
- return streamManager.update(streamId, content, { append, finished, msgItem });
87
+ function updateStream(streamId, content, { append = false, finished = false, msgItem, thinkingContent } = {}) {
88
+ return streamManager.update(streamId, content, { append, finished, msgItem, thinkingContent });
89
89
  }
90
90
 
91
- function finishStream(streamId, content, { msgItem } = {}) {
91
+ function finishStream(streamId, content, { msgItem, thinkingContent } = {}) {
92
92
  const normalizedStreamId = String(streamId ?? "").trim();
93
- const stream = streamManager.finish(normalizedStreamId, content, { msgItem });
93
+ const stream = streamManager.finish(normalizedStreamId, content, { msgItem, thinkingContent });
94
94
  if (stream) {
95
95
  const sessionId = streamToSession.get(normalizedStreamId);
96
96
  if (sessionId) unregisterActiveStream(sessionId, normalizedStreamId);
@@ -184,4 +184,3 @@ export function createWecomBotStateStore({
184
184
  startCleanup,
185
185
  };
186
186
  }
187
-
@@ -25,6 +25,7 @@ function buildEncryptedStreamPayload({
25
25
  content,
26
26
  finish,
27
27
  msgItem,
28
+ thinkingContent,
28
29
  feedbackId,
29
30
  }) {
30
31
  const streamPayload = {
@@ -35,6 +36,9 @@ function buildEncryptedStreamPayload({
35
36
  if (Array.isArray(msgItem) && msgItem.length > 0) {
36
37
  streamPayload.msg_item = msgItem;
37
38
  }
39
+ if (String(thinkingContent ?? "").trim()) {
40
+ streamPayload.thinking_content = String(thinkingContent).trim();
41
+ }
38
42
  if (feedbackId) {
39
43
  streamPayload.feedback = { id: feedbackId };
40
44
  }
@@ -113,6 +117,7 @@ export function createWecomBotParsedDispatcher({
113
117
  content: stream?.content ?? "会话已过期",
114
118
  finish: stream ? stream.finished === true : true,
115
119
  msgItem: stream?.msgItem,
120
+ thinkingContent: stream?.thinkingContent,
116
121
  feedbackId,
117
122
  });
118
123
  sendEncryptedJson(res, encryptedResponse);
@@ -1,5 +1,6 @@
1
1
  import crypto from "node:crypto";
2
2
  import { createWecomBotParsedDispatcher } from "./bot-webhook-dispatch.js";
3
+ import { markWecomInboundActivity } from "./channel-status-state.js";
3
4
 
4
5
  export function createWecomBotWebhookHandler({
5
6
  api,
@@ -167,6 +168,10 @@ export function createWecomBotWebhookHandler({
167
168
  const parsed = parseWecomBotInboundMessage(incomingPayload);
168
169
  if (parsed && typeof parsed === "object") {
169
170
  parsed.accountId = String(matchedBotConfig.accountId ?? "default").trim().toLowerCase() || "default";
171
+ markWecomInboundActivity({
172
+ accountId: parsed.accountId,
173
+ timestamp: incomingPayload?.create_time ?? incomingPayload?.CreateTime,
174
+ });
170
175
  }
171
176
  api.logger.info?.(
172
177
  `wecom(bot): inbound ${describeWecomBotParsedMessage(parsed)} account=${matchedBotConfig.accountId || "default"}`,
@@ -0,0 +1,86 @@
1
+ function previewBody(body, maxLength = 120) {
2
+ return String(body ?? "").slice(0, Math.max(1, maxLength));
3
+ }
4
+
5
+ function isHtmlBody(body) {
6
+ return /<!doctype html|<html/i.test(String(body ?? ""));
7
+ }
8
+
9
+ export function diagnoseWecomCallbackHealth({
10
+ status,
11
+ body,
12
+ mode = "agent",
13
+ endpoint = "",
14
+ webhookPath = "",
15
+ gatewayPort = null,
16
+ location = "",
17
+ } = {}) {
18
+ const rawBody = String(body ?? "");
19
+ const preview = previewBody(rawBody);
20
+ const normalizedMode = String(mode ?? "agent").trim().toLowerCase() === "bot" ? "bot" : "agent";
21
+ const healthyMarker = normalizedMode === "bot" ? "wecom bot webhook" : "wecom webhook";
22
+ const healthy = status === 200 && rawBody.toLowerCase().includes(healthyMarker);
23
+ if (healthy) {
24
+ return {
25
+ ok: true,
26
+ detail: `status=${status} body=${preview}`,
27
+ data: null,
28
+ };
29
+ }
30
+
31
+ const hints = [];
32
+ let reason = "unexpected-response";
33
+ const effectivePath = String(webhookPath ?? "").trim() || String(endpoint ?? "").trim() || "/";
34
+ const authScopeHint =
35
+ normalizedMode === "bot"
36
+ ? "为 /wecom/*(以及 legacy /webhooks/wecom*)单独放行,或使用独立回调域名/端口"
37
+ : "为 /wecom/*(以及 legacy /webhooks/app*)单独放行,或使用独立回调域名/端口";
38
+
39
+ if (status === 404) {
40
+ reason = "route-not-found";
41
+ hints.push(`路径 ${effectivePath} 未命中${normalizedMode === "bot" ? " Bot" : ""}回调路由`);
42
+ } else if (status === 401 || status === 403) {
43
+ reason = "gateway-auth";
44
+ hints.push("回调路径被 Gateway Auth / Zero Trust / 反向代理鉴权拦截");
45
+ hints.push("企业微信回调与健康探测必须直达 webhook 路径,不能要求 Authorization、Cookie 或交互登录");
46
+ hints.push(authScopeHint);
47
+ } else if ([301, 302, 303, 307, 308].includes(Number(status))) {
48
+ reason = "redirect-auth";
49
+ hints.push("回调路径发生了重定向,通常被登录页、SSO 或前端路由接管");
50
+ if (location) hints.push(`重定向目标:${location}`);
51
+ hints.push("请让 webhook 路径直接反代到 OpenClaw 网关,不要跳转到登录页或前端应用");
52
+ } else if (status === 502 || status === 503 || status === 504) {
53
+ reason = "gateway-unreachable";
54
+ if (gatewayPort != null) {
55
+ hints.push(`网关 ${gatewayPort} 端口不可达或反向代理后端异常`);
56
+ } else {
57
+ hints.push("网关端口不可达或反向代理后端异常");
58
+ }
59
+ } else if (status === 200 && isHtmlBody(rawBody)) {
60
+ reason = "html-fallback";
61
+ hints.push("返回了 WebUI HTML,通常表示 webhook 路由未注册或 webhookPath 配置不一致");
62
+ if (webhookPath) {
63
+ const configPathHint =
64
+ normalizedMode === "bot"
65
+ ? `请确认 channels.wecom.bot.webhookPath=${webhookPath} 与企业微信后台回调地址完全一致`
66
+ : `请确认 channels.wecom.webhookPath=${webhookPath} 与企业微信后台回调地址完全一致`;
67
+ hints.push(configPathHint);
68
+ }
69
+ hints.push("确认插件已加载:plugins.entries.openclaw-wechat.enabled=true 且 plugins.allow 包含 openclaw-wechat");
70
+ }
71
+
72
+ return {
73
+ ok: false,
74
+ detail: `status=${status} body=${preview}${hints.length > 0 ? ` hint=${hints.join(";")}` : ""}`,
75
+ data: {
76
+ status,
77
+ reason,
78
+ mode: normalizedMode,
79
+ endpoint: endpoint || null,
80
+ webhookPath: webhookPath || null,
81
+ gatewayPort: gatewayPort == null ? null : gatewayPort,
82
+ location: location || null,
83
+ hints,
84
+ },
85
+ };
86
+ }
@@ -0,0 +1,242 @@
1
+ import pluginManifest from "../../openclaw.plugin.json" with { type: "json" };
2
+
3
+ function asObject(value) {
4
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
5
+ return value;
6
+ }
7
+
8
+ const manifestConfigSchema = asObject(pluginManifest?.configSchema);
9
+ const manifestUiHints = asObject(pluginManifest?.uiHints) ?? {};
10
+
11
+ const localizedUiHints = {
12
+ name: {
13
+ label: "渠道显示名",
14
+ help: "仅用于展示,不影响消息路由。",
15
+ },
16
+ enabled: {
17
+ label: "启用企业微信渠道",
18
+ help: "开启后才会接收/发送企业微信消息。",
19
+ },
20
+ corpId: {
21
+ label: "企业 ID(CorpId)",
22
+ placeholder: "wwxxxxxxxxxxxxxxxx",
23
+ },
24
+ corpSecret: {
25
+ label: "应用 Secret(CorpSecret)",
26
+ sensitive: true,
27
+ },
28
+ agentId: {
29
+ label: "应用 AgentId",
30
+ placeholder: "1000002",
31
+ },
32
+ callbackToken: {
33
+ label: "回调 Token",
34
+ sensitive: true,
35
+ },
36
+ callbackAesKey: {
37
+ label: "回调 EncodingAESKey",
38
+ sensitive: true,
39
+ },
40
+ webhookPath: {
41
+ label: "自建应用回调路径",
42
+ placeholder: "/wecom/callback",
43
+ },
44
+ outboundProxy: {
45
+ label: "WeCom 出站代理",
46
+ placeholder: "http://127.0.0.1:7890",
47
+ },
48
+ defaultAccount: {
49
+ label: "默认账号",
50
+ help: "文档工具等多账号能力未显式指定账号时优先使用该账号。",
51
+ },
52
+ tools: {
53
+ label: "工具能力",
54
+ help: "控制 OpenClaw 工具级能力是否启用。",
55
+ },
56
+ "tools.doc": {
57
+ label: "启用文档工具",
58
+ },
59
+ "tools.docAutoGrantRequesterCollaborator": {
60
+ label: "创建后自动加当前发送者为协作者",
61
+ help: "仅在 WeCom 会话中生效;创建文档后会把当前发送者自动加入协作者。",
62
+ },
63
+ accounts: {
64
+ label: "多账号配置",
65
+ help: "按账户 ID 管理多套企业微信配置。",
66
+ },
67
+ "accounts.*.enabled": {
68
+ label: "启用该账号",
69
+ },
70
+ "accounts.*.name": {
71
+ label: "账号名称",
72
+ },
73
+ "accounts.*.corpId": {
74
+ label: "账号 CorpId",
75
+ },
76
+ "accounts.*.corpSecret": {
77
+ label: "账号 CorpSecret",
78
+ sensitive: true,
79
+ },
80
+ "accounts.*.agentId": {
81
+ label: "账号 AgentId",
82
+ },
83
+ "accounts.*.callbackToken": {
84
+ label: "账号回调 Token",
85
+ sensitive: true,
86
+ },
87
+ "accounts.*.callbackAesKey": {
88
+ label: "账号回调 EncodingAESKey",
89
+ sensitive: true,
90
+ },
91
+ "accounts.*.webhookPath": {
92
+ label: "账号回调路径",
93
+ },
94
+ "accounts.*.tools": {
95
+ label: "账号工具能力",
96
+ },
97
+ "accounts.*.tools.doc": {
98
+ label: "启用该账号文档工具",
99
+ },
100
+ "accounts.*.tools.docAutoGrantRequesterCollaborator": {
101
+ label: "自动加当前发送者为协作者",
102
+ },
103
+ bot: {
104
+ label: "企业微信 Bot 模式",
105
+ help: "用于企业微信群机器人/Bot 回调与回包。",
106
+ },
107
+ "bot.enabled": {
108
+ label: "启用 Bot 模式",
109
+ },
110
+ "bot.token": {
111
+ label: "Bot Token",
112
+ sensitive: true,
113
+ },
114
+ "bot.encodingAesKey": {
115
+ label: "Bot EncodingAESKey",
116
+ sensitive: true,
117
+ },
118
+ "bot.webhookPath": {
119
+ label: "Bot 回调路径",
120
+ placeholder: "/wecom/bot/callback",
121
+ },
122
+ "bot.longConnection": {
123
+ label: "Bot 长连接",
124
+ help: "企业微信智能机器人长连接(WebSocket)模式,无需公网回调地址。",
125
+ },
126
+ "bot.longConnection.enabled": {
127
+ label: "启用 Bot 长连接",
128
+ },
129
+ "bot.longConnection.botId": {
130
+ label: "BotID",
131
+ },
132
+ "bot.longConnection.secret": {
133
+ label: "长连接 Secret",
134
+ sensitive: true,
135
+ },
136
+ "bot.longConnection.url": {
137
+ label: "长连接地址",
138
+ placeholder: "wss://openws.work.weixin.qq.com",
139
+ },
140
+ "bot.longConnection.pingIntervalMs": {
141
+ label: "心跳间隔(毫秒)",
142
+ },
143
+ "bot.longConnection.reconnectDelayMs": {
144
+ label: "重连基准延迟(毫秒)",
145
+ },
146
+ "bot.longConnection.maxReconnectDelayMs": {
147
+ label: "最大重连延迟(毫秒)",
148
+ },
149
+ "bot.replyTimeoutMs": {
150
+ label: "Bot 回复超时(毫秒)",
151
+ },
152
+ "bot.streamExpireMs": {
153
+ label: "Bot 流会话保留(毫秒)",
154
+ },
155
+ "bot.placeholderText": {
156
+ label: "Bot 首包占位文本",
157
+ },
158
+ "accounts.*.bot.longConnection": {
159
+ label: "账号 Bot 长连接",
160
+ },
161
+ "accounts.*.bot.longConnection.enabled": {
162
+ label: "启用该账号长连接",
163
+ },
164
+ "accounts.*.bot.longConnection.botId": {
165
+ label: "账号 BotID",
166
+ },
167
+ "accounts.*.bot.longConnection.secret": {
168
+ label: "账号长连接 Secret",
169
+ sensitive: true,
170
+ },
171
+ "accounts.*.bot.longConnection.url": {
172
+ label: "账号长连接地址",
173
+ },
174
+ "accounts.*.bot.longConnection.pingIntervalMs": {
175
+ label: "账号心跳间隔(毫秒)",
176
+ },
177
+ "accounts.*.bot.longConnection.reconnectDelayMs": {
178
+ label: "账号重连基准延迟(毫秒)",
179
+ },
180
+ "accounts.*.bot.longConnection.maxReconnectDelayMs": {
181
+ label: "账号最大重连延迟(毫秒)",
182
+ },
183
+ webhookBot: {
184
+ label: "Webhook Bot 出站回包",
185
+ },
186
+ "webhookBot.enabled": {
187
+ label: "启用 Webhook Bot 回包",
188
+ },
189
+ "webhookBot.url": {
190
+ label: "Webhook Bot URL",
191
+ },
192
+ "webhookBot.key": {
193
+ label: "Webhook Bot Key",
194
+ sensitive: true,
195
+ },
196
+ groupChat: {
197
+ label: "群聊触发策略",
198
+ },
199
+ "groupChat.triggerMode": {
200
+ label: "群聊触发模式",
201
+ },
202
+ dynamicAgent: {
203
+ label: "动态 Agent 路由",
204
+ },
205
+ dm: {
206
+ label: "私聊策略",
207
+ },
208
+ commands: {
209
+ label: "指令白名单",
210
+ },
211
+ events: {
212
+ label: "事件消息策略",
213
+ },
214
+ voiceTranscription: {
215
+ label: "语音转写",
216
+ },
217
+ "voiceTranscription.enabled": {
218
+ label: "启用语音转写",
219
+ },
220
+ "voiceTranscription.command": {
221
+ label: "本地转写命令",
222
+ placeholder: "whisper / whisper-cli",
223
+ },
224
+ "voiceTranscription.modelPath": {
225
+ label: "本地模型路径",
226
+ },
227
+ "voiceTranscription.language": {
228
+ label: "转写语言",
229
+ placeholder: "zh",
230
+ },
231
+ };
232
+
233
+ export const wecomChannelConfigSchema = manifestConfigSchema ?? {
234
+ type: "object",
235
+ additionalProperties: true,
236
+ properties: {},
237
+ };
238
+
239
+ export const wecomChannelConfigUiHints = {
240
+ ...manifestUiHints,
241
+ ...localizedUiHints,
242
+ };
@@ -1,9 +1,138 @@
1
+ import { wecomChannelConfigSchema, wecomChannelConfigUiHints } from "./channel-config-schema.js";
2
+ import {
3
+ getWecomChannelInboundActivity,
4
+ getWecomInboundActivity,
5
+ } from "./channel-status-state.js";
6
+
1
7
  function assertFunction(name, fn) {
2
8
  if (typeof fn !== "function") {
3
9
  throw new Error(`createWecomChannelPlugin: ${name} is required`);
4
10
  }
5
11
  }
6
12
 
13
+ function readString(value) {
14
+ const trimmed = String(value ?? "").trim();
15
+ return trimmed || "";
16
+ }
17
+
18
+ function readNumber(value) {
19
+ const num = Number(value);
20
+ return Number.isFinite(num) ? num : null;
21
+ }
22
+
23
+ function normalizeTimestampMs(value) {
24
+ if (value == null || value === "") return null;
25
+ const direct = Number(value);
26
+ if (Number.isFinite(direct) && direct > 0) {
27
+ return direct < 1e12 ? Math.floor(direct * 1000) : Math.floor(direct);
28
+ }
29
+ const parsed = Date.parse(String(value));
30
+ if (Number.isFinite(parsed) && parsed > 0) return Math.floor(parsed);
31
+ return null;
32
+ }
33
+
34
+ function resolveBotCallbackConfig(cfg, accountId = "default") {
35
+ const normalizedAccountId = readString(accountId).toLowerCase() || "default";
36
+ const channelConfig = cfg?.channels?.wecom;
37
+ const accountConfig = channelConfig?.accounts?.[normalizedAccountId];
38
+ const accountBot = accountConfig?.bot;
39
+ const channelBot = channelConfig?.bot;
40
+
41
+ const enabled =
42
+ accountBot?.enabled ??
43
+ channelBot?.enabled ??
44
+ false;
45
+ const token = readString(
46
+ accountBot?.token ??
47
+ accountBot?.callbackToken ??
48
+ channelBot?.token ??
49
+ channelBot?.callbackToken ??
50
+ channelConfig?.token ??
51
+ channelConfig?.callbackToken,
52
+ );
53
+ const aesKey = readString(
54
+ accountBot?.encodingAesKey ??
55
+ accountBot?.callbackAesKey ??
56
+ channelBot?.encodingAesKey ??
57
+ channelBot?.callbackAesKey ??
58
+ channelConfig?.encodingAesKey ??
59
+ channelConfig?.callbackAesKey,
60
+ );
61
+ const webhookPath = readString(
62
+ accountBot?.webhookPath ?? channelBot?.webhookPath,
63
+ );
64
+ const longConnection =
65
+ accountBot?.longConnection && typeof accountBot.longConnection === "object"
66
+ ? accountBot.longConnection
67
+ : channelBot?.longConnection && typeof channelBot.longConnection === "object"
68
+ ? channelBot.longConnection
69
+ : {};
70
+ const longConnectionEnabled = longConnection?.enabled === true;
71
+ const longConnectionBotId = readString(longConnection?.botId ?? longConnection?.botid);
72
+ const longConnectionSecret = readString(longConnection?.secret);
73
+
74
+ return {
75
+ enabled: enabled === true,
76
+ token,
77
+ aesKey,
78
+ webhookPath,
79
+ longConnectionEnabled,
80
+ longConnectionBotId,
81
+ longConnectionSecret,
82
+ };
83
+ }
84
+
85
+ function hasConfiguredBotCallback(cfg, accountId = "default") {
86
+ const bot = resolveBotCallbackConfig(cfg, accountId);
87
+ return (
88
+ bot.enabled &&
89
+ ((Boolean(bot.token) && Boolean(bot.aesKey)) ||
90
+ (bot.longConnectionEnabled && Boolean(bot.longConnectionBotId) && Boolean(bot.longConnectionSecret)))
91
+ );
92
+ }
93
+
94
+ function hasConfiguredAgentCredentials(account) {
95
+ return Boolean(
96
+ readString(account?.corpId) &&
97
+ readString(account?.corpSecret) &&
98
+ readNumber(account?.agentId),
99
+ );
100
+ }
101
+
102
+ function buildWecomAccountSnapshot(account, cfg, runtime = {}) {
103
+ const accountId = readString(account?.accountId).toLowerCase() || "default";
104
+ const agentConfigured = hasConfiguredAgentCredentials(account);
105
+ const botConfig = resolveBotCallbackConfig(cfg, accountId);
106
+ const botConfigured = hasConfiguredBotCallback(cfg, accountId);
107
+ const configured = agentConfigured || botConfigured;
108
+ const enabled = account?.enabled !== false;
109
+ const inboundActivity = getWecomInboundActivity(accountId);
110
+ const mode = agentConfigured && botConfigured ? "agent+bot" : botConfigured ? "bot" : "agent";
111
+ const running = runtime?.running ?? (enabled && configured);
112
+ const connected =
113
+ runtime?.connected ??
114
+ inboundActivity?.connected ??
115
+ (running && configured);
116
+ const lastInboundAt =
117
+ normalizeTimestampMs(runtime?.lastInboundAt ?? runtime?.lastInbound) ??
118
+ normalizeTimestampMs(inboundActivity?.lastInboundAtMs ?? inboundActivity?.lastInbound) ??
119
+ null;
120
+ const localizedName = accountId === "default" ? "默认账号" : accountId;
121
+ return {
122
+ ...runtime,
123
+ accountId,
124
+ name: readString(account?.name) || localizedName,
125
+ displayName: readString(account?.name) || localizedName,
126
+ enabled,
127
+ configured,
128
+ running,
129
+ connected,
130
+ lastInboundAt,
131
+ mode,
132
+ webhookPath: readString(account?.webhookPath) || botConfig.webhookPath || runtime?.webhookPath || undefined,
133
+ };
134
+ }
135
+
7
136
  export function createWecomChannelPlugin({
8
137
  listWecomAccountIds,
9
138
  getWecomConfig,
@@ -29,12 +158,16 @@ export function createWecomChannelPlugin({
29
158
  id: "wecom",
30
159
  meta: {
31
160
  id: "wecom",
32
- label: "WeCom",
33
- selectionLabel: "WeCom (企业微信自建应用)",
161
+ label: "企业微信 WeCom",
162
+ selectionLabel: "企业微信 WeCom(自建应用/Bot)",
34
163
  docsPath: "/channels/wecom",
35
- blurb: "Enterprise WeChat internal app via callback + send API.",
164
+ blurb: "企业微信消息通道(自建应用回调 + Bot 回调 + 发送 API)。",
36
165
  aliases: ["wework", "qiwei", "wxwork"],
37
166
  },
167
+ configSchema: {
168
+ schema: wecomChannelConfigSchema,
169
+ uiHints: wecomChannelConfigUiHints,
170
+ },
38
171
  capabilities: {
39
172
  chatTypes: ["direct", "group"],
40
173
  media: {
@@ -44,11 +177,36 @@ export function createWecomChannelPlugin({
44
177
  markdown: true,
45
178
  },
46
179
  config: {
47
- listAccountIds: (cfg) => listWecomAccountIds({ config: cfg }),
180
+ listAccountIds: (cfg) => {
181
+ const accountIds = listWecomAccountIds({ config: cfg });
182
+ if (accountIds.length > 0) return accountIds;
183
+ return hasConfiguredBotCallback(cfg, "default") ? ["default"] : [];
184
+ },
48
185
  resolveAccount: (cfg, accountId) =>
49
186
  (getWecomConfig({ config: cfg }, accountId ?? "default") ?? {
50
187
  accountId: accountId ?? "default",
51
188
  }),
189
+ isConfigured: (account, cfg) =>
190
+ hasConfiguredAgentCredentials(account) || hasConfiguredBotCallback(cfg, account?.accountId ?? "default"),
191
+ describeAccount: (account, cfg) => buildWecomAccountSnapshot(account, cfg),
192
+ },
193
+ status: {
194
+ buildAccountSnapshot: ({ account, cfg, runtime }) =>
195
+ buildWecomAccountSnapshot(account, cfg, runtime),
196
+ buildChannelSummary: ({ snapshot }) => ({
197
+ configured: snapshot?.configured ?? false,
198
+ running: snapshot?.running ?? false,
199
+ connected:
200
+ snapshot?.connected ??
201
+ (snapshot?.running && snapshot?.configured) ??
202
+ null,
203
+ lastInbound:
204
+ normalizeTimestampMs(snapshot?.lastInboundAt ?? snapshot?.lastInbound) ??
205
+ normalizeTimestampMs(
206
+ getWecomChannelInboundActivity([snapshot?.accountId]).lastInboundAtMs,
207
+ ) ??
208
+ null,
209
+ }),
52
210
  },
53
211
  outbound: {
54
212
  deliveryMode: "direct",