@yanhaidao/wecom 2.3.160 → 2.3.190

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 (49) hide show
  1. package/README.md +294 -379
  2. package/SKILLS_CAL.md +895 -0
  3. package/SKILLS_DOC.md +2288 -0
  4. package/changelog/v2.3.18.md +22 -0
  5. package/changelog/v2.3.19.md +73 -0
  6. package/index.ts +39 -3
  7. package/package.json +2 -3
  8. package/src/agent/handler.event-filter.test.ts +11 -0
  9. package/src/agent/handler.ts +732 -643
  10. package/src/app/account-runtime.ts +46 -20
  11. package/src/app/index.ts +20 -1
  12. package/src/capability/bot/stream-orchestrator.ts +1 -1
  13. package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
  14. package/src/capability/calendar/client.ts +815 -0
  15. package/src/capability/calendar/index.ts +3 -0
  16. package/src/capability/calendar/schema.ts +417 -0
  17. package/src/capability/calendar/tool.ts +417 -0
  18. package/src/capability/calendar/types.ts +309 -0
  19. package/src/capability/doc/client.ts +788 -64
  20. package/src/capability/doc/schema.ts +419 -318
  21. package/src/capability/doc/tool.ts +1517 -1178
  22. package/src/capability/doc/types.ts +130 -14
  23. package/src/capability/mcp/index.ts +10 -0
  24. package/src/capability/mcp/schema.ts +107 -0
  25. package/src/capability/mcp/tool.ts +170 -0
  26. package/src/capability/mcp/transport.ts +394 -0
  27. package/src/channel.ts +70 -28
  28. package/src/config/index.ts +7 -1
  29. package/src/config/media.test.ts +113 -0
  30. package/src/config/media.ts +133 -6
  31. package/src/config/schema.ts +74 -102
  32. package/src/outbound.test.ts +250 -15
  33. package/src/outbound.ts +155 -30
  34. package/src/runtime/reply-orchestrator.test.ts +35 -2
  35. package/src/runtime/reply-orchestrator.ts +14 -2
  36. package/src/runtime/routing-bridge.test.ts +115 -0
  37. package/src/runtime/routing-bridge.ts +26 -1
  38. package/src/runtime/session-manager.ts +20 -6
  39. package/src/runtime/source-registry.ts +165 -0
  40. package/src/transport/bot-webhook/inbound-normalizer.ts +4 -4
  41. package/src/transport/bot-ws/media.test.ts +44 -0
  42. package/src/transport/bot-ws/media.ts +272 -0
  43. package/src/transport/bot-ws/reply.test.ts +216 -18
  44. package/src/transport/bot-ws/reply.ts +116 -21
  45. package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
  46. package/src/transport/bot-ws/sdk-adapter.ts +89 -12
  47. package/src/types/config.ts +3 -0
  48. package/.claude/settings.local.json +0 -11
  49. package/docs/update-content-fix.md +0 -135
package/src/outbound.ts CHANGED
@@ -1,7 +1,12 @@
1
1
  import type { ChannelOutboundAdapter, ChannelOutboundContext } from "openclaw/plugin-sdk";
2
-
3
- import { resolveWecomAccount, resolveWecomAccountConflict, resolveWecomAccounts } from "./config/index.js";
4
2
  import { WecomAgentDeliveryService } from "./capability/agent/index.js";
3
+ import {
4
+ resolveWecomMergedMediaLocalRoots,
5
+ resolveWecomMediaMaxBytes,
6
+ resolveWecomAccount,
7
+ resolveWecomAccountConflict,
8
+ resolveWecomAccounts,
9
+ } from "./config/index.js";
5
10
  import { getAccountRuntime, getBotWsPushHandle, getWecomRuntime } from "./runtime.js";
6
11
  import { resolveScopedWecomTarget } from "./target.js";
7
12
 
@@ -49,7 +54,9 @@ function resolveAgentConfigOrThrow(params: {
49
54
  );
50
55
  }
51
56
  // 注意:不要在日志里输出 corpSecret 等敏感信息
52
- getAccountRuntime(account.accountId)?.log.info?.(`[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`);
57
+ getAccountRuntime(account.accountId)?.log.info?.(
58
+ `[wecom-outbound] Using agent config: accountId=${account.accountId}, corpId=${account.corpId}, agentId=${account.agentId}`,
59
+ );
53
60
  return account;
