@openclaw-china/wecom 2026.3.18 → 2026.3.20

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.
package/dist/index.d.ts CHANGED
@@ -414,6 +414,38 @@ declare const wecomPlugin: {
414
414
  error?: Error;
415
415
  }>;
416
416
  };
417
+ status: {
418
+ defaultRuntime: {
419
+ accountId: string;
420
+ running: boolean;
421
+ lastStartAt: null;
422
+ lastStopAt: null;
423
+ lastError: null;
424
+ };
425
+ buildAccountSnapshot: ({ account, runtime, probe }: {
426
+ account: ResolvedWecomAccount;
427
+ runtime?: Record<string, unknown>;
428
+ probe?: unknown;
429
+ }) => {
430
+ accountId: string;
431
+ name: string | undefined;
432
+ enabled: boolean;
433
+ configured: boolean;
434
+ linked: boolean;
435
+ connected: boolean;
436
+ running: boolean;
437
+ lastStartAt: number | null;
438
+ lastStopAt: number | null;
439
+ lastError: string | null;
440
+ lastInboundAt: number | null;
441
+ lastOutboundAt: number | null;
442
+ mode: string;
443
+ webhookPath: string | undefined;
444
+ dmPolicy: WecomDmPolicy;
445
+ allowFrom: string[] | undefined;
446
+ probe: unknown;
447
+ };
448
+ };
417
449
  gateway: {
418
450
  startAccount: (ctx: {
419
451
  cfg: PluginConfig;
@@ -476,6 +508,10 @@ interface PluginRuntime {
476
508
  mediaUrl?: string;
477
509
  mediaUrls?: string[];
478
510
  }) => Promise<void>;
511
+ onSkip?: (payload: unknown, info: {
512
+ kind: string;
513
+ reason: string;
514
+ }) => void;
479
515
  onError?: (err: unknown, info: {
480
516
  kind: string;
481
517
  }) => void;
package/dist/index.js CHANGED
@@ -1729,6 +1729,7 @@ var CHANNEL_ORDER = [
1729
1729
  "wecom",
1730
1730
  "wecom-app",
1731
1731
  "wecom-kf",
1732
+ "wechat-mp",
1732
1733
  "feishu-china"
1733
1734
  ];
1734
1735
  var CHANNEL_DISPLAY_LABELS = {
@@ -1737,6 +1738,7 @@ var CHANNEL_DISPLAY_LABELS = {
1737
1738
  wecom: "WeCom\uFF08\u4F01\u4E1A\u5FAE\u4FE1-\u667A\u80FD\u673A\u5668\u4EBA\uFF09",
1738
1739
  "wecom-app": "WeCom App\uFF08\u81EA\u5EFA\u5E94\u7528-\u53EF\u63A5\u5165\u5FAE\u4FE1\uFF09",
1739
1740
  "wecom-kf": "WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09",
1741
+ "wechat-mp": "WeChat MP\uFF08\u5FAE\u4FE1\u516C\u4F17\u53F7\uFF09",
1740
1742
  qqbot: "QQBot\uFF08QQ \u673A\u5668\u4EBA\uFF09"
1741
1743
  };
1742
1744
  var CHANNEL_GUIDE_LINKS = {
@@ -1745,6 +1747,7 @@ var CHANNEL_GUIDE_LINKS = {
1745
1747
  wecom: `${GUIDES_BASE}/wecom/configuration.md`,
1746
1748
  "wecom-app": `${GUIDES_BASE}/wecom-app/configuration.md`,
1747
1749
  "wecom-kf": "https://github.com/BytePioneer-AI/openclaw-china/blob/main/extensions/wecom-kf/README.md",
1750
+ "wechat-mp": `${GUIDES_BASE}/wechat-mp/configuration.md`,
1748
1751
  qqbot: `${GUIDES_BASE}/qqbot/configuration.md`
1749
1752
  };
1750
1753
  var CHINA_CLI_STATE_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-cli-state");
@@ -1954,7 +1957,9 @@ function isChannelConfigured(cfg, channelId) {
1954
1957
  case "wecom-app":
1955
1958
  return hasTokenPair(channelCfg);
1956
1959
  case "wecom-kf":
1957
- return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.corpSecret) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
1960
+ return hasNonEmptyString(channelCfg.corpId) && hasNonEmptyString(channelCfg.token) && hasNonEmptyString(channelCfg.encodingAESKey);
1961
+ case "wechat-mp":
1962
+ return hasNonEmptyString(channelCfg.appId) && hasNonEmptyString(channelCfg.token);
1958
1963
  default:
1959
1964
  return false;
1960
1965
  }
@@ -2215,6 +2220,15 @@ async function configureWecomKf(prompter, cfg) {
2215
2220
  section("\u914D\u7F6E WeCom KF\uFF08\u5FAE\u4FE1\u5BA2\u670D\uFF09");
2216
2221
  showGuideLink("wecom-kf");
2217
2222
  const existing = getChannelConfig(cfg, "wecom-kf");
2223
+ Ve(
2224
+ [
2225
+ "\u5411\u5BFC\u987A\u5E8F\uFF1AwebhookPath / token / encodingAESKey / corpId / open_kfid / corpSecret",
2226
+ "\u57FA\u7840\u5FC5\u586B\uFF1AcorpId / token / encodingAESKey / open_kfid",
2227
+ "corpSecret \u4F1A\u4F5C\u4E3A\u6700\u540E\u4E00\u4E2A\u53C2\u6570\u8BE2\u95EE\uFF1B\u9996\u6B21\u63A5\u5165\u53EF\u5148\u7559\u7A7A\uFF0C\u5F85\u56DE\u8C03 URL \u6821\u9A8C\u901A\u8FC7\u5E76\u70B9\u51FB\u201C\u5F00\u59CB\u4F7F\u7528\u201D\u540E\u518D\u8865",
2228
+ "webhookPath \u9ED8\u8BA4\u503C\uFF1A/wecom-kf"
2229
+ ].join("\n"),
2230
+ "\u53C2\u6570\u8BF4\u660E"
2231
+ );
2218
2232
  const webhookPath = await prompter.askText({
2219
2233
  label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wecom-kf\uFF09",
2220
2234
  defaultValue: toTrimmedString(existing.webhookPath) ?? "/wecom-kf",
@@ -2235,19 +2249,14 @@ async function configureWecomKf(prompter, cfg) {
2235
2249
  defaultValue: toTrimmedString(existing.corpId),
2236
2250
  required: true
2237
2251
  });
2238
- const corpSecret = await prompter.askSecret({
2239
- label: "\u5FAE\u4FE1\u5BA2\u670D Secret",
2240
- existingValue: toTrimmedString(existing.corpSecret),
2241
- required: true
2242
- });
2243
2252
  const openKfId = await prompter.askText({
2244
2253
  label: "open_kfid",
2245
2254
  defaultValue: toTrimmedString(existing.openKfId),
2246
2255
  required: true
2247
2256
  });
2248
- const welcomeText = await prompter.askText({
2249
- label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
2250
- defaultValue: toTrimmedString(existing.welcomeText),
2257
+ const corpSecret = await prompter.askSecret({
2258
+ label: "\u5FAE\u4FE1\u5BA2\u670D Secret\uFF08\u6700\u540E\u586B\u5199\uFF1B\u9996\u6B21\u63A5\u5165\u53EF\u5148\u7559\u7A7A\uFF09",
2259
+ existingValue: toTrimmedString(existing.corpSecret),
2251
2260
  required: false
2252
2261
  });
2253
2262
  return mergeChannelConfig(cfg, "wecom-kf", {
@@ -2255,8 +2264,89 @@ async function configureWecomKf(prompter, cfg) {
2255
2264
  token,
2256
2265
  encodingAESKey,
2257
2266
  corpId,
2258
- corpSecret,
2259
2267
  openKfId,
2268
+ corpSecret: corpSecret || void 0
2269
+ });
2270
+ }
2271
+ async function configureWechatMp(prompter, cfg) {
2272
+ section("\u914D\u7F6E WeChat MP\uFF08\u5FAE\u4FE1\u516C\u4F17\u53F7\uFF09");
2273
+ showGuideLink("wechat-mp");
2274
+ const existing = getChannelConfig(cfg, "wechat-mp");
2275
+ const webhookPath = await prompter.askText({
2276
+ label: "Webhook \u8DEF\u5F84\uFF08\u9ED8\u8BA4 /wechat-mp\uFF09",
2277
+ defaultValue: toTrimmedString(existing.webhookPath) ?? "/wechat-mp",
2278
+ required: true
2279
+ });
2280
+ const appId = await prompter.askText({
2281
+ label: "\u516C\u4F17\u53F7 appId",
2282
+ defaultValue: toTrimmedString(existing.appId),
2283
+ required: true
2284
+ });
2285
+ const appSecret = await prompter.askSecret({
2286
+ label: "\u516C\u4F17\u53F7 appSecret\uFF08\u4E3B\u52A8\u53D1\u9001\u9700\u8981\uFF09",
2287
+ existingValue: toTrimmedString(existing.appSecret),
2288
+ required: false
2289
+ });
2290
+ const token = await prompter.askSecret({
2291
+ label: "\u670D\u52A1\u5668\u914D\u7F6E token",
2292
+ existingValue: toTrimmedString(existing.token),
2293
+ required: true
2294
+ });
2295
+ const messageMode = await prompter.askSelect(
2296
+ "\u6D88\u606F\u52A0\u89E3\u5BC6\u6A21\u5F0F",
2297
+ [
2298
+ { value: "plain", label: "plain\uFF08\u660E\u6587\uFF09" },
2299
+ { value: "safe", label: "safe\uFF08\u5B89\u5168\u6A21\u5F0F\uFF09" },
2300
+ { value: "compat", label: "compat\uFF08\u517C\u5BB9\u6A21\u5F0F\uFF09" }
2301
+ ],
2302
+ toTrimmedString(existing.messageMode) ?? "safe"
2303
+ );
2304
+ let encodingAESKey = toTrimmedString(existing.encodingAESKey);
2305
+ if (messageMode !== "plain") {
2306
+ encodingAESKey = await prompter.askSecret({
2307
+ label: "EncodingAESKey\uFF08safe/compat \u5FC5\u586B\uFF09",
2308
+ existingValue: encodingAESKey,
2309
+ required: true
2310
+ });
2311
+ }
2312
+ const replyMode = await prompter.askSelect(
2313
+ "\u56DE\u590D\u6A21\u5F0F",
2314
+ [
2315
+ { value: "passive", label: "passive\uFF085 \u79D2\u5185\u88AB\u52A8\u56DE\u590D\uFF09" },
2316
+ { value: "active", label: "active\uFF08\u5BA2\u670D\u6D88\u606F\u4E3B\u52A8\u53D1\u9001\uFF09" }
2317
+ ],
2318
+ toTrimmedString(existing.replyMode) ?? "passive"
2319
+ );
2320
+ let activeDeliveryMode;
2321
+ if (replyMode === "active") {
2322
+ activeDeliveryMode = await prompter.askSelect(
2323
+ "\u4E3B\u52A8\u53D1\u9001\u6A21\u5F0F\uFF08activeDeliveryMode\uFF09",
2324
+ [
2325
+ { value: "split", label: "split\uFF08\u9010\u5757\u53D1\u9001\uFF0C\u63A8\u8350\uFF09" },
2326
+ { value: "merged", label: "merged\uFF08\u5408\u5E76\u540E\u5355\u6B21\u53D1\u9001\uFF09" }
2327
+ ],
2328
+ toTrimmedString(existing.activeDeliveryMode) ?? "split"
2329
+ );
2330
+ }
2331
+ const renderMarkdown = await prompter.askConfirm(
2332
+ "\u542F\u7528 Markdown \u6E32\u67D3\uFF08\u63A8\u8350\u5F00\u542F\uFF09",
2333
+ toBoolean(existing.renderMarkdown, true)
2334
+ );
2335
+ const welcomeText = await prompter.askText({
2336
+ label: "\u6B22\u8FCE\u8BED\uFF08\u53EF\u9009\uFF09",
2337
+ defaultValue: toTrimmedString(existing.welcomeText),
2338
+ required: false
2339
+ });
2340
+ return mergeChannelConfig(cfg, "wechat-mp", {
2341
+ webhookPath,
2342
+ appId,
2343
+ appSecret: appSecret || void 0,
2344
+ token,
2345
+ encodingAESKey: messageMode === "plain" ? void 0 : encodingAESKey,
2346
+ messageMode,
2347
+ replyMode,
2348
+ activeDeliveryMode,
2349
+ renderMarkdown,
2260
2350
  welcomeText: welcomeText || void 0
2261
2351
  });
2262
2352
  }
@@ -2318,6 +2408,8 @@ async function configureSingleChannel(channel, prompter, cfg) {
2318
2408
  return configureWecomApp(prompter, cfg);
2319
2409
  case "wecom-kf":
2320
2410
  return configureWecomKf(prompter, cfg);
2411
+ case "wechat-mp":
2412
+ return configureWechatMp(prompter, cfg);
2321
2413
  case "qqbot":
2322
2414
  return configureQQBot(prompter, cfg);
2323
2415
  default:
@@ -2459,6 +2551,7 @@ var SUPPORTED_CHANNELS = [
2459
2551
  "wecom",
2460
2552
  "wecom-app",
2461
2553
  "wecom-kf",
2554
+ "wechat-mp",
2462
2555
  "qqbot"
2463
2556
  ];
2464
2557
  var CHINA_INSTALL_HINT_SHOWN_KEY = /* @__PURE__ */ Symbol.for("@openclaw-china/china-install-hint-shown");
@@ -7181,6 +7274,8 @@ function normalizeWecomWsCallback(frame) {
7181
7274
  var MESSAGE_CONTEXT_TTL_MS = 6 * 60 * 1e3;
7182
7275
  var EVENT_CONTEXT_TTL_MS = 10 * 1e3;
7183
7276
  var STREAM_FINISH_GRACE_MS = 2500;
7277
+ var WECOM_WS_THINKING_MESSAGE = "<think></think>";
7278
+ var WECOM_WS_FINISH_FALLBACK_MESSAGE = "\u2705 \u5904\u7406\u5B8C\u6210\u3002";
7184
7279
  var messageContexts = /* @__PURE__ */ new Map();
7185
7280
  var eventContexts = /* @__PURE__ */ new Map();
7186
7281
  var messageBySessionKey = /* @__PURE__ */ new Map();
@@ -7342,6 +7437,8 @@ function registerWecomWsMessageContext(params) {
7342
7437
  pendingAutoImagePaths: [],
7343
7438
  createdAt: now2(),
7344
7439
  updatedAt: now2(),
7440
+ placeholderContent: "",
7441
+ suppressVisibleFallback: false,
7345
7442
  started: false,
7346
7443
  finished: false,
7347
7444
  queue: Promise.resolve(),
@@ -7367,6 +7464,7 @@ async function sendWecomWsMessagePlaceholder(params) {
7367
7464
  finish: false
7368
7465
  })
7369
7466
  );
7467
+ context.placeholderContent = content;
7370
7468
  context.started = true;
7371
7469
  context.updatedAt = now2();
7372
7470
  });
@@ -7405,6 +7503,16 @@ function bindWecomWsRouteContext(params) {
7405
7503
  }
7406
7504
  context.updatedAt = now2();
7407
7505
  }
7506
+ function markWecomWsMessageContextSkipped(params) {
7507
+ pruneMessageContexts();
7508
+ const key = messageKey(params.accountId.trim(), params.reqId.trim());
7509
+ const context = messageContexts.get(key);
7510
+ if (!context || context.finished) return;
7511
+ const reason = String(params.reason ?? "").trim();
7512
+ if (!reason) return;
7513
+ context.suppressVisibleFallback = true;
7514
+ context.updatedAt = now2();
7515
+ }
7408
7516
  async function appendWecomWsActiveStreamChunk(params) {
7409
7517
  const result = await appendWecomWsActiveStreamReply({
7410
7518
  accountId: params.accountId,
@@ -7465,6 +7573,7 @@ async function appendWecomWsActiveStreamReply(params) {
7465
7573
  context.msgItems.push(...acceptedMsgItems);
7466
7574
  }
7467
7575
  if (chunk.trim()) {
7576
+ context.placeholderContent = "";
7468
7577
  context.content = appendStreamSnapshotContent(context.content, chunk);
7469
7578
  await context.send(
7470
7579
  buildWecomWsRespondMessageCommand({
@@ -7520,13 +7629,15 @@ async function finishWecomWsMessageContext(params) {
7520
7629
  const finalContent = errorMessage ? context.content ? `${context.content}
7521
7630
 
7522
7631
  ${errorMessage}` : errorMessage : context.content;
7523
- const sendFinish = context.started || Boolean(finalContent) || context.msgItems.length > 0;
7632
+ const fallbackContent = !finalContent && !context.suppressVisibleFallback && context.placeholderContent === WECOM_WS_THINKING_MESSAGE ? WECOM_WS_FINISH_FALLBACK_MESSAGE : void 0;
7633
+ const finishContent = finalContent || fallbackContent;
7634
+ const sendFinish = context.started || Boolean(finishContent) || context.msgItems.length > 0;
7524
7635
  if (sendFinish) {
7525
7636
  await context.send(
7526
7637
  buildWecomWsRespondMessageCommand({
7527
7638
  reqId: context.reqId,
7528
7639
  streamId: context.streamId,
7529
- content: finalContent || void 0,
7640
+ content: finishContent,
7530
7641
  finish: true,
7531
7642
  msgItems: context.msgItems
7532
7643
  })
@@ -8056,6 +8167,9 @@ async function dispatchWecomMessage(params) {
8056
8167
  if (!normalized.trim()) return;
8057
8168
  await hooks.onChunk(normalized);
8058
8169
  },
8170
+ onSkip: (_payload, info) => {
8171
+ hooks.onSkip?.(info);
8172
+ },
8059
8173
  onError: (err, info) => {
8060
8174
  hooks.onError?.(err);
8061
8175
  logger.error(`${info.kind} reply failed: ${String(err)}`);
@@ -9214,6 +9328,52 @@ async function handleWecomWebhookRequest(req, res) {
9214
9328
  jsonOk(res, encReply);
9215
9329
  return true;
9216
9330
  }
9331
+
9332
+ // src/status.ts
9333
+ function resolveWsLinked(runtime2) {
9334
+ if (typeof runtime2?.linked === "boolean") return runtime2.linked;
9335
+ return runtime2?.connectionState === "ready";
9336
+ }
9337
+ function resolveWsConnected(runtime2) {
9338
+ if (typeof runtime2?.connected === "boolean") return runtime2.connected;
9339
+ return runtime2?.connectionState === "ready";
9340
+ }
9341
+ function normalizeStringArray(value) {
9342
+ if (!Array.isArray(value)) return void 0;
9343
+ const normalized = value.map((entry) => entry.trim()).filter(Boolean);
9344
+ return normalized.length > 0 ? normalized : void 0;
9345
+ }
9346
+ function createDefaultWecomRuntime(accountId) {
9347
+ return {
9348
+ accountId,
9349
+ running: false,
9350
+ lastStartAt: null,
9351
+ lastStopAt: null,
9352
+ lastError: null
9353
+ };
9354
+ }
9355
+ function buildWecomAccountSnapshot(params) {
9356
+ const { account, runtime: runtime2, probe } = params;
9357
+ return {
9358
+ accountId: account.accountId,
9359
+ name: account.name,
9360
+ enabled: account.enabled,
9361
+ configured: account.configured,
9362
+ linked: resolveWsLinked(runtime2),
9363
+ connected: resolveWsConnected(runtime2),
9364
+ running: runtime2?.running ?? false,
9365
+ lastStartAt: runtime2?.lastStartAt ?? null,
9366
+ lastStopAt: runtime2?.lastStopAt ?? null,
9367
+ lastError: runtime2?.lastError ?? null,
9368
+ lastInboundAt: runtime2?.lastInboundAt ?? null,
9369
+ lastOutboundAt: runtime2?.lastOutboundAt ?? null,
9370
+ mode: runtime2?.mode ?? account.mode,
9371
+ webhookPath: runtime2?.webhookPath ?? (account.mode === "webhook" ? account.config.webhookPath?.trim() || "/wecom" : void 0),
9372
+ dmPolicy: account.config.dmPolicy ?? "pairing",
9373
+ allowFrom: normalizeStringArray(account.config.allowFrom),
9374
+ probe
9375
+ };
9376
+ }
9217
9377
  var DOC_BIZ_TYPE = "doc";
9218
9378
  var DEFAULT_DOC_MCP_TYPE = "streamable-http";
9219
9379
  var MCP_GET_CONFIG_CMD = "aibot_get_mcp_config";
@@ -9378,6 +9538,7 @@ async function fetchAndSaveWecomDocMcpConfig(params) {
9378
9538
  var activeConnections = /* @__PURE__ */ new Map();
9379
9539
  var processedMessageIds = /* @__PURE__ */ new Map();
9380
9540
  var PROCESSED_MESSAGE_TTL_MS = 10 * 60 * 1e3;
9541
+ var WECOM_WS_SHUTDOWN_GRACE_MS = 1e3;
9381
9542
  var activatedTargets = /* @__PURE__ */ new Map();
9382
9543
  function getOrCreateConnection(accountId) {
9383
9544
  let conn = activeConnections.get(accountId);
@@ -9493,7 +9654,11 @@ function summarizeWecomReplyFrame(frame) {
9493
9654
  }
9494
9655
  return JSON.stringify(summary);
9495
9656
  }
9496
- function createSdkLogger(logger) {
9657
+ function isExpectedShutdownWsLog(message) {
9658
+ const lowered = message.toLowerCase();
9659
+ return lowered.includes("invalid websocket frame") || lowered.includes("invalid opcode") || lowered.includes("websocket connection closed: code: 1006");
9660
+ }
9661
+ function createSdkLogger(logger, opts) {
9497
9662
  return {
9498
9663
  debug(message, ...args) {
9499
9664
  logger.debug(formatLogMessage(message, args));
@@ -9502,13 +9667,27 @@ function createSdkLogger(logger) {
9502
9667
  logger.info(formatLogMessage(message, args));
9503
9668
  },
9504
9669
  warn(message, ...args) {
9505
- logger.warn(formatLogMessage(message, args));
9670
+ const formatted = formatLogMessage(message, args);
9671
+ if (opts?.isShuttingDown?.() && isExpectedShutdownWsLog(formatted)) {
9672
+ logger.debug(`wecom ws shutdown noise suppressed: ${formatted}`);
9673
+ return;
9674
+ }
9675
+ logger.warn(formatted);
9506
9676
  },
9507
9677
  error(message, ...args) {
9508
- logger.error(formatLogMessage(message, args));
9678
+ const formatted = formatLogMessage(message, args);
9679
+ if (opts?.isShuttingDown?.() && isExpectedShutdownWsLog(formatted)) {
9680
+ logger.debug(`wecom ws shutdown noise suppressed: ${formatted}`);
9681
+ return;
9682
+ }
9683
+ logger.error(formatted);
9509
9684
  }
9510
9685
  };
9511
9686
  }
9687
+ function isExpectedShutdownWsError(error) {
9688
+ const message = error.message.toLowerCase();
9689
+ return message.includes("invalid websocket frame") || message.includes("invalid opcode");
9690
+ }
9512
9691
  function requireActiveClient(accountId) {
9513
9692
  const conn = activeConnections.get(accountId);
9514
9693
  if (!conn?.client) {
@@ -9634,6 +9813,8 @@ async function startWecomWsGateway(opts) {
9634
9813
  }
9635
9814
  conn.promise = new Promise((resolve4, reject) => {
9636
9815
  let finished = false;
9816
+ let shuttingDown = false;
9817
+ let shutdownTimer = null;
9637
9818
  const client = new WSClient({
9638
9819
  botId: account.botId ?? "",
9639
9820
  secret: account.secret ?? "",
@@ -9641,18 +9822,18 @@ async function startWecomWsGateway(opts) {
9641
9822
  heartbeatInterval: account.heartbeatIntervalMs,
9642
9823
  reconnectInterval: account.reconnectInitialDelayMs,
9643
9824
  maxReconnectAttempts: -1,
9644
- logger: createSdkLogger(logger)
9825
+ logger: createSdkLogger(logger, { isShuttingDown: () => shuttingDown })
9645
9826
  });
9646
9827
  conn.client = client;
9647
- const finish = (err) => {
9828
+ const cleanup = (err) => {
9648
9829
  if (finished) return;
9649
9830
  finished = true;
9831
+ if (shutdownTimer) {
9832
+ clearTimeout(shutdownTimer);
9833
+ shutdownTimer = null;
9834
+ }
9650
9835
  abortSignal?.removeEventListener("abort", onAbort);
9651
9836
  client.removeAllListeners();
9652
- try {
9653
- client.disconnect();
9654
- } catch {
9655
- }
9656
9837
  clearWecomWsReplyContextsForAccount(account.accountId);
9657
9838
  clearActivatedTargetsForAccount(account.accountId);
9658
9839
  conn.client = null;
@@ -9668,9 +9849,27 @@ async function startWecomWsGateway(opts) {
9668
9849
  if (err) reject(err);
9669
9850
  else resolve4();
9670
9851
  };
9852
+ const beginShutdown = (err) => {
9853
+ if (finished) return;
9854
+ if (shuttingDown) {
9855
+ return;
9856
+ }
9857
+ shuttingDown = true;
9858
+ abortSignal?.removeEventListener("abort", onAbort);
9859
+ shutdownTimer = setTimeout(() => {
9860
+ logger.warn(`wecom ws shutdown timed out for account ${account.accountId}; forcing cleanup`);
9861
+ cleanup(err);
9862
+ }, WECOM_WS_SHUTDOWN_GRACE_MS);
9863
+ shutdownTimer.unref?.();
9864
+ try {
9865
+ client.disconnect();
9866
+ } catch (disconnectErr) {
9867
+ cleanup(disconnectErr);
9868
+ }
9869
+ };
9671
9870
  const onAbort = () => {
9672
9871
  logger.info("abort signal received, stopping wecom ws gateway");
9673
- finish();
9872
+ beginShutdown();
9674
9873
  };
9675
9874
  const handleMessageCallback = (frame) => {
9676
9875
  const callback = normalizeWecomWsCallback(toWecomWsFrame(frame));
@@ -9717,7 +9916,7 @@ async function startWecomWsGateway(opts) {
9717
9916
  void sendWecomWsMessagePlaceholder({
9718
9917
  accountId: account.accountId,
9719
9918
  reqId: callback.reqId,
9720
- content: "\u23F3"
9919
+ content: WECOM_WS_THINKING_MESSAGE
9721
9920
  }).catch((err) => {
9722
9921
  logger.warn(`wecom ws placeholder ack failed: ${String(err)}`);
9723
9922
  });
@@ -9730,6 +9929,13 @@ async function startWecomWsGateway(opts) {
9730
9929
  runId: context.runId
9731
9930
  });
9732
9931
  },
9932
+ onSkip: (info) => {
9933
+ markWecomWsMessageContextSkipped({
9934
+ accountId: account.accountId,
9935
+ reqId: callback.reqId,
9936
+ reason: info.reason
9937
+ });
9938
+ },
9733
9939
  onChunk: async (text) => {
9734
9940
  await appendWecomWsActiveStreamChunk({
9735
9941
  accountId: account.accountId,
@@ -9919,13 +10125,20 @@ async function startWecomWsGateway(opts) {
9919
10125
  setStatus?.({
9920
10126
  accountId: account.accountId,
9921
10127
  mode: "ws",
9922
- running: true,
10128
+ running: !shuttingDown,
9923
10129
  connectionState: "disconnected",
9924
10130
  lastDisconnectAt: Date.now(),
9925
10131
  lastDisconnectReason: reason
9926
10132
  });
10133
+ if (shuttingDown) {
10134
+ cleanup();
10135
+ }
9927
10136
  });
9928
10137
  client.on("error", (error) => {
10138
+ if (shuttingDown && isExpectedShutdownWsError(error)) {
10139
+ logger.debug(`wecom ws shutdown noise suppressed: ${error.message}`);
10140
+ return;
10141
+ }
9929
10142
  logger.error(`wecom ws sdk error: ${error.message}`);
9930
10143
  setStatus?.({
9931
10144
  accountId: account.accountId,
@@ -9935,17 +10148,17 @@ async function startWecomWsGateway(opts) {
9935
10148
  });
9936
10149
  });
9937
10150
  conn.stop = () => {
9938
- finish();
10151
+ beginShutdown();
9939
10152
  };
9940
10153
  if (abortSignal?.aborted) {
9941
- finish();
10154
+ beginShutdown();
9942
10155
  return;
9943
10156
  }
9944
10157
  abortSignal?.addEventListener("abort", onAbort, { once: true });
9945
10158
  try {
9946
10159
  client.connect();
9947
10160
  } catch (err) {
9948
- finish(err);
10161
+ cleanup(err);
9949
10162
  }
9950
10163
  });
9951
10164
  return conn.promise;
@@ -10626,6 +10839,14 @@ var wecomPlugin = {
10626
10839
  }
10627
10840
  }
10628
10841
  },
10842
+ status: {
10843
+ defaultRuntime: createDefaultWecomRuntime(DEFAULT_ACCOUNT_ID),
10844
+ buildAccountSnapshot: ({ account, runtime: runtime2, probe }) => buildWecomAccountSnapshot({
10845
+ account,
10846
+ runtime: runtime2,
10847
+ probe
10848
+ })
10849
+ },
10629
10850
  gateway: {
10630
10851
  startAccount: async (ctx) => {
10631
10852
  ctx.setStatus?.({ accountId: ctx.accountId });