@yanhaidao/wecom 2.3.4 → 2.3.9

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 (108) hide show
  1. package/README.md +213 -339
  2. package/assets/03.bot.page.png +0 -0
  3. package/changelog/v2.3.9.md +22 -0
  4. package/compat-single-account.md +32 -2
  5. package/index.ts +5 -5
  6. package/package.json +8 -7
  7. package/src/agent/api-client.upload.test.ts +1 -2
  8. package/src/agent/handler.ts +82 -9
  9. package/src/agent/index.ts +1 -1
  10. package/src/app/account-runtime.ts +245 -0
  11. package/src/app/bootstrap.ts +29 -0
  12. package/src/app/index.ts +31 -0
  13. package/src/capability/agent/delivery-service.ts +79 -0
  14. package/src/capability/agent/fallback-policy.ts +13 -0
  15. package/src/capability/agent/index.ts +3 -0
  16. package/src/capability/agent/ingress-service.ts +38 -0
  17. package/src/capability/bot/dispatch-config.ts +47 -0
  18. package/src/capability/bot/fallback-delivery.ts +178 -0
  19. package/src/capability/bot/index.ts +1 -0
  20. package/src/capability/bot/local-path-delivery.ts +215 -0
  21. package/src/capability/bot/service.ts +56 -0
  22. package/src/capability/bot/stream-delivery.ts +379 -0
  23. package/src/capability/bot/stream-finalizer.ts +120 -0
  24. package/src/capability/bot/stream-orchestrator.ts +352 -0
  25. package/src/capability/bot/types.ts +8 -0
  26. package/src/capability/index.ts +2 -0
  27. package/src/channel.lifecycle.test.ts +9 -6
  28. package/src/channel.meta.test.ts +12 -0
  29. package/src/channel.ts +48 -21
  30. package/src/config/accounts.ts +223 -283
  31. package/src/config/derived-paths.test.ts +111 -0
  32. package/src/config/derived-paths.ts +41 -0
  33. package/src/config/index.ts +10 -12
  34. package/src/config/runtime-config.ts +46 -0
  35. package/src/config/schema.ts +59 -102
  36. package/src/domain/models.ts +7 -0
  37. package/src/domain/policies.ts +36 -0
  38. package/src/dynamic-agent.ts +6 -0
  39. package/src/gateway-monitor.ts +43 -93
  40. package/src/http.ts +23 -2
  41. package/src/monitor/limits.ts +7 -0
  42. package/src/monitor/state.ts +28 -508
  43. package/src/monitor.active.test.ts +3 -3
  44. package/src/monitor.integration.test.ts +0 -1
  45. package/src/monitor.ts +64 -2603
  46. package/src/monitor.webhook.test.ts +127 -42
  47. package/src/observability/audit-log.ts +48 -0
  48. package/src/observability/legacy-operational-event-store.ts +36 -0
  49. package/src/observability/raw-envelope-log.ts +28 -0
  50. package/src/observability/status-registry.ts +13 -0
  51. package/src/observability/transport-session-view.ts +14 -0
  52. package/src/onboarding.test.ts +219 -0
  53. package/src/onboarding.ts +88 -71
  54. package/src/outbound.test.ts +5 -5
  55. package/src/outbound.ts +18 -66
  56. package/src/runtime/dispatcher.ts +52 -0
  57. package/src/runtime/index.ts +4 -0
  58. package/src/runtime/outbound-intent.ts +4 -0
  59. package/src/runtime/reply-orchestrator.test.ts +38 -0
  60. package/src/runtime/reply-orchestrator.ts +55 -0
  61. package/src/runtime/routing-bridge.ts +19 -0
  62. package/src/runtime/session-manager.ts +76 -0
  63. package/src/runtime.ts +7 -14
  64. package/src/shared/command-auth.ts +1 -17
  65. package/src/shared/media-service.ts +36 -0
  66. package/src/shared/media-types.ts +5 -0
  67. package/src/store/active-reply-store.ts +42 -0
  68. package/src/store/interfaces.ts +11 -0
  69. package/src/store/memory-store.ts +43 -0
  70. package/src/store/stream-batch-store.ts +350 -0
  71. package/src/target.ts +28 -0
  72. package/src/transport/agent-api/client.ts +44 -0
  73. package/src/transport/agent-api/core.ts +367 -0
  74. package/src/transport/agent-api/delivery.ts +41 -0
  75. package/src/transport/agent-api/media-upload.ts +11 -0
  76. package/src/transport/agent-api/reply.ts +39 -0
  77. package/src/transport/agent-callback/http-handler.ts +47 -0
  78. package/src/transport/agent-callback/inbound.ts +5 -0
  79. package/src/transport/agent-callback/reply.ts +13 -0
  80. package/src/transport/agent-callback/request-handler.ts +244 -0
  81. package/src/transport/agent-callback/session.ts +23 -0
  82. package/src/transport/bot-webhook/active-reply.ts +36 -0
  83. package/src/transport/bot-webhook/http-handler.ts +48 -0
  84. package/src/transport/bot-webhook/inbound-normalizer.ts +371 -0
  85. package/src/transport/bot-webhook/inbound.ts +5 -0
  86. package/src/transport/bot-webhook/message-shape.ts +89 -0
  87. package/src/transport/bot-webhook/protocol.ts +148 -0
  88. package/src/transport/bot-webhook/reply.ts +15 -0
  89. package/src/transport/bot-webhook/request-handler.ts +394 -0
  90. package/src/transport/bot-webhook/session.ts +23 -0
  91. package/src/transport/bot-ws/inbound.ts +109 -0
  92. package/src/transport/bot-ws/reply.ts +48 -0
  93. package/src/transport/bot-ws/sdk-adapter.ts +180 -0
  94. package/src/transport/bot-ws/session.ts +28 -0
  95. package/src/transport/http/common.ts +109 -0
  96. package/src/transport/http/registry.ts +92 -0
  97. package/src/transport/http/request-handler.ts +84 -0
  98. package/src/transport/index.ts +14 -0
  99. package/src/types/account.ts +56 -91
  100. package/src/types/config.ts +59 -112
  101. package/src/types/constants.ts +20 -35
  102. package/src/types/events.ts +21 -0
  103. package/src/types/index.ts +14 -38
  104. package/src/types/legacy-stream.ts +50 -0
  105. package/src/types/runtime-context.ts +28 -0
  106. package/src/types/runtime.ts +161 -0
  107. package/src/agent/api-client.ts +0 -383
  108. package/src/monitor/types.ts +0 -136