54
61
  }
55
62
 
@@ -89,7 +96,13 @@ function shouldPreferBotWsOutbound(params: {
89
96
  accountId: params.accountId,
90
97
  });
91
98
  return {
92
- preferred: !isExplicitAgentTarget(params.to) && Boolean(account.bot?.configured && account.bot.primaryTransport === "ws" && account.bot.wsConfigured),
99
+ preferred:
100
+ !isExplicitAgentTarget(params.to) &&
101
+ Boolean(
102
+ account.bot?.configured &&
103
+ account.bot.primaryTransport === "ws" &&
104
+ account.bot.wsConfigured,
105
+ ),
93
106
  accountId: account.accountId,
94
107
  };
95
108
  }
@@ -122,12 +135,71 @@ async function sendTextViaBotWs(params: {
122
135
  `WeCom outbound account=${accountId} is configured for Bot WS active push, but the WS transport is not connected.`,
123
136
  );
124
137
  }
125
- console.log(`[wecom-outbound] Sending Bot WS active message to target=${String(params.to ?? "")} chatId=${chatId} (len=${params.text.length})`);
138
+ console.log(
139
+ `[wecom-outbound] Sending Bot WS active message to target=${String(params.to ?? "")} chatId=${chatId} (len=${params.text.length})`,
140
+ );
126
141
  await handle.sendMarkdown(chatId, params.text);
127
142
  console.log(`[wecom-outbound] Successfully sent Bot WS active message to ${chatId}`);
128
143
  return true;
129
144
  }
130
145
 
