@yanhaidao/wecom 2.3.150 → 2.3.180

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 (43) hide show
  1. package/README.md +238 -385
  2. package/SKILLS_CAL.md +895 -0
  3. package/SKILLS_DOC.md +2136 -0
  4. package/changelog/v2.3.16.md +11 -0
  5. package/changelog/v2.3.18.md +22 -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 +19 -1
  12. package/src/capability/calendar/SKILLS_CHECKLIST.md +251 -0
  13. package/src/capability/calendar/client.ts +815 -0
  14. package/src/capability/calendar/index.ts +3 -0
  15. package/src/capability/calendar/schema.ts +417 -0
  16. package/src/capability/calendar/tool.ts +417 -0
  17. package/src/capability/calendar/types.ts +309 -0
  18. package/src/capability/doc/client.ts +567 -62
  19. package/src/capability/doc/schema.ts +419 -318
  20. package/src/capability/doc/tool.ts +1510 -1178
  21. package/src/capability/doc/types.ts +130 -14
  22. package/src/capability/mcp/index.ts +10 -0
  23. package/src/capability/mcp/schema.ts +107 -0
  24. package/src/capability/mcp/tool.ts +170 -0
  25. package/src/capability/mcp/transport.ts +394 -0
  26. package/src/channel.ts +70 -28
  27. package/src/config/schema.ts +71 -102
  28. package/src/outbound.test.ts +91 -14
  29. package/src/outbound.ts +143 -30
  30. package/src/runtime/reply-orchestrator.test.ts +35 -2
  31. package/src/runtime/reply-orchestrator.ts +14 -2
  32. package/src/runtime/session-manager.ts +20 -6
  33. package/src/runtime/source-registry.ts +165 -0
  34. package/src/target.ts +7 -4
  35. package/src/transport/bot-ws/inbound.test.ts +46 -0
  36. package/src/transport/bot-ws/inbound.ts +23 -5
  37. package/src/transport/bot-ws/media.ts +269 -0
  38. package/src/transport/bot-ws/reply.test.ts +85 -17
  39. package/src/transport/bot-ws/reply.ts +109 -21
  40. package/src/transport/bot-ws/sdk-adapter.test.ts +64 -1
  41. package/src/transport/bot-ws/sdk-adapter.ts +88 -12
  42. package/.claude/settings.local.json +0 -11
  43. package/docs/update-content-fix.md +0 -135
@@ -1,7 +1,5 @@
1
- import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
2
-
3
1
  import type { WSClient } from "@wecom/aibot-node-sdk";
4
-
2
+ import { describe, expect, it, vi, beforeEach, afterEach } from "vitest";
5
3
  import { createBotWsReplyHandle } from "./reply.js";
6
4
 
7
5
  type ReplyHandleParams = Parameters<typeof createBotWsReplyHandle>[0];