@@ -0,0 +1,352 @@
1
+ import { pathToFileURL } from "node:url";
2
+
3
+ import type { PluginRuntime } from "openclaw/plugin-sdk";
4
+
5
+ import { resolveWecomMediaMaxBytes, shouldRejectWecomDefaultRoute } from "../../config/index.js";
6
+ import { ensureDynamicAgentListed, generateAgentId, shouldUseDynamicAgent } from "../../dynamic-agent.js";
7
+ import { LIMITS, type StreamStore } from "../../monitor/state.js";
8
+ import { getWecomRuntime } from "../../runtime.js";
9
+ import { buildWecomUnauthorizedCommandPrompt, resolveWecomCommandAuthorization } from "../../shared/command-auth.js";
10
+ import type { PendingInbound } from "../../types/legacy-stream.js";
11
+ import type { WecomBotInboundMessage as WecomInboundMessage } from "../../types/index.js";
12
+ import type { WecomWebhookTarget } from "../../types/runtime-context.js";
13
+ import { looksLikeSendLocalFileIntent, processBotInboundMessage } from "../../transport/bot-webhook/inbound-normalizer.js";
14
+ import { resolveWecomSenderUserId } from "../../transport/bot-webhook/message-shape.js";
15
+ import { buildWecomBotDispatchConfig } from "./dispatch-config.js";
16
+ import { sendBotFallbackPromptNow } from "./fallback-delivery.js";
17
+ import { finalizeBotStream } from "./stream-finalizer.js";
18
+ import { handleDirectLocalPathIntent } from "./local-path-delivery.js";
19
+ import { createBotReplyDispatcher } from "./stream-delivery.js";
20
+ import type { BotRuntimeLogger, RecordBotOperationalEvent } from "./types.js";
21
+
22
+ export type StartBotAgentStreamParams = {
23
+ target: WecomWebhookTarget;
24
+ accountId: string;
25
+ msg: WecomInboundMessage;
26
+ streamId: string;
27
+ mergedContents?: string[] | string;
28
+ mergedMsgids?: string[];
29
+ };
30
+
31
+ export function createBotStreamOrchestrator(params: {
32
+ streamStore: StreamStore;
33
+ recordBotOperationalEvent: RecordBotOperationalEvent;
34
+ }) {
35
+ const { streamStore, recordBotOperationalEvent } = params;
36
+
37
+ const logVerbose: BotRuntimeLogger = (target, message) => {
38
+ const should =
39
+ target.core.logging?.shouldLogVerbose?.() ??
40
+ (() => {
41
+ try {
42
+ return getWecomRuntime().logging.shouldLogVerbose();
43
+ } catch {
44
+ return false;
45
+ }
46
+ })();
47
+ if (!should) return;
48
+ target.runtime.log?.(`[wecom] ${message}`);
49
+ };
50
+
51
+ const logInfo: BotRuntimeLogger = (target, message) => {
52
+ target.runtime.log?.(`[wecom] ${message}`);
53
+ };
54
+
55
+ const truncateUtf8Bytes = (text: string, maxBytes: number): string => {
56
+ const buf = Buffer.from(text, "utf8");
57
+ if (buf.length <= maxBytes) return text;
58
+ return buf.subarray(buf.length - maxBytes).toString("utf8");
59
+ };
60
+
61
+ const computeTaskKey = (target: WecomWebhookTarget, msg: WecomInboundMessage): string | undefined => {
62
+ const msgid = msg.msgid ? String(msg.msgid) : "";
63
+ if (!msgid) return undefined;
64
+ const aibotid = String((msg as any).aibotid ?? "unknown").trim() || "unknown";
65
+ return `bot:${target.account.accountId}:${aibotid}:${msgid}`;
66
+ };
67
+
68
+ async function flushPending(pending: PendingInbound): Promise<void> {
69
+ const { streamId, target, msg, contents, msgids, conversationKey, batchKey } = pending;
70
+ const mergedContents = contents.filter((c) => c.trim()).join("\n").trim();
71
+
72
+ let core: PluginRuntime | null = null;
73
+ try {
74
+ core = getWecomRuntime();
75
+ } catch (err) {
76
+ logVerbose(target, `flush pending: runtime not ready: ${String(err)}`);
77
+ streamStore.markFinished(streamId);
78
+ logInfo(target, `queue: runtime not ready,结束批次并推进 streamId=${streamId}`);
79
+ streamStore.onStreamFinished(streamId);
80
+ return;
81
+ }
82
+
83
+ if (!core) return;
84
+
85
+ streamStore.markStarted(streamId);
86
+ const enrichedTarget: WecomWebhookTarget = { ...target, core };
87
+ logInfo(
88
+ target,
89
+ `flush pending: start batch streamId=${streamId} batchKey=${batchKey} conversationKey=${conversationKey} mergedCount=${contents.length}`,
90
+ );
91
+ logVerbose(target, `防抖结束: 开始处理聚合消息 数量=${contents.length} streamId=${streamId}`);
92
+
93
+ startAgentForStream({
94
+ target: enrichedTarget,
95
+ accountId: target.account.accountId,
96
+ msg,
97
+ streamId,
98
+ mergedContents: contents.length > 1 ? mergedContents : undefined,
99
+ mergedMsgids: msgids.length > 1 ? msgids : undefined,
100
+ }).catch((err) => {
101
+ streamStore.updateStream(streamId, (state) => {
102
+ state.error = err instanceof Error ? err.message : String(err);
103
+ state.content = state.content || `Error: ${state.error}`;
104
+ state.finished = true;
105
+ });
106
+ target.runtime.error?.(`[${target.account.accountId}] wecom agent failed (处理失败): ${String(err)}`);
107
+ streamStore.onStreamFinished(streamId);
108
+ });
109
+ }
110
+
111
+ async function startAgentForStream(params: StartBotAgentStreamParams): Promise<void> {
112
+ const { target, msg, streamId } = params;
113
+ const core = target.core;
114
+ const config = target.config;
115
+ const account = target.account;
116
+
117
+ const userId = resolveWecomSenderUserId(msg) || "unknown";
118
+ const chatType = msg.chattype === "group" ? "group" : "direct";
119
+ const chatId = msg.chattype === "group" ? (msg.chatid?.trim() || "unknown") : userId;
120
+ const taskKey = computeTaskKey(target, msg);
121
+ const aibotid = String((msg as any).aibotid ?? "").trim() || undefined;
122
+
123
+ streamStore.updateStream(streamId, (s) => {
124
+ s.userId = userId;
125
+ s.chatType = chatType;
126
+ s.chatId = chatId;
127
+ s.taskKey = taskKey;
128
+ s.aibotid = aibotid;
129
+ });
130
+
131
+ let { body: rawBody, media } = await processBotInboundMessage({
132
+ target,
133
+ msg,
134
+ recordOperationalIssue: (event) => recordBotOperationalEvent(target, event),
135
+ });
136
+
137
+ if (params.mergedContents) {
138
+ rawBody = Array.isArray(params.mergedContents) ? params.mergedContents.join("\n") : params.mergedContents;
139
+ }
140
+
141
+ const handledLocalPath = await handleDirectLocalPathIntent({
142
+ streamStore,
143
+ target,
144
+ streamId,
145
+ rawBody,
146
+ userId,
147
+ chatType,
148
+ logVerbose,
149
+ looksLikeSendLocalFileIntent,
150
+ });
151
+ if (handledLocalPath) return;
152
+
153
+ let mediaPath: string | undefined;
154
+ let mediaType: string | undefined;
155
+ if (media) {
156
+ try {
157
+ const maxBytes = resolveWecomMediaMaxBytes(target.config);
158
+ const saved = await core.channel.media.saveMediaBuffer(media.buffer, media.contentType, "inbound", maxBytes, media.filename);
159
+ mediaPath = saved.path;
160
+ mediaType = saved.contentType;
161
+ logVerbose(target, `saved inbound media to ${mediaPath} (${mediaType})`);
162
+ } catch (err) {
163
+ target.runtime.error?.(`Failed to save inbound media: ${String(err)}`);
164
+ }
165
+ }
166
+
167
+ const route = core.channel.routing.resolveAgentRoute({
168
+ cfg: config,
169
+ channel: "wecom",
170
+ accountId: account.accountId,
171
+ peer: { kind: chatType === "group" ? "group" : "direct", id: chatId },
172
+ });
173
+
174
+ const useDynamicAgent = shouldUseDynamicAgent({
175
+ chatType: chatType === "group" ? "group" : "dm",
176
+ senderId: userId,
177
+ config,
178
+ });
179
+
180
+ if (shouldRejectWecomDefaultRoute({ cfg: config, matchedBy: route.matchedBy, useDynamicAgent })) {
181
+ const prompt =
182
+ `当前账号(${account.accountId})未绑定 OpenClaw Agent,已拒绝回退到默认主智能体。` +
183
+ `请在 bindings 中添加:{"agentId":"你的Agent","match":{"channel":"wecom","accountId":"${account.accountId}"}}`;
184
+ target.runtime.error?.(
185
+ `[wecom] routing guard: blocked default fallback accountId=${account.accountId} matchedBy=${route.matchedBy} streamId=${streamId}`,
186
+ );
187
+ streamStore.updateStream(streamId, (s) => {
188
+ s.finished = true;
189
+ s.content = prompt;
190
+ });
191
+ try {
192
+ await sendBotFallbackPromptNow({ streamId, text: prompt });
193
+ } catch (err) {
194
+ target.runtime.error?.(`routing guard prompt push failed streamId=${streamId}: ${String(err)}`);
195
+ }
196
+ streamStore.onStreamFinished(streamId);
197
+ return;
198
+ }
199
+
200
+ if (useDynamicAgent) {
201
+ const targetAgentId = generateAgentId(chatType === "group" ? "group" : "dm", chatId, account.accountId);
202
+ route.agentId = targetAgentId;
203
+ route.sessionKey = `agent:${targetAgentId}:wecom:${account.accountId}:${chatType === "group" ? "group" : "dm"}:${chatId}`;
204
+ ensureDynamicAgentListed(targetAgentId, core).catch(() => {});
205
+ logVerbose(target, `dynamic agent routing: ${targetAgentId}, sessionKey=${route.sessionKey}`);
206
+ }
207
+
208
+ logVerbose(target, `starting agent processing (streamId=${streamId}, agentId=${route.agentId}, peerKind=${chatType}, peerId=${chatId})`);
209
+ logVerbose(target, `启动 Agent 处理: streamId=${streamId} 路由=${route.agentId} 类型=${chatType} ID=${chatId}`);
210
+
211
+ const fromLabel = chatType === "group" ? `group:${chatId}` : `user:${userId}`;
212
+ const storePath = core.channel.session.resolveStorePath(config.session?.store, { agentId: route.agentId });
213
+ const envelopeOptions = core.channel.reply.resolveEnvelopeFormatOptions(config);
214
+ const previousTimestamp = core.channel.session.readSessionUpdatedAt({
215
+ storePath,
216
+ sessionKey: route.sessionKey,
217
+ });
218
+ const body = core.channel.reply.formatAgentEnvelope({
219
+ channel: "WeCom",
220
+ from: fromLabel,
221
+ previousTimestamp,
222
+ envelope: envelopeOptions,
223
+ body: rawBody,
224
+ });
225
+
226
+ const authz = await resolveWecomCommandAuthorization({
227
+ core,
228
+ cfg: config,
229
+ accountConfig: account.config,
230
+ rawBody,
231
+ senderUserId: userId,
232
+ });
233
+ const commandAuthorized = authz.commandAuthorized;
234
+ logVerbose(
235
+ target,
236
+ `authz: dmPolicy=${authz.dmPolicy} shouldCompute=${authz.shouldComputeAuth} sender=${userId.toLowerCase()} senderAllowed=${authz.senderAllowed} authorizerConfigured=${authz.authorizerConfigured} commandAuthorized=${String(authz.commandAuthorized)}`,
237
+ );
238
+
239
+ if (authz.shouldComputeAuth && authz.commandAuthorized !== true) {
240
+ const prompt = buildWecomUnauthorizedCommandPrompt({ senderUserId: userId, dmPolicy: authz.dmPolicy, scope: "bot" });
241
+ streamStore.updateStream(streamId, (s) => {
242
+ s.finished = true;
243
+ s.content = prompt;
244
+ });
245
+ try {
246
+ await sendBotFallbackPromptNow({ streamId, text: prompt });
247
+ logInfo(target, `authz: 未授权命令已提示用户 streamId=${streamId}`);
248
+ } catch (err) {
249
+ target.runtime.error?.(`authz: 未授权命令提示推送失败 streamId=${streamId}: ${String(err)}`);
250
+ }
251
+ streamStore.onStreamFinished(streamId);
252
+ return;
253
+ }
254
+
255
+ const attachments = mediaPath
256
+ ? [
257
+ {
258
+ name: media?.filename || "file",
259
+ mimeType: mediaType,
260
+ url: pathToFileURL(mediaPath).href,
261
+ },
262
+ ]
263
+ : undefined;
264
+
265
+ const ctxPayload = core.channel.reply.finalizeInboundContext({
266
+ Body: body,
267
+ RawBody: rawBody,
268
+ CommandBody: rawBody,
269
+ Attachments: attachments,
270
+ From: chatType === "group" ? `wecom:group:${chatId}` : `wecom:${userId}`,
271
+ To: `wecom:${chatId}`,
272
+ SessionKey: route.sessionKey,
273
+ AccountId: route.accountId,
274
+ ChatType: chatType,
275
+ ConversationLabel: fromLabel,
276
+ SenderName: userId,
277
+ SenderId: userId,
278
+ Provider: "wecom",
279
+ Surface: "wecom",
280
+ MessageSid: msg.msgid,
281
+ CommandAuthorized: commandAuthorized,
282
+ OriginatingChannel: "wecom",
283
+ OriginatingTo: `wecom:${chatId}`,
284
+ MediaPath: mediaPath,
285
+ MediaType: mediaType,
286
+ MediaUrl: mediaPath,
287
+ });
288
+
289
+ await core.channel.session.recordInboundSession({
290
+ storePath,
291
+ sessionKey: ctxPayload.SessionKey ?? route.sessionKey,
292
+ ctx: ctxPayload,
293
+ onRecordError: (err) => {
294
+ target.runtime.error?.(`wecom: failed updating session meta: ${String(err)}`);
295
+ },
296
+ });
297
+
298
+ const tableMode = core.channel.text.resolveMarkdownTableMode({
299
+ cfg: config,
300
+ channel: "wecom",
301
+ accountId: account.accountId,
302
+ });
303
+ const cfgForDispatch = buildWecomBotDispatchConfig(config);
304
+ logVerbose(target, "tool-policy: WeCom Bot 会话已禁用 message 工具(tools.deny += message;并同步到 tools.sandbox.tools.deny,防止绕过 Bot 交付)");
305
+
306
+ await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
307
+ ctx: ctxPayload,
308
+ cfg: cfgForDispatch,
309
+ replyOptions: { disableBlockStreaming: false },
310
+ dispatcherOptions: createBotReplyDispatcher({
311
+ streamStore,
312
+ target,
313
+ accountId: account.accountId,
314
+ config,
315
+ msg,
316
+ streamId,
317
+ rawBody,
318
+ chatType,
319
+ userId,
320
+ core,
321
+ tableMode,
322
+ logVerbose,
323
+ truncateUtf8Bytes,
324
+ recordBotOperationalEvent,
325
+ }),
326
+ });
327
+
328
+ const rawBodyNormalized = rawBody.trim();
329
+ const isResetCommand = /^\/(new|reset)(?:\s|$)/i.test(rawBodyNormalized);
330
+ const resetCommandKind = isResetCommand ? (rawBodyNormalized.match(/^\/(new|reset)/i)?.[1]?.toLowerCase() ?? "new") : null;
331
+
332
+ await finalizeBotStream({
333
+ streamStore,
334
+ target,
335
+ streamId,
336
+ chatType,
337
+ core,
338
+ config,
339
+ accountId: account.accountId,
340
+ isResetCommand,
341
+ resetCommandKind,
342
+ logInfo,
343
+ logVerbose,
344
+ recordBotOperationalEvent,
345
+ });
346
+ }
347
+
348
+ return {
349
+ flushPending,
350
+ startAgentForStream,
351
+ };
352
+ }
@@ -0,0 +1,8 @@
1
+ import type { WecomRuntimeAuditEvent, WecomWebhookTarget } from "../../types/runtime-context.js";
2
+
3
+ export type BotRuntimeLogger = (target: WecomWebhookTarget, message: string) => void;
4
+
5
+ export type RecordBotOperationalEvent = (
6
+ target: Pick<WecomWebhookTarget, "account" | "auditSink">,
7
+ event: Omit<WecomRuntimeAuditEvent, "transport">,
8
+ ) => void;
@@ -0,0 +1,2 @@
1
+ export { WecomBotCapabilityService } from "./bot/index.js";
2
+ export { WecomAgentIngressService } from "./agent/index.js";
@@ -89,7 +89,7 @@ function createCtx(params: {
89
89
  };
90
90
  }