146
+ async function sendMediaViaBotWs(params: {
147
+ cfg: ChannelOutboundContext["cfg"];
148
+ accountId?: string | null;
149
+ to: string | undefined;
150
+ mediaUrl: string;
151
+ text?: string;
152
+ mediaLocalRoots?: readonly string[];
153
+ }): Promise<{
154
+ attempted: boolean;
155
+ sent: boolean;
156
+ reason?: string;
157
+ }> {
158
+ const { preferred, accountId } = shouldPreferBotWsOutbound(params);
159
+ if (!preferred) {
160
+ return { attempted: false, sent: false };
161
+ }
162
+ const chatId = resolveBotWsChatTarget({
163
+ to: params.to,
164
+ accountId,
165
+ });
166
+ if (!chatId) {
167
+ return { attempted: false, sent: false };
168
+ }
169
+ const handle = getBotWsPushHandle(accountId);
170
+ if (!handle) {
171
+ throw new Error(
172
+ `WeCom outbound account=${accountId} is configured for Bot WS active push, but no live WS runtime is registered.`,
173
+ );
174
+ }
175
+ if (!handle.isConnected()) {
176
+ throw new Error(
177
+ `WeCom outbound account=${accountId} is configured for Bot WS active push, but the WS transport is not connected.`,
178
+ );
179
+ }
180
+ console.log(
181
+ `[wecom-outbound] Sending Bot WS media to target=${String(params.to ?? "")} chatId=${chatId} media=${params.mediaUrl}`,
182
+ );
183
+ const effectiveMediaLocalRoots = resolveWecomMergedMediaLocalRoots({
184
+ cfg: params.cfg,
185
+ baseRoots: params.mediaLocalRoots,
186
+ });
187
+ const result = await handle.sendMedia({
188
+ chatId,
189
+ mediaUrl: params.mediaUrl,
190
+ text: params.text,
191
+ mediaLocalRoots: effectiveMediaLocalRoots,
192
+ maxBytes: resolveWecomMediaMaxBytes(params.cfg, accountId),
193
+ });
194
+ if (result.ok) {
195
+ console.log(`[wecom-outbound] Successfully sent Bot WS media to ${chatId}`);
196
+ return { attempted: true, sent: true };
197
+ }
198
+ const reason = result.rejectReason || result.error || "unknown";
199
+ console.warn(`[wecom-outbound] Bot WS media failed for ${chatId}: ${reason}`);
200
+ return { attempted: true, sent: false, reason };
201
+ }
202
+
131
203
  export const wecomOutbound: ChannelOutboundAdapter = {
132
204
  deliveryMode: "direct",
133
205
  chunkerMode: "text",
@@ -155,8 +227,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
155
227
  const trimmed = String(outgoingText ?? "").trim();
156
228
  const rawTo = typeof to === "string" ? to.trim().toLowerCase() : "";
157
229
  const isAgentSessionTarget = rawTo.startsWith("wecom-agent:");
158
- const looksLikeNewSessionAck =
159
- /new session started/i.test(trimmed) && /model:/i.test(trimmed);
230
+ const looksLikeNewSessionAck = /new session started/i.test(trimmed) && /model:/i.test(trimmed);
160
231
 
161
232
  if (looksLikeNewSessionAck) {
162
233
  if (!isAgentSessionTarget) {
@@ -174,7 +245,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
174
245
 
175
246
  let sentViaBotWs = false;
176
247
  let agent: any = null;
177
-
248
+
178
249
  try {
179
250
  sentViaBotWs = await sendTextViaBotWs({
180
251
  cfg,
@@ -185,7 +256,9 @@ export const wecomOutbound: ChannelOutboundAdapter = {
185
256
  if (!sentViaBotWs) {
186
257
  // Defer Agent resolution until needed for fallback
187
258
  agent = resolveAgentConfigOrThrow({ cfg, accountId });
188
- getAccountRuntime(agent.accountId)?.log.info?.(`[wecom-outbound] Sending text to target=${String(to ?? "")} (len=${outgoingText.length})`);
259
+ getAccountRuntime(agent.accountId)?.log.info?.(
260
+ `[wecom-outbound] Sending text to target=${String(to ?? "")} (len=${outgoingText.length})`,
261
+ );
189
262
  const deliveryService = new WecomAgentDeliveryService(agent);
190
263
  await deliveryService.sendText({
191
264
  to,
@@ -195,7 +268,9 @@ export const wecomOutbound: ChannelOutboundAdapter = {
195
268
  }
196
269
  } catch (err) {
197
270
  if (agent) {
198
- getAccountRuntime(agent.accountId)?.log.error?.(`[wecom-outbound] Failed to send text to ${String(to ?? "")}: ${err instanceof Error ? err.message : String(err)}`);
271
+ getAccountRuntime(agent.accountId)?.log.error?.(
272
+ `[wecom-outbound] Failed to send text to ${String(to ?? "")}: ${err instanceof Error ? err.message : String(err)}`,
273
+ );
199
274
  }
200
275
  throw err;
201
276
  }
@@ -206,18 +281,42 @@ export const wecomOutbound: ChannelOutboundAdapter = {
206
281
  timestamp: Date.now(),
207
282
  };
208
283
  },
209
- sendMedia: async ({ cfg, to, text, mediaUrl, accountId }: ChannelOutboundContext) => {
284
+ sendMedia: async ({
285
+ cfg,
286
+ to,
287
+ text,
288
+ mediaUrl,
289
+ accountId,
290
+ mediaLocalRoots,
291
+ }: ChannelOutboundContext) => {
210
292
  // signal removed - not supported in current SDK
293
+ if (!mediaUrl) {
294
+ throw new Error("WeCom outbound requires mediaUrl.");
295
+ }
211
296
 
212
- const { preferred } = shouldPreferBotWsOutbound({ cfg, accountId, to });
213
- if (preferred) {
214
- console.log(`[wecom-outbound] Bot WS active push does not support outbound media; falling back to Agent for target=${String(to ?? "")}`);
297
+ const botWs = await sendMediaViaBotWs({
298
+ cfg,
299
+ accountId,
300
+ to,
301
+ text,
302
+ mediaUrl,
303
+ mediaLocalRoots,
304
+ });
305
+ if (botWs.sent) {
306
+ return {
307
+ channel: "wecom",
308
+ messageId: `bot-ws-media-${Date.now()}`,
309
+ timestamp: Date.now(),
310
+ };
311
+ }
312
+ if (botWs.attempted) {
313
+ throw new Error(
314
+ `WeCom Bot WS media delivery failed for ${String(to ?? "")}: ${botWs.reason ?? "unknown"}`,
315
+ );
215
316
  }
317
+
216
318
  const agent = resolveAgentConfigOrThrow({ cfg, accountId });
217
319
  const deliveryService = new WecomAgentDeliveryService(agent);
218
- if (!mediaUrl) {
219
- throw new Error("WeCom outbound requires mediaUrl.");
220
- }
221
320
 
222
321
  let buffer: Buffer;
223
322
  let contentType: string;
@@ -246,23 +345,49 @@ export const wecomOutbound: ChannelOutboundAdapter = {
246
345
  // 根据扩展名推断 content-type
247
346
  const ext = path.extname(mediaUrl).slice(1).toLowerCase();
248
347
  const mimeTypes: Record<string, string> = {
249
- jpg: "image/jpeg", jpeg: "image/jpeg", png: "image/png", gif: "image/gif",
250
- webp: "image/webp", bmp: "image/bmp", mp3: "audio/mpeg", wav: "audio/wav",
251
- amr: "audio/amr", mp4: "video/mp4", pdf: "application/pdf", doc: "application/msword",
348
+ jpg: "image/jpeg",
349
+ jpeg: "image/jpeg",
350
+ png: "image/png",
351
+ gif: "image/gif",
352
+ webp: "image/webp",
353
+ bmp: "image/bmp",
354
+ mp3: "audio/mpeg",
355
+ wav: "audio/wav",
356
+ amr: "audio/amr",
357
+ mp4: "video/mp4",
358
+ pdf: "application/pdf",
359
+ doc: "application/msword",
252
360
  docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
253
- xls: "application/vnd.ms-excel", xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
254
- ppt: "application/vnd.ms-powerpoint", pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
255
- txt: "text/plain", csv: "text/csv", tsv: "text/tab-separated-values", md: "text/markdown", json: "application/json",
256
- xml: "application/xml", yaml: "application/yaml", yml: "application/yaml",
257
- zip: "application/zip", rar: "application/vnd.rar", "7z": "application/x-7z-compressed",
258
- tar: "application/x-tar", gz: "application/gzip", tgz: "application/gzip",
259
- rtf: "application/rtf", odt: "application/vnd.oasis.opendocument.text",
361
+ xls: "application/vnd.ms-excel",
362
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
363
+ ppt: "application/vnd.ms-powerpoint",
364
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
365
+ txt: "text/plain",
366
+ csv: "text/csv",
367
+ tsv: "text/tab-separated-values",
368
+ md: "text/markdown",
369
+ json: "application/json",
370
+ xml: "application/xml",
371
+ yaml: "application/yaml",
372
+ yml: "application/yaml",
373
+ zip: "application/zip",
374
+ rar: "application/vnd.rar",
375
+ "7z": "application/x-7z-compressed",
376
+ tar: "application/x-tar",
377
+ gz: "application/gzip",
378
+ tgz: "application/gzip",
379
+ rtf: "application/rtf",
380
+ odt: "application/vnd.oasis.opendocument.text",
260
381
  };
261
382
  contentType = mimeTypes[ext] || "application/octet-stream";
262
- console.log(`[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`);
383
+ console.log(
384
+ `[wecom-outbound] Reading local file: ${mediaUrl}, ext=${ext}, contentType=${contentType}`,
385
+ );
263
386
  }
264
387
 
265
- console.log(`[wecom-outbound] Sending media to ${String(to ?? "")} (filename=${filename}, contentType=${contentType})`);
388
+ console.log(
389
+ `[wecom-outbound] Sending media to ${String(to ?? "")} (filename=${filename}, contentType=${contentType})`,
390
+ );
266
391
 
267
392
  try {
268
393
  await deliveryService.sendMedia({
@@ -280,7 +405,7 @@ export const wecomOutbound: ChannelOutboundAdapter = {
280
405
 
281
406
  return {
282
407
  channel: "wecom",
283
- messageId: `agent-media-${Date.now()}`,
408
+ messageId: `${botWs.attempted ? "agent-fallback-media" : "agent-media"}-${Date.now()}`,
284
409
  timestamp: Date.now(),
285
410
  };
286
411
  },
@@ -1,10 +1,11 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
-
3
2
  import { dispatchRuntimeReply } from "./reply-orchestrator.js";
4
3
 
5
4
  describe("dispatchRuntimeReply", () => {
6
5
  it("enables block streaming for bot-ws replies", async () => {
7
- const dispatchReplyWithBufferedBlockDispatcher = vi.fn().mockResolvedValue(undefined);
6
+ const dispatchReplyWithBufferedBlockDispatcher = vi
7
+ .fn()
8
+ .mockResolvedValue({ queuedFinal: false, counts: { block: 0, final: 0, tool: 0 } });
8
9
  const core = {
9
10
  channel: {
10
11
  reply: {
@@ -35,4 +36,36 @@ describe("dispatchRuntimeReply", () => {
35
36
  }),
36
37
  );
37
38
  });
39
+
40
+ it("synthesizes a final close for bot-ws when only block replies were queued", async () => {
41
+ const dispatchReplyWithBufferedBlockDispatcher = vi.fn().mockResolvedValue({
42
+ queuedFinal: false,
43
+ counts: { block: 1, final: 0, tool: 0 },
44
+ });
45
+ const deliver = vi.fn().mockResolvedValue(undefined);
46
+ const core = {
47
+ channel: {
48
+ reply: {
49
+ dispatchReplyWithBufferedBlockDispatcher,
50
+ },
51
+ },
52
+ } as any;
53
+
54
+ await dispatchRuntimeReply({
55
+ core,
56
+ cfg: {} as any,
57
+ session: { ctx: { SessionKey: "session-a" } } as any,
58
+ replyHandle: {
59
+ context: {
60
+ transport: "bot-ws",
61
+ accountId: "default",
62
+ raw: { transport: "bot-ws", envelopeType: "ws", body: {} },
63
+ },
64
+ deliver,
65
+ } as any,
66
+ });
67
+
68
+ expect(deliver).toHaveBeenCalledTimes(1);
69
+ expect(deliver).toHaveBeenCalledWith({ text: "" }, { kind: "final" });
70
+ });
38
71
  });
@@ -1,5 +1,4 @@
1
1
  import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
-
3
2
  import type { ReplyHandle } from "../types/index.js";
4
3
  import type { PreparedSession } from "./session-manager.js";
5
4
 
@@ -29,7 +28,7 @@ export async function dispatchRuntimeReply(params: {
29
28
  replyHandle: ReplyHandle;
30
29
  }): Promise<void> {
31
30
  const { core, cfg, session, replyHandle } = params;
32
- await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
31
+ const result = await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
33
32
  ctx: session.ctx,
34
33
  cfg,
35
34
  replyOptions:
@@ -52,4 +51,17 @@ export async function dispatchRuntimeReply(params: {
52
51
  },
53
52
  },
54
53
  });
54
+
55
+ if (
56
+ replyHandle.context.transport === "bot-ws" &&
57
+ result &&
58
+ result.queuedFinal !== true &&
59
+ (result.counts?.block ?? 0) > 0
60
+ ) {
61
+ await dispatchReplyPayload({
62
+ replyHandle,
63
+ payload: { text: "" },
64
+ kind: "final",
65
+ });
66
+ }
55
67
  }
@@ -0,0 +1,115 @@
1
+ import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
+ import { beforeEach, describe, expect, it, vi } from "vitest";
3
+
4
+ import { resolveRuntimeRoute } from "./routing-bridge.js";
5
+ import { ensureDynamicAgentListed } from "../dynamic-agent.js";
6
+ import type { UnifiedInboundEvent } from "../types/index.js";
7
+
8
+ vi.mock("../dynamic-agent.js", async () => {
9
+ const actual = await vi.importActual<typeof import("../dynamic-agent.js")>(
10
+ "../dynamic-agent.js",
11
+ );
12
+ return {
13
+ ...actual,
14
+ ensureDynamicAgentListed: vi.fn().mockResolvedValue(undefined),
15
+ };
16
+ });
17
+
18
+ describe("resolveRuntimeRoute", () => {
19
+ beforeEach(() => {
20
+ vi.mocked(ensureDynamicAgentListed).mockClear();
21
+ });
22
+
23
+ it("overrides WS runtime routes with dynamic agent routing for direct chats", () => {
24
+ const baseRoute = {
25
+ agentId: "main",
26
+ channel: "wecom",
27
+ accountId: "acct-ws",
28
+ sessionKey: "agent:main",
29
+ mainSessionKey: "agent:main:main",
30
+ lastRoutePolicy: "session" as const,
31
+ matchedBy: "default" as const,
32
+ };
33
+ const resolveAgentRoute = vi.fn().mockReturnValue({ ...baseRoute });
34
+ const core = {
35
+ channel: {
36
+ routing: {
37
+ resolveAgentRoute,
38
+ },
39
+ },
40
+ } as unknown as PluginRuntime;
41
+ const cfg = {
42
+ channels: {
43
+ wecom: {
44
+ dynamicAgents: {
45
+ enabled: true,
46
+ },
47
+ },
48
+ },
49
+ } as OpenClawConfig;
50
+ const event = {
51
+ accountId: "acct-ws",
52
+ conversation: {
53
+ accountId: "acct-ws",
54
+ peerKind: "direct",
55
+ peerId: "HiDaoMax",
56
+ senderId: "HiDaoMax",
57
+ },
58
+ } as UnifiedInboundEvent;
59
+
60
+ const route = resolveRuntimeRoute({ core, cfg, event });
61
+
62
+ expect(resolveAgentRoute).toHaveBeenCalledOnce();
63
+ expect(route.agentId).toBe("wecom-acct-ws-dm-hidaomax");
64
+ expect(route.sessionKey).toBe(
65
+ "agent:wecom-acct-ws-dm-hidaomax:wecom:acct-ws:dm:HiDaoMax",
66
+ );
67
+ expect(vi.mocked(ensureDynamicAgentListed)).toHaveBeenCalledWith(
68
+ "wecom-acct-ws-dm-hidaomax",
69
+ core,
70
+ );
71
+ });
72
+
73
+ it("keeps the resolved core route when dynamic agent routing is disabled for the sender", () => {
74
+ const baseRoute = {
75
+ agentId: "main",
76
+ channel: "wecom",
77
+ accountId: "acct-ws",
78
+ sessionKey: "agent:main",
79
+ mainSessionKey: "agent:main:main",
80
+ lastRoutePolicy: "session" as const,
81
+ matchedBy: "binding.account" as const,
82
+ };
83
+ const core = {
84
+ channel: {
85
+ routing: {
86
+ resolveAgentRoute: vi.fn().mockReturnValue({ ...baseRoute }),
87
+ },
88
+ },
89
+ } as unknown as PluginRuntime;
90
+ const cfg = {
91
+ channels: {
92
+ wecom: {
93
+ dynamicAgents: {
94
+ enabled: true,
95
+ adminUsers: ["HiDaoMax"],
96
+ },
97
+ },
98
+ },
99
+ } as OpenClawConfig;
100
+ const event = {
101
+ accountId: "acct-ws",
102
+ conversation: {
103
+ accountId: "acct-ws",
104
+ peerKind: "direct",
105
+ peerId: "HiDaoMax",
106
+ senderId: "HiDaoMax",
107
+ },
108
+ } as UnifiedInboundEvent;
109
+
110
+ const route = resolveRuntimeRoute({ core, cfg, event });
111
+
112
+ expect(route).toEqual(baseRoute);
113
+ expect(vi.mocked(ensureDynamicAgentListed)).not.toHaveBeenCalled();
114
+ });
115
+ });
@@ -1,5 +1,10 @@
1
1
  import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
2
 
3
+ import {
4
+ ensureDynamicAgentListed,
5
+ generateAgentId,
6
+ shouldUseDynamicAgent,
7
+ } from "../dynamic-agent.js";
3
8
  import type { UnifiedInboundEvent } from "../types/index.js";
4
9
 
5
10
  export function resolveRuntimeRoute(params: {
@@ -7,7 +12,7 @@ export function resolveRuntimeRoute(params: {
7
12
  cfg: OpenClawConfig;
8
13
  event: UnifiedInboundEvent;
9
14
  }) {
10
- return params.core.channel.routing.resolveAgentRoute({
15
+ const route = params.core.channel.routing.resolveAgentRoute({
11
16
  cfg: params.cfg,
12
17
  channel: "wecom",
13
18
  accountId: params.event.accountId,
@@ -16,4 +21,24 @@ export function resolveRuntimeRoute(params: {
16
21
  id: params.event.conversation.peerId,
17
22
  },
18
23
  });
24
+
25
+ const chatType = params.event.conversation.peerKind === "group" ? "group" : "dm";
26
+ const useDynamicAgent = shouldUseDynamicAgent({
27
+ chatType,
28
+ senderId: params.event.conversation.senderId,
29
+ config: params.cfg,
30
+ });
31
+ if (!useDynamicAgent) {
32
+ return route;
33
+ }
34
+
35
+ const targetAgentId = generateAgentId(
36
+ chatType,
37
+ params.event.conversation.peerId,
38
+ params.event.accountId,
39
+ );
40
+ route.agentId = targetAgentId;
41
+ route.sessionKey = `agent:${targetAgentId}:wecom:${params.event.accountId}:${chatType}:${params.event.conversation.peerId}`;
42
+ ensureDynamicAgentListed(targetAgentId, params.core).catch(() => {});
43
+ return route;
19
44
  }
@@ -1,8 +1,8 @@
1
1
  import type { OpenClawConfig, PluginRuntime } from "openclaw/plugin-sdk";
2
-
3
- import { resolveRuntimeRoute } from "./routing-bridge.js";
4
- import type { UnifiedInboundEvent } from "../types/index.js";
5
2
  import type { WecomMediaService } from "../shared/media-service.js";
3
+ import type { UnifiedInboundEvent } from "../types/index.js";
4
+ import { resolveRuntimeRoute } from "./routing-bridge.js";
5
+ import { registerWecomSourceSnapshot } from "./source-registry.js";
6
6
 
7
7
  export type PreparedSession = {
8
8
  route: ReturnType<typeof resolveRuntimeRoute>;
@@ -18,6 +18,14 @@ export async function prepareInboundSession(params: {
18
18
  }): Promise<PreparedSession> {
19
19
  const { core, cfg, event, mediaService } = params;
20
20
  const route = resolveRuntimeRoute({ core, cfg, event });
21
+ if (event.transport === "bot-ws") {
22
+ registerWecomSourceSnapshot({
23
+ accountId: event.accountId,
24
+ source: "bot-ws",
25
+ messageId: event.messageId,
26
+ sessionKey: route.sessionKey,
27
+ });
28
+ }
21
29
  const storePath = core.channel.session.resolveStorePath(cfg.session?.store, {
22
30
  agentId: route.agentId,
23
31
  });
@@ -47,7 +55,10 @@ export async function prepareInboundSession(params: {
47
55
  event.conversation.peerKind === "group"
48
56
  ? `wecom:group:${event.conversation.peerId}`
49
57
  : `wecom:user:${event.conversation.senderId}`,
50
- To: event.conversation.peerKind === "group" ? `wecom:group:${event.conversation.peerId}` : `wecom:user:${event.conversation.peerId}`,
58
+ To:
59
+ event.conversation.peerKind === "group"
60
+ ? `wecom:group:${event.conversation.peerId}`
61
+ : `wecom:user:${event.conversation.peerId}`,
51
62
  SessionKey: route.sessionKey,
52
63
  AccountId: route.accountId,
53
64
  ChatType: event.conversation.peerKind,
@@ -57,7 +68,10 @@ export async function prepareInboundSession(params: {
57
68
  Provider: "wecom",
58
69
  Surface: "wecom",
59
70
  OriginatingChannel: "wecom",
60
- OriginatingTo: event.conversation.peerKind === "group" ? `wecom:group:${event.conversation.peerId}` : `wecom:user:${event.conversation.peerId}`,
71
+ OriginatingTo:
72
+ event.conversation.peerKind === "group"
73
+ ? `wecom:group:${event.conversation.peerId}`
74
+ : `wecom:user:${event.conversation.peerId}`,
61
75
  MessageSid: event.messageId,
62
76
  CommandAuthorized: true,
63
77
  MediaPath: mediaPath,
@@ -69,7 +83,7 @@ export async function prepareInboundSession(params: {
69
83
  storePath,
70
84
  sessionKey: ctx.SessionKey ?? route.sessionKey,
71
85
  ctx,
72
- onRecordError: () => { },
86
+ onRecordError: () => {},
73
87
  });
74
88
 
75
89
  return { route, ctx, storePath };