@@ -43,10 +41,10 @@ describe("createBotWsReplyHandle", () => {
43
41
  vi.advanceTimersByTime(3000);
44
42
  // Let promises flush
45
43
  await Promise.resolve();
46
-
44
+
47
45
  expect(mockClient.replyStream).toHaveBeenCalledWith(
48
46
  expect.objectContaining({
49
- headers: { req_id: "req-1" }
47
+ headers: { req_id: "req-1" },
50
48
  }),
51
49
  expect.any(String),
52
50
  "正在思考...",
@@ -69,7 +67,7 @@ describe("createBotWsReplyHandle", () => {
69
67
  vi.advanceTimersByTime(3000);
70
68
  // Flush the microtasks so `placeholderInFlight` becomes false
71
69
  for (let i = 0; i < 10; i++) await Promise.resolve();
72
-
70
+
73
71
  // Now trigger the next timer
74
72
  vi.advanceTimersByTime(3000);
75
73
  for (let i = 0; i < 10; i++) await Promise.resolve();
@@ -90,7 +88,7 @@ describe("createBotWsReplyHandle", () => {
90
88
  // Ensure interval is cleared
91
89
  vi.advanceTimersByTime(6000);
92
90
  await Promise.resolve();
93
- expect(mockClient.replyStream).toHaveBeenCalledTimes(3);
91
+ expect(mockClient.replyStream).toHaveBeenCalledTimes(3);
94
92
  });
95
93
 
96
94
  it("does not auto-send placeholder when disabled", async () => {
@@ -149,6 +147,73 @@ describe("createBotWsReplyHandle", () => {
149
147
  );
150
148
  });
151
149
 
150
+ it("streams block text even when media is deferred to final", async () => {
151
+ const handle = createBotWsReplyHandle({
152
+ client: mockClient,
153
+ frame: {
154
+ headers: { req_id: "req-block-media" },
155
+ body: {},
156
+ } as unknown as ReplyHandleParams["frame"],
157
+ accountId: "default",
158
+ inboundKind: "text",
159
+ autoSendPlaceholder: false,
160
+ });
161
+
162
+ await handle.deliver(
163
+ {
164
+ text: "正文先发",
165
+ mediaUrls: ["/tmp/a.png", "/tmp/b.png"],
166
+ isReasoning: false,
167
+ },
168
+ { kind: "block" },
169
+ );
170
+
171
+ expect(mockClient.replyStream).toHaveBeenCalledWith(
172
+ expect.objectContaining({ headers: { req_id: "req-block-media" } }),
173
+ expect.any(String),
174
+ "正文先发",
175
+ false,
176
+ );
177
+ });
178
+
179
+ it("stops placeholder keepalive when the first block contains media", async () => {
180
+ const handle = createBotWsReplyHandle({
181
+ client: mockClient,
182
+ frame: {
183
+ headers: { req_id: "req-placeholder-media" },
184
+ body: {},
185
+ } as unknown as ReplyHandleParams["frame"],
186
+ accountId: "default",
187
+ inboundKind: "text",
188
+ placeholderContent: "正在思考...",
189
+ });
190
+
191
+ vi.advanceTimersByTime(3000);
192
+ for (let i = 0; i < 10; i++) await Promise.resolve();
193
+ expect(mockClient.replyStream).toHaveBeenCalledTimes(1);
194
+
195
+ await handle.deliver(
196
+ {
197
+ text: "正文先发",
198
+ mediaUrls: ["/tmp/a.png"],
199
+ isReasoning: false,
200
+ },
201
+ { kind: "block" },
202
+ );
203
+
204
+ vi.advanceTimersByTime(6000);
205
+ for (let i = 0; i < 10; i++) await Promise.resolve();
206
+
207
+ expect(mockClient.replyStream).toHaveBeenCalledTimes(2);
208
+ expect(mockClient.replyStream).toHaveBeenNthCalledWith(
209
+ 2,
210
+ expect.objectContaining({ headers: { req_id: "req-placeholder-media" } }),
211
+ expect.any(String),
212
+ "正文先发",
213
+ false,
214
+ );
215
+ });
216
+
152
217
  it("swallows expired stream update errors during delivery", async () => {
153
218
  const expiredError = {
154
219
  headers: { req_id: "req-expired" },
@@ -157,7 +222,7 @@ describe("createBotWsReplyHandle", () => {
157
222
  };
158
223
  mockClient.replyStream.mockRejectedValueOnce(expiredError);
159
224
  const onFail = vi.fn();
160
-
225
+
161
226
  const handle = createBotWsReplyHandle({
162
227
  client: mockClient,
163
228
  frame: {
@@ -178,7 +243,13 @@ describe("createBotWsReplyHandle", () => {
178
243
 
179
244
  it.each([
180
245
  [{ headers: { req_id: "req-invalid" }, errcode: 846605, errmsg: "invalid req_id" }],
181
- [{ headers: { req_id: "req-expired" }, errcode: 846608, errmsg: "stream message update expired (>6 minutes), cannot update" }],
246
+ [
247
+ {
248
+ headers: { req_id: "req-expired" },
249
+ errcode: 846608,
250
+ errmsg: "stream message update expired (>6 minutes), cannot update",
251
+ },
252
+ ],
182
253
  ])("does not retry error reply when the ws reply window is already closed", async (error) => {
183
254
  const onFail = vi.fn();
184
255
  const handle = createBotWsReplyHandle({
@@ -218,13 +289,10 @@ describe("createBotWsReplyHandle", () => {
218
289
  handle.deliver({ text: "Event Reply", isReasoning: false }, { kind: "final" });
219
290
  await Promise.resolve();
220
291
 
221
- expect(mockClient.sendMessage).toHaveBeenCalledWith(
222
- "alice",
223
- {
224
- msgtype: "markdown",
225
- markdown: { content: "Event Reply" },
226
- }
227
- );
292
+ expect(mockClient.sendMessage).toHaveBeenCalledWith("alice", {
293
+ msgtype: "markdown",
294
+ markdown: { content: "Event Reply" },
295
+ });
228
296
  });
229
297
 
230
298
  it("sends replyWelcome for welcome events", async () => {
@@ -246,7 +314,7 @@ describe("createBotWsReplyHandle", () => {
246
314
  {
247
315
  msgtype: "text",
248
316
  text: { content: "Hello Bob" },
249
- }
317
+ },
250
318
  );
251
319
  });
252
320
  });
@@ -1,7 +1,13 @@
1
+ import {
2
+ generateReqId,
3
+ type WsFrame,
4
+ type BaseMessage,
5
+ type EventMessage,
6
+ type WSClient,
7
+ } from "@wecom/aibot-node-sdk";
1
8
  import { formatErrorMessage } from "openclaw/plugin-sdk";
2
- import { generateReqId, type WsFrame, type BaseMessage, type EventMessage, type WSClient } from "@wecom/aibot-node-sdk";
3
-
4
9
  import type { ReplyHandle, ReplyPayload } from "../../types/index.js";
10
+ import { uploadAndSendBotWsMedia } from "./media.js";
5
11
 
6
12
  const PLACEHOLDER_KEEPALIVE_MS = 3000;
7
13
  const MAX_KEEPALIVE_MS = 120 * 1000; // Force stop keepalive after 120s if ignored
@@ -32,7 +38,14 @@ function isAckTimeoutError(error: unknown): boolean {
32
38
  }
33
39
 
34
40
  function isTerminalReplyError(error: unknown): boolean {
35
- return isInvalidReqIdError(error) || isExpiredStreamUpdateError(error) || isAckTimeoutError(error);
41
+ return (
42
+ isInvalidReqIdError(error) || isExpiredStreamUpdateError(error) || isAckTimeoutError(error)
43
+ );
44
+ }
45
+
46
+ function formatMediaFailure(mediaUrl: string, error?: string, rejectReason?: string): string {
47
+ const reason = rejectReason || error || "unknown";
48
+ return `媒体发送失败:${mediaUrl} (${reason})`;
36
49
  }
37
50
 
38
51
  // Global registry to track active keepalives by peerId
@@ -54,6 +67,7 @@ export function createBotWsReplyHandle(params: {
54
67
  }): ReplyHandle {
55
68
  let streamId: string | undefined;
56
69
  let accumulatedText = "";
70
+ let deferredMediaUrls: string[] = [];
57
71
  const resolveStreamId = () => {
58
72
  streamId ||= generateReqId("stream");
59
73
  return streamId;
@@ -68,11 +82,15 @@ export function createBotWsReplyHandle(params: {
68
82
  // Extract peerId for clustering handles
69
83
  const body = params.frame.body as any;
70
84
  const peerId = String(
71
- (body?.chattype === "group" ? body?.chatid || body?.from?.userid : body?.from?.userid) || "unknown"
85
+ (body?.chattype === "group" ? body?.chatid || body?.from?.userid : body?.from?.userid) ||
86
+ "unknown",
72
87
  );
73
88
  const reqId = params.frame.headers.req_id || "unknown";
74
89
 
75
- const isEvent = params.inboundKind === "welcome" || params.inboundKind === "event" || params.inboundKind === "template-card-event";
90
+ const isEvent =
91
+ params.inboundKind === "welcome" ||
92
+ params.inboundKind === "event" ||
93
+ params.inboundKind === "template-card-event";
76
94
 
77
95
  const stopPlaceholderKeepalive = () => {
78
96
  if (placeholderKeepalive) {
@@ -83,7 +101,7 @@ export function createBotWsReplyHandle(params: {
83
101
  clearTimeout(placeholderTimeout);
84
102
  placeholderTimeout = undefined;
85
103
  }
86
-
104
+
87
105
  // Remove from registry
88
106
  const keepalives = activeKeepalivesByPeer.get(peerId);
89
107
  if (keepalives) {
@@ -107,7 +125,8 @@ export function createBotWsReplyHandle(params: {
107
125
  const sendPlaceholder = () => {
108
126
  if (streamSettled || placeholderInFlight || isEvent) return;
109
127
  placeholderInFlight = true;
110
- params.client.replyStream(params.frame, resolveStreamId(), placeholderText, false)
128
+ params.client
129
+ .replyStream(params.frame, resolveStreamId(), placeholderText, false)
111
130
  .catch((error) => {
112
131
  if (!isTerminalReplyError(error)) {
113
132
  return;
@@ -134,13 +153,27 @@ export function createBotWsReplyHandle(params: {
134
153
  }
135
154
  };
136
155
 
156
+ const mergeDeferredMediaUrls = (urls: string[]): string[] => {
157
+ if (urls.length === 0) {
158
+ return deferredMediaUrls;
159
+ }
160
+ const merged = [...deferredMediaUrls];
161
+ for (const url of urls) {
162
+ if (!merged.includes(url)) {
163
+ merged.push(url);
164
+ }
165
+ }
166
+ deferredMediaUrls = merged;
167
+ return deferredMediaUrls;
168
+ };
169
+
137
170
  if (params.autoSendPlaceholder !== false && !isEvent) {
138
171
  sendPlaceholder();
139
172
  placeholderKeepalive = setInterval(() => {
140
173
  sendPlaceholder();
141
174
  }, PLACEHOLDER_KEEPALIVE_MS);
142
-
143
- // Safety net: force stop keepalive after MAX_KEEPALIVE_MS
175
+
176
+ // Safety net: force stop keepalive after MAX_KEEPALIVE_MS
144
177
  // in case the message is completely ignored by the core and never triggers deliver/fail
145
178
  placeholderTimeout = setTimeout(() => {
146
179
  stopPlaceholderKeepalive();
@@ -183,10 +216,22 @@ export function createBotWsReplyHandle(params: {
183
216
  return;
184
217
  }
185
218
 
186
- const text = payload.text?.trim();
187
- if (!text) return;
219
+ const text = payload.text?.trim() || "";
220
+ const incomingMediaUrls = payload.mediaUrls || (payload.mediaUrl ? [payload.mediaUrl] : []);
221
+ const hasIncomingMedia = incomingMediaUrls.length > 0;
222
+ if (info.kind !== "final" && hasIncomingMedia) {
223
+ mergeDeferredMediaUrls(incomingMediaUrls);
224
+ }
225
+ const mediaUrls =
226
+ info.kind === "final" ? mergeDeferredMediaUrls(incomingMediaUrls) : incomingMediaUrls;
227
+ if (!text && mediaUrls.length === 0) {
228
+ return;
229
+ }
188
230
 
189
231
  if (info.kind === "block") {
232
+ if (!text) {
233
+ return;
234
+ }
190
235
  accumulatedText = accumulatedText ? `${accumulatedText}\n${text}` : text;
191
236
  }
192
237
 
@@ -199,6 +244,46 @@ export function createBotWsReplyHandle(params: {
199
244
  : text
200
245
  : accumulatedText || text;
201
246
 
247
+ let finalText = outboundText;
248
+ if (info.kind === "final" && mediaUrls.length > 0) {
249
+ const mediaFailures: string[] = [];
250
+ const mediaNotes: string[] = [];
251
+ let mediaSent = 0;
252
+ for (const mediaUrl of mediaUrls) {
253
+ const result = await uploadAndSendBotWsMedia({
254
+ wsClient: params.client,
255
+ chatId: peerId,
256
+ mediaUrl,
257
+ });
258
+ if (result.ok) {
259
+ mediaSent += 1;
260
+ if (result.downgradeNote) {
261
+ mediaNotes.push(result.downgradeNote);
262
+ }
263
+ continue;
264
+ }
265
+ mediaFailures.push(formatMediaFailure(mediaUrl, result.error, result.rejectReason));
266
+ }
267
+
268
+ if (!finalText && mediaSent > 0) {
269
+ finalText = "文件已发送。";
270
+ }
271
+ if (mediaFailures.length > 0) {
272
+ finalText = finalText
273
+ ? `${finalText}\n\n${mediaFailures.join("\n")}`
274
+ : mediaFailures.join("\n");
275
+ }
276
+ if (mediaNotes.length > 0) {
277
+ finalText = finalText
278
+ ? `${finalText}\n\n${mediaNotes.join("\n")}`
279
+ : mediaNotes.join("\n");
280
+ }
281
+ deferredMediaUrls = [];
282
+ }
283
+ if (!finalText) {
284
+ return;
285
+ }
286
+
202
287
  // Event frames do not support streaming chunks
203
288
  if (isEvent && info.kind !== "final") {
204
289
  return;
@@ -209,19 +294,19 @@ export function createBotWsReplyHandle(params: {
209
294
  if (params.inboundKind === "welcome") {
210
295
  await params.client.replyWelcome(params.frame, {
211
296
  msgtype: "text",
212
- text: { content: outboundText },
297
+ text: { content: finalText },
213
298
  });
214
299
  } else if (isEvent) {
215
300
  // Send push message for other events
216
301
  await params.client.sendMessage(peerId, {
217
302
  msgtype: "markdown",
218
- markdown: { content: outboundText },
303
+ markdown: { content: finalText },
219
304
  });
220
305
  } else {
221
306
  await params.client.replyStream(
222
307
  params.frame,
223
308
  resolveStreamId(),
224
- outboundText,
309
+ finalText,
225
310
  info.kind === "final",
226
311
  );
227
312
  }
@@ -243,17 +328,20 @@ export function createBotWsReplyHandle(params: {
243
328
  }
244
329
  const message = formatErrorMessage(error);
245
330
  const text = `WeCom WS reply failed: ${message}`;
246
-
331
+
247
332
  try {
248
333
  if (params.inboundKind === "welcome") {
249
- await params.client.replyWelcome(params.frame, { msgtype: "text", text: { content: text }});
334
+ await params.client.replyWelcome(params.frame, {
335
+ msgtype: "text",
336
+ text: { content: text },
337
+ });
250
338
  } else if (isEvent) {
251
- await params.client.sendMessage(peerId, {
252
- msgtype: "markdown",
253
- markdown: { content: text },
254
- });
339
+ await params.client.sendMessage(peerId, {
340
+ msgtype: "markdown",
341
+ markdown: { content: text },
342
+ });
255
343
  } else {
256
- await params.client.replyStream(params.frame, resolveStreamId(), text, true);
344
+ await params.client.replyStream(params.frame, resolveStreamId(), text, true);
257
345
  }
258
346
  } catch (sendError) {
259
347
  params.onFail?.(sendError);
@@ -5,6 +5,7 @@ const sdkMockState = vi.hoisted(() => {
5
5
  readonly handlers = new Map<string, Array<(payload: any) => void>>();
6
6
  readonly isConnected = true;
7
7
  readonly replyStream = vi.fn().mockResolvedValue(undefined);
8
+ readonly replyWelcome = vi.fn().mockResolvedValue(undefined);
8
9
 
9
10
  constructor(_options: unknown) {
10
11
  sdkMockState.client = this;
@@ -117,7 +118,69 @@ describe("BotWsSdkAdapter", () => {
117
118
  }),
118
119
  );
119
120
  expect(log.error).toHaveBeenCalledWith(
120
- expect.stringContaining("frame handler failed account=acc-1 reqId=req-1 message=frame exploded"),
121
+ expect.stringContaining(
122
+ "frame handler failed account=acc-1 reqId=req-1 message=frame exploded",
123
+ ),
124
+ );
125
+ expect(unhandledRejections).toHaveLength(0);
126
+ });
127
+
128
+ it("short-circuits enter_chat welcome events to a static ws welcome reply", async () => {
129
+ process.on("unhandledRejection", onUnhandledRejection);
130
+
131
+ const runtime = {
132
+ account: {
133
+ accountId: "acc-1",
134
+ bot: {
135
+ wsConfigured: true,
136
+ ws: {
137
+ botId: "bot-1",
138
+ secret: "secret-1",
139
+ },
140
+ config: {
141
+ welcomeText: "欢迎来到 WeCom",
142
+ },
143
+ },
144
+ },
145
+ handleEvent: vi.fn().mockResolvedValue(undefined),
146
+ updateTransportSession: vi.fn(),
147
+ touchTransportSession: vi.fn(),
148
+ recordOperationalIssue: vi.fn(),
149
+ };
150
+ const log = {
151
+ info: vi.fn(),
152
+ warn: vi.fn(),
153
+ error: vi.fn(),
154
+ };
155
+
156
+ new BotWsSdkAdapter(runtime as any, log as any).start();
157
+
158
+ sdkMockState.client?.emit("event", {
159
+ cmd: "aibot_event_callback",
160
+ headers: { req_id: "req-welcome" },
161
+ body: {
162
+ msgid: "msg-welcome",
163
+ msgtype: "event",
164
+ chattype: "single",
165
+ from: { userid: "user-1" },
166
+ event: { eventtype: "enter_chat" },
167
+ },
168
+ });
169
+
170
+ await waitForAsyncCallbacks();
171
+
172
+ expect(runtime.handleEvent).not.toHaveBeenCalled();
173
+ expect(sdkMockState.client?.replyWelcome).toHaveBeenCalledWith(
174
+ expect.objectContaining({
175
+ headers: { req_id: "req-welcome" },
176
+ }),
177
+ {
178
+ msgtype: "text",
179
+ text: { content: "欢迎来到 WeCom" },
180
+ },
181
+ );
182
+ expect(log.info).toHaveBeenCalledWith(
183
+ expect.stringContaining("static welcome delivered account=acc-1 messageId=msg-welcome"),
121
184
  );
122
185
  expect(unhandledRejections).toHaveLength(0);
123
186
  });
@@ -1,13 +1,18 @@
1
1
  import crypto from "node:crypto";
2
-
3
- import AiBot, { type BaseMessage, type EventMessage, type WsFrame } from "@wecom/aibot-node-sdk";
4
-
5
- import type { RuntimeLogSink } from "../../types/index.js";
2
+ import AiBot, {
3
+ generateReqId,
4
+ type BaseMessage,
5
+ type EventMessage,
6
+ type WsFrame,
7
+ } from "@wecom/aibot-node-sdk";
8
+ import type { WecomAccountRuntime } from "../../app/account-runtime.js";
6
9
  import { registerBotWsPushHandle, unregisterBotWsPushHandle } from "../../app/index.js";
10
+ import { clearWecomMcpAccountCache } from "../../capability/mcp/index.js";
11
+ import type { RuntimeLogSink } from "../../types/index.js";
7
12
  import { mapBotWsFrameToInboundEvent } from "./inbound.js";
13
+ import { uploadAndSendBotWsMedia } from "./media.js";
8
14
  import { createBotWsReplyHandle } from "./reply.js";
9
15
  import { createBotWsSessionSnapshot } from "./session.js";
10
- import type { WecomAccountRuntime } from "../../app/account-runtime.js";
11
16
 
12
17
  export class BotWsSdkAdapter {
13
18
  private client?: AiBot.WSClient;
@@ -32,15 +37,35 @@ export class BotWsSdkAdapter {
32
37
  botId: bot.ws.botId,
33
38
  secret: bot.ws.secret,
34
39
  logger: {
35
- debug: (message, ...args) => this.log.info?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
36
- info: (message, ...args) => this.log.info?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
37
- warn: (message, ...args) => this.log.warn?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
38
- error: (message, ...args) => this.log.error?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
40
+ debug: (message, ...args) =>
41
+ this.log.info?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
42
+ info: (message, ...args) =>
43
+ this.log.info?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
44
+ warn: (message, ...args) =>
45
+ this.log.warn?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
46
+ error: (message, ...args) =>
47
+ this.log.error?.(`[wecom-ws] ${message} ${args.join(" ")}`.trim()),
39
48
  },
40
49
  });
41
50
  this.client = client;
42
51
  registerBotWsPushHandle(this.runtime.account.accountId, {
43
52
  isConnected: () => client.isConnected,
53
+ replyCommand: async ({ cmd, body, headers }) => {
54
+ const result = await client.reply(
55
+ { headers: headers ?? { req_id: generateReqId("wecom_ws") } },
56
+ body ?? {},
57
+ cmd,
58
+ );
59
+ this.runtime.touchTransportSession("bot-ws", {
60
+ ownerId: this.ownerId,
61
+ running: true,
62
+ connected: client.isConnected,
63
+ authenticated: client.isConnected,
64
+ lastOutboundAt: Date.now(),
65
+ lastError: undefined,
66
+ });
67
+ return result as Record<string, unknown>;
68
+ },
44
69
  sendMarkdown: async (chatId, content) => {
45
70
  await client.sendMessage(chatId, {
46
71
  msgtype: "markdown",
@@ -55,6 +80,29 @@ export class BotWsSdkAdapter {
55
80
  lastError: undefined,
56
81
  });
57
82
  },
83
+ sendMedia: async ({ chatId, mediaUrl, text, mediaLocalRoots }) => {
84
+ const result = await uploadAndSendBotWsMedia({
85
+ wsClient: client,
86
+ chatId,
87
+ mediaUrl,
88
+ mediaLocalRoots,
89
+ });
90
+ if (result.ok && text?.trim()) {
91
+ await client.sendMessage(chatId, {
92
+ msgtype: "markdown",
93
+ markdown: { content: text.trim() },
94
+ });
95
+ }
96
+ this.runtime.touchTransportSession("bot-ws", {
97
+ ownerId: this.ownerId,
98
+ running: true,
99
+ connected: client.isConnected,
100
+ authenticated: client.isConnected,
101
+ lastOutboundAt: Date.now(),
102
+ lastError: result.ok ? undefined : result.error,
103
+ });
104
+ return result;
105
+ },
58
106
  });
59
107
 
60
108
  client.on("connected", () => {
@@ -82,8 +130,12 @@ export class BotWsSdkAdapter {
82
130
  });
83
131
 
84
132
  client.on("disconnected", (reason) => {
133
+ clearWecomMcpAccountCache(this.runtime.account.accountId);
85
134
  const normalizedReason = String(reason ?? "").toLowerCase();
86
- const kicked = normalizedReason.includes("kick") || normalizedReason.includes("owner") || normalizedReason.includes("replaced");
135
+ const kicked =
136
+ normalizedReason.includes("kick") ||
137
+ normalizedReason.includes("owner") ||
138
+ normalizedReason.includes("replaced");
87
139
  this.log.warn?.(
88
140
  `[wecom-ws] disconnected account=${this.runtime.account.accountId} kicked=${String(kicked)} reason=${reason ?? "unknown"}`,
89
141
  );
@@ -109,11 +161,15 @@ export class BotWsSdkAdapter {
109
161
  });
110
162
 
111
163
  client.on("reconnecting", (attempt) => {
112
- this.log.warn?.(`[wecom-ws] reconnecting account=${this.runtime.account.accountId} attempt=${attempt}`);
164
+ this.log.warn?.(
165
+ `[wecom-ws] reconnecting account=${this.runtime.account.accountId} attempt=${attempt}`,
166
+ );
113
167
  });
114
168
 
115
169
  client.on("error", (error) => {
116
- this.log.error?.(`[wecom-ws] error account=${this.runtime.account.accountId} message=${error.message}`);
170
+ this.log.error?.(
171
+ `[wecom-ws] error account=${this.runtime.account.accountId} message=${error.message}`,
172
+ );
117
173
  this.runtime.updateTransportSession(
118
174
  createBotWsSessionSnapshot({
119
175
  accountId: this.runtime.account.accountId,
@@ -176,6 +232,25 @@ export class BotWsSdkAdapter {
176
232
  });
177
233
  },
178
234
  });
235
+
236
+ const staticWelcomeText =
237
+ event.inboundKind === "welcome" ? botAccount.config.welcomeText?.trim() : undefined;
238
+ if (staticWelcomeText) {
239
+ this.log.info?.(
240
+ `[wecom-ws] static welcome reply account=${this.runtime.account.accountId} messageId=${event.messageId} peer=${event.conversation.peerKind}:${event.conversation.peerId} len=${staticWelcomeText.length}`,
241
+ );
242
+ await replyHandle.deliver(
243
+ {
244
+ text: staticWelcomeText,
245
+ },
246
+ { kind: "final" },
247
+ );
248
+ this.log.info?.(
249
+ `[wecom-ws] static welcome delivered account=${this.runtime.account.accountId} messageId=${event.messageId}`,
250
+ );
251
+ return;
252
+ }
253
+
179
254
  await this.runtime.handleEvent(event, replyHandle);
180
255
  };
181
256
 
@@ -221,6 +296,7 @@ export class BotWsSdkAdapter {
221
296
 
222
297
  stop(): void {
223
298
  this.log.info?.(`[wecom-ws] stop account=${this.runtime.account.accountId}`);
299
+ clearWecomMcpAccountCache(this.runtime.account.accountId);
224
300
  unregisterBotWsPushHandle(this.runtime.account.accountId);
225
301
  this.runtime.updateTransportSession(
226
302
  createBotWsSessionSnapshot({
@@ -1,11 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(npx tsc:*)",
5
- "Bash(./node_modules/.bin/tsc:*)",
6
- "Bash(npm install)",
7
- "Bash(npx vitest:*)",
8
- "Bash(node --input-type=module -e:*)"
9
- ]
10
- }
11
- }