91
91
 
92
- function createLegacyBotConfig(params: {
92
+ function createWebhookBotConfig(params: {
93
93
  token: string;
94
94
  encodingAESKey: string;
95
95
  receiveId?: string;
@@ -99,9 +99,12 @@ function createLegacyBotConfig(params: {
99
99
  wecom: {
100
100
  enabled: true,
101
101
  bot: {
102
- token: params.token,
103
- encodingAESKey: params.encodingAESKey,
104
- receiveId: params.receiveId ?? "",
102
+ primaryTransport: "webhook",
103
+ webhook: {
104
+ token: params.token,
105
+ encodingAESKey: params.encodingAESKey,
106
+ receiveId: params.receiveId ?? "",
107
+ },
105
108
  },
106
109
  },
107
110
  },
@@ -148,7 +151,7 @@ describe("wecomPlugin gateway lifecycle", () => {
148
151
  it("keeps startAccount pending until abort signal", async () => {
149
152
  const token = "token";
150
153
  const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
151
- const cfg = createLegacyBotConfig({ token, encodingAESKey });
154
+ const cfg = createWebhookBotConfig({ token, encodingAESKey });
152
155
  const abortController = new AbortController();
153
156
  const ctx = createCtx({ cfg, abortController });
154
157
 
@@ -171,7 +174,7 @@ describe("wecomPlugin gateway lifecycle", () => {
171
174
  const token = "token";
172
175
  const encodingAESKey = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFG";
173
176
  const receiveId = "";
174
- const cfg = createLegacyBotConfig({ token, encodingAESKey, receiveId });
177
+ const cfg = createWebhookBotConfig({ token, encodingAESKey, receiveId });
175
178
  const abortController = new AbortController();
176
179
  const ctx = createCtx({ cfg, abortController });
177
180
 
@@ -0,0 +1,12 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { wecomPlugin } from "./channel.js";
3
+
4
+ describe("wecomPlugin meta", () => {
5
+ it("uses chinese-facing labels in channel selection", () => {
6
+ expect(wecomPlugin.meta.label).toBe("WeCom (企业微信)");
7
+ expect(wecomPlugin.meta.selectionLabel).toBe("WeCom (企业微信)");
8
+ expect(wecomPlugin.meta.blurb).toContain("企业微信官方推荐三方插件");
9
+ expect(wecomPlugin.meta.docsLabel).toBe("企业微信");
10
+ expect(wecomPlugin.meta.selectionDocsPrefix).toBe("文档:");
11
+ });
12
+ });
package/src/channel.ts CHANGED
@@ -11,6 +11,7 @@ import {
11
11
  import {
12
12
  DEFAULT_ACCOUNT_ID,
13
13
  listWecomAccountIds,
14
+ resolveDerivedPathSummary,
14
15
  resolveDefaultWecomAccountId,
15
16
  resolveWecomAccount,
16
17
  resolveWecomAccountConflict,
@@ -19,24 +20,38 @@ import type { ResolvedWecomAccount } from "./types/index.js";
19
20
  import { monitorWecomProvider } from "./gateway-monitor.js";
20
21
  import { wecomOnboardingAdapter } from "./onboarding.js";
21
22
  import { wecomOutbound } from "./outbound.js";
22
- import { WEBHOOK_PATHS } from "./types/constants.js";
23
23
 
24
24
  const meta = {
25
25
  id: "wecom",
26
- label: "WeCom",
27
- selectionLabel: "WeCom (plugin)",
26
+ label: "WeCom (企业微信)",
27
+ selectionLabel: "WeCom (企业微信)",
28
28
  docsPath: "/channels/wecom",
29
- docsLabel: "wecom",
30
- blurb: "Enterprise WeCom intelligent bot (API mode) via encrypted webhooks + passive replies.",
29
+ docsLabel: "企业微信",
30
+ blurb: "企业微信官方推荐三方插件,默认 Bot WS 配置简单,支持主动发消息与 Agent 全能力。",
31
+ selectionDocsPrefix: "文档:",
31
32
  aliases: ["wechatwork", "wework", "qywx", "企微", "企业微信"],
32
33
  order: 85,
33
34
  quickstartAllowFrom: true,
34
35
  };
35
36
 
37
+ function resolveAccountInboundPath(account: ResolvedWecomAccount): string | undefined {
38
+ const derivedPaths = resolveDerivedPathSummary(account.accountId);
39
+ if (account.bot?.primaryTransport === "webhook" && account.bot.webhookConfigured) {
40
+ return derivedPaths.botWebhook[0];
41
+ }
42
+ if (account.agent?.callbackConfigured) {
43
+ return derivedPaths.agentCallback[0];
44
+ }
45
+ return undefined;
46
+ }
47
+
36
48
  function normalizeWecomMessagingTarget(raw: string): string | undefined {
37
49
  const trimmed = raw.trim();
38
50
  if (!trimmed) return undefined;
39
- return trimmed.replace(/^(wecom-agent|wecom|wechatwork|wework|qywx):/i, "").trim() || undefined;
51
+ if (/^wecom-agent:/i.test(trimmed)) {
52
+ return trimmed;
53
+ }
54
+ return trimmed.replace(/^(wecom|wechatwork|wework|qywx):/i, "").trim() || undefined;
40
55
  }
41
56
 
42
57
  export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
@@ -98,7 +113,6 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
98
113
  accountId: account.accountId,
99
114
  })?.message ?? "not configured",
100
115
  describeAccount: (account, cfg): ChannelAccountSnapshot => {
101
- const matrixMode = account.accountId !== DEFAULT_ACCOUNT_ID;
102
116
  const conflict = resolveWecomAccountConflict({
103
117
  cfg: cfg as OpenClawConfig,
104
118
  accountId: account.accountId,
@@ -108,11 +122,7 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
108
122
  name: account.name,
109
123
  enabled: account.enabled,
110
124
  configured: account.configured && !conflict,
111
- webhookPath: account.bot?.config
112
- ? (matrixMode ? `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.BOT_PLUGIN)
113
- : account.agent?.config
114
- ? (matrixMode ? `${WEBHOOK_PATHS.AGENT_PLUGIN}/${account.accountId}` : WEBHOOK_PATHS.AGENT_PLUGIN)
115
- : WEBHOOK_PATHS.BOT_PLUGIN,
125
+ webhookPath: resolveAccountInboundPath(account),
116
126
  };
117
127
  },
118
128
  resolveAllowFrom: ({ cfg, accountId }) => {
@@ -157,11 +167,23 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
157
167
  configured: snapshot.configured ?? false,
158
168
  running: snapshot.running ?? false,
159
169
  webhookPath: snapshot.webhookPath ?? null,
170
+ transport: (snapshot as { transport?: string }).transport ?? null,
171
+ ownerId: (snapshot as { ownerId?: string }).ownerId ?? null,
172
+ health: (snapshot as { health?: string }).health ?? "idle",
173
+ ownerDriftAt: (snapshot as { ownerDriftAt?: number | null }).ownerDriftAt ?? null,
174
+ connected: (snapshot as { connected?: boolean }).connected,
175
+ authenticated: (snapshot as { authenticated?: boolean }).authenticated,
160
176
  lastStartAt: snapshot.lastStartAt ?? null,
161
177
  lastStopAt: snapshot.lastStopAt ?? null,
162
178
  lastError: snapshot.lastError ?? null,
179
+ lastErrorAt: (snapshot as { lastErrorAt?: number | null }).lastErrorAt ?? null,
163
180
  lastInboundAt: snapshot.lastInboundAt ?? null,
164
181
  lastOutboundAt: snapshot.lastOutboundAt ?? null,
182
+ recentInboundSummary: (snapshot as { recentInboundSummary?: string | null }).recentInboundSummary ?? null,
183
+ recentOutboundSummary: (snapshot as { recentOutboundSummary?: string | null }).recentOutboundSummary ?? null,
184
+ recentIssueCategory: (snapshot as { recentIssueCategory?: string | null }).recentIssueCategory ?? null,
185
+ recentIssueSummary: (snapshot as { recentIssueSummary?: string | null }).recentIssueSummary ?? null,
186
+ transportSessions: (snapshot as { transportSessions?: string[] }).transportSessions ?? [],
165
187
  probe: snapshot.probe,
166
188
  lastProbeAt: snapshot.lastProbeAt ?? null,
167
189
  }),
@@ -176,21 +198,26 @@ export const wecomPlugin: ChannelPlugin<ResolvedWecomAccount> = {
176
198
  name: account.name,
177
199
  enabled: account.enabled,
178
200
  configured: account.configured && !conflict,
179
- webhookPath: account.bot?.config
180
- ? (account.accountId === DEFAULT_ACCOUNT_ID
181
- ? WEBHOOK_PATHS.BOT_PLUGIN
182
- : `${WEBHOOK_PATHS.BOT_PLUGIN}/${account.accountId}`)
183
- : account.agent?.config
184
- ? (account.accountId === DEFAULT_ACCOUNT_ID
185
- ? WEBHOOK_PATHS.AGENT_PLUGIN
186
- : `${WEBHOOK_PATHS.AGENT_PLUGIN}/${account.accountId}`)
187
- : WEBHOOK_PATHS.BOT_PLUGIN,
201
+ webhookPath: resolveAccountInboundPath(account),
202
+ primaryTransport: account.bot?.primaryTransport ?? (account.agent ? "agent-callback" : null),
203
+ transport: (runtime as { transport?: string } | undefined)?.transport ?? null,
204
+ ownerId: (runtime as { ownerId?: string } | undefined)?.ownerId ?? null,
205
+ health: (runtime as { health?: string } | undefined)?.health ?? "idle",
206
+ ownerDriftAt: (runtime as { ownerDriftAt?: number | null } | undefined)?.ownerDriftAt ?? null,
207
+ connected: (runtime as { connected?: boolean } | undefined)?.connected,
208
+ authenticated: (runtime as { authenticated?: boolean } | undefined)?.authenticated,
188
209
  running: runtime?.running ?? false,
189
210
  lastStartAt: runtime?.lastStartAt ?? null,
190
211
  lastStopAt: runtime?.lastStopAt ?? null,
191
212
  lastError: runtime?.lastError ?? conflict?.message ?? null,
213
+ lastErrorAt: (runtime as { lastErrorAt?: number | null } | undefined)?.lastErrorAt ?? null,
192
214
  lastInboundAt: runtime?.lastInboundAt ?? null,
193
215
  lastOutboundAt: runtime?.lastOutboundAt ?? null,
216
+ recentInboundSummary: (runtime as { recentInboundSummary?: string | null } | undefined)?.recentInboundSummary ?? null,
217
+ recentOutboundSummary: (runtime as { recentOutboundSummary?: string | null } | undefined)?.recentOutboundSummary ?? null,
218
+ recentIssueCategory: (runtime as { recentIssueCategory?: string | null } | undefined)?.recentIssueCategory ?? null,
219
+ recentIssueSummary: (runtime as { recentIssueSummary?: string | null } | undefined)?.recentIssueSummary ?? null,
220
+ transportSessions: (runtime as { transportSessions?: string[] } | undefined)?.transportSessions ?? [],
194
221
  dmPolicy: account.bot?.config.dm?.policy ?? "pairing",
195
222
  };
196
223
  },