cli-wechat-bridge 1.1.5 → 1.1.7

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 (52) hide show
  1. package/README.md +212 -232
  2. package/bin/_run-entry.mjs +20 -5
  3. package/bin/wecom-check-update.mjs +5 -0
  4. package/bin/wecom-claude.mjs +5 -0
  5. package/bin/wecom-codex.mjs +5 -0
  6. package/bin/wecom-daemon.mjs +5 -0
  7. package/bin/wecom-opencode.mjs +5 -0
  8. package/bin/wecom-pi.mjs +5 -0
  9. package/bin/wecom-setup.mjs +5 -0
  10. package/dist/bridge/bridge-adapters.claude.js +128 -4
  11. package/dist/bridge/bridge-adapters.codex.js +214 -23
  12. package/dist/bridge/bridge-adapters.core.js +2 -1
  13. package/dist/bridge/bridge-adapters.pi.js +1 -1
  14. package/dist/bridge/bridge-adapters.shared.js +2 -1
  15. package/dist/bridge/bridge-final-reply.js +2 -65
  16. package/dist/bridge/bridge-process-reaper.js +35 -2
  17. package/dist/bridge/bridge-state.js +11 -4
  18. package/dist/bridge/bridge-utils.js +4 -96
  19. package/dist/bridge/wechat-bridge.js +476 -206
  20. package/dist/bridge/wechat-forwarding.js +2 -58
  21. package/dist/bridge/wechat-outbound-queue.js +2 -0
  22. package/dist/channels/wechat/bridge-final-reply.js +65 -0
  23. package/dist/channels/wechat/channel-message.js +24 -0
  24. package/dist/channels/wechat/index.js +5 -0
  25. package/dist/channels/wechat/wechat-channel-port.js +67 -0
  26. package/dist/channels/wechat/wechat-forwarding.js +79 -0
  27. package/dist/channels/wechat/wechat-outbound-queue.js +79 -0
  28. package/dist/channels/wecom/bridge-final-reply.js +26 -0
  29. package/dist/channels/wecom/setup-cli.js +6 -0
  30. package/dist/channels/wecom/setup.js +176 -0
  31. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  32. package/dist/channels/wecom/wecom-config.js +66 -0
  33. package/dist/channels/wecom/wecom-media.js +141 -0
  34. package/dist/channels/wecom/wecom-message.js +149 -0
  35. package/dist/channels/wecom/wecom-transport.js +308 -0
  36. package/dist/companion/codex-remote-client.js +2 -1
  37. package/dist/companion/local-companion-start.js +49 -9
  38. package/dist/companion/local-companion.js +2 -1
  39. package/dist/core/bridge-control.js +32 -0
  40. package/dist/core/bridge-event-forwarder.js +50 -0
  41. package/dist/core/bridge-message-router.js +32 -0
  42. package/dist/core/channel-types.js +3 -0
  43. package/dist/core/direct-run.js +17 -0
  44. package/dist/core/inbound-message-claims.js +61 -0
  45. package/dist/core/text-utils.js +63 -0
  46. package/dist/daemon/daemon-link.js +3 -0
  47. package/dist/daemon/wechat-daemon.js +466 -172
  48. package/dist/i18n/messages-en.js +4 -2
  49. package/dist/i18n/messages-zh.js +4 -2
  50. package/dist/utils/doctor.js +30 -10
  51. package/dist/wechat/setup.js +113 -13
  52. package/package.json +117 -107
@@ -4,18 +4,26 @@ import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
4
4
  import { delay } from "./bridge-adapters.shared.js";
5
5
  import { t } from "../i18n/index.js";
6
6
  import { BridgeController } from "./bridge-controller.js";
7
- import { forwardWechatFinalReply } from "./bridge-final-reply.js";
8
7
  import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "./bridge-session-resume.js";
9
- import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "./wechat-forwarding.js";
8
+ import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
10
9
  import { ensureWechatCredentials } from "../wechat/setup.js";
11
10
  import { BridgeStateStore } from "./bridge-state.js";
12
11
  import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "./bridge-process-reaper.js";
13
12
  import { createRuntimeHost } from "../runtime/create-runtime-host.js";
13
+ import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
14
+ import { routeBridgeMessage } from "../core/bridge-message-router.js";
15
+ import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
16
+ import { isDirectModuleRun } from "../core/direct-run.js";
17
+ import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
18
+ import { ensureWecomAccount } from "../channels/wecom/setup.js";
19
+ import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
20
+ import { WecomTransport } from "../channels/wecom/wecom-transport.js";
21
+ import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
14
22
  import { buildWechatInboundPrompt, buildOneTimeCode, formatApprovalMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatDuration, formatMirroredUserInputMessage, formatSessionSwitchMessage, formatStatusReport, formatTaskFailedMessage, formatThinkingForWechat, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "./bridge-utils.js";
15
23
  import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
24
+ import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
16
25
  import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
17
26
  import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
18
- import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "../daemon/emoji-bindings.js";
19
27
  const POLL_RETRY_BASE_MS = 1_000;
20
28
  const POLL_RETRY_MAX_MS = 30_000;
21
29
  const PARENT_PROCESS_POLL_MS = 5_000;
@@ -102,6 +110,7 @@ export function parseCliArgs(argv) {
102
110
  let profile;
103
111
  let lifecycle = "persistent";
104
112
  let sessionStartMode = "restore";
113
+ let channelId = "wechat";
105
114
  for (let i = 0; i < argv.length; i += 1) {
106
115
  const arg = argv[i];
107
116
  const next = argv[i + 1];
@@ -148,6 +157,13 @@ export function parseCliArgs(argv) {
148
157
  sessionStartMode = next;
149
158
  i += 1;
150
159
  break;
160
+ case "--channel":
161
+ if (!next || (next !== "wechat" && next !== "wecom")) {
162
+ throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
163
+ }
164
+ channelId = next;
165
+ i += 1;
166
+ break;
151
167
  case "--shutdown-on-parent-exit":
152
168
  lifecycle = "companion_bound";
153
169
  break;
@@ -170,6 +186,51 @@ export function parseCliArgs(argv) {
170
186
  profile,
171
187
  lifecycle,
172
188
  sessionStartMode,
189
+ channelId,
190
+ };
191
+ }
192
+ function toWecomOutputKind(context) {
193
+ const known = new Set([
194
+ "status",
195
+ "notice",
196
+ "thinking",
197
+ "final_reply",
198
+ "approval_required",
199
+ "user_input_required",
200
+ "mirrored_input",
201
+ "task_failed",
202
+ "fatal_error",
203
+ ]);
204
+ return known.has(context)
205
+ ? context
206
+ : context === "mirrored_user_input"
207
+ ? "mirrored_input"
208
+ : "notice";
209
+ }
210
+ function toWechatSendContext(kind) {
211
+ if (kind === "mirrored_input") {
212
+ return "mirrored_user_input";
213
+ }
214
+ if (kind === "status") {
215
+ return "message";
216
+ }
217
+ return kind;
218
+ }
219
+ function toLegacyInboundWechatMessage(message) {
220
+ return {
221
+ senderId: message.senderId,
222
+ sender: message.senderId,
223
+ sessionId: message.conversation.conversationId,
224
+ text: message.text,
225
+ attachments: message.attachments.map((attachment) => ({
226
+ kind: attachment.kind === "image" ? "image" : "file",
227
+ path: attachment.path,
228
+ fileName: attachment.fileName || path.basename(attachment.path),
229
+ sizeBytes: attachment.sizeBytes ?? 0,
230
+ })),
231
+ contextToken: message.conversation.opaqueRef,
232
+ createdAt: message.createdAt,
233
+ createdAtMs: Date.parse(message.createdAt),
173
234
  };
174
235
  }
175
236
  function printUsageAndExit() {
@@ -177,7 +238,7 @@ function printUsageAndExit() {
177
238
  "Internal bridge runtime usage:",
178
239
  " npm run bridge -- --adapter <codex|claude|opencode|pi> [--cmd <executable>] [--cwd <path>] [--profile <name-or-path>] [--lifecycle <persistent|companion_bound>] [--session-start-mode <restore|new>]",
179
240
  "",
180
- "This entry is internal. Users should run wechat-codex, wechat-claude, wechat-opencode, wechat-pi, or wechat-daemon.",
241
+ "This entry is internal. Users should run a wechat-* or wecom-* launcher.",
181
242
  "",
182
243
  ].join("\n"));
183
244
  process.exit(0);
@@ -191,21 +252,37 @@ async function main() {
191
252
  const options = parseCliArgs(process.argv.slice(2));
192
253
  const daemonEndpoint = readDaemonEndpoint();
193
254
  if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
194
- throw new Error(`wechat-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
255
+ throw new Error(`${daemonEndpoint.channelId ?? "wechat"}-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
195
256
  }
196
257
  if (daemonEndpoint) {
197
258
  clearDaemonEndpoint(daemonEndpoint.pid);
198
259
  log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
199
260
  }
200
- const credentials = await ensureWechatCredentials({
201
- requireUserId: true,
202
- validateExisting: true,
203
- log,
204
- });
261
+ const wecomAccount = options.channelId === "wecom"
262
+ ? await ensureWecomAccount({ log })
263
+ : null;
264
+ const credentials = wecomAccount
265
+ ? { userId: wecomAccount.operatorUserId }
266
+ : await ensureWechatCredentials({
267
+ requireUserId: true,
268
+ validateExisting: true,
269
+ log,
270
+ });
205
271
  if (!credentials.userId) {
206
- throw new Error("Saved WeChat credentials are missing userId.");
272
+ throw new Error(options.channelId === "wecom"
273
+ ? "Saved WeCom credentials are missing operatorUserId."
274
+ : "Saved WeChat credentials are missing userId.");
207
275
  }
208
276
  const transport = new WeChatTransport({ log, logError });
277
+ const wecomTransport = wecomAccount
278
+ ? new WecomTransport({
279
+ account: wecomAccount,
280
+ logger: {
281
+ log: (message) => log(message),
282
+ error: (message) => logError(message),
283
+ },
284
+ })
285
+ : null;
209
286
  // 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
210
287
  // unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
211
288
  // 否则会与强制退出 teardown 竞态。
@@ -224,6 +301,7 @@ async function main() {
224
301
  const stateStore = new BridgeStateStore({
225
302
  ...options,
226
303
  authorizedUserId: credentials.userId,
304
+ channelId: options.channelId,
227
305
  });
228
306
  const reapedPeerPids = await reapPeerBridgeProcesses({
229
307
  logger: (message) => stateStore.appendLog(message),
@@ -270,6 +348,9 @@ async function main() {
270
348
  initialResumeConversationId: stateStore.getState().resumeConversationId,
271
349
  initialTranscriptPath: stateStore.getState().transcriptPath,
272
350
  });
351
+ const pendingWechatMessages = new PendingWechatMessageStore(options.channelId === "wechat"
352
+ ? getPendingWechatMessagesFile(options.cwd)
353
+ : getPendingChannelMessagesFile(options.cwd, options.channelId));
273
354
  const resumeCoordinator = new ResumeSessionCoordinator({
274
355
  adapter: options.adapter,
275
356
  runtime: adapter,
@@ -281,6 +362,15 @@ async function main() {
281
362
  let attachmentSendChain = Promise.resolve();
282
363
  const pendingWechatForwardTasks = new Set();
283
364
  let activeTask = null;
365
+ let currentWecomConversation = null;
366
+ let activeWecomConversation = null;
367
+ let lastWecomConversation = {
368
+ channelId: "wecom",
369
+ accountId: wecomAccount?.botId,
370
+ conversationId: credentials.userId,
371
+ recipientId: credentials.userId,
372
+ metadata: { chatType: "direct" },
373
+ };
284
374
  const deferredInboundMessages = [];
285
375
  let drainingDeferredInboundMessages = false;
286
376
  let consecutivePollFailures = 0;
@@ -295,48 +385,116 @@ async function main() {
295
385
  attachmentSendChain = run.then(() => undefined, () => undefined);
296
386
  return run;
297
387
  };
298
- const queueWechatMessage = (senderId, text, context = "message") => {
299
- return queueWechatTextAction(async () => {
300
- for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
301
- try {
302
- await transport.sendText(senderId, text);
303
- return true;
388
+ const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
389
+ if (options.channelId === "wecom") {
390
+ const target = targetOverride ??
391
+ (senderId === credentials.userId
392
+ ? currentWecomConversation ??
393
+ activeWecomConversation ??
394
+ lastWecomConversation
395
+ : {
396
+ channelId: "wecom",
397
+ accountId: wecomAccount?.botId,
398
+ conversationId: senderId,
399
+ recipientId: senderId,
400
+ metadata: { chatType: "direct" },
401
+ });
402
+ const startedAtMs = Date.now();
403
+ stateStore.appendLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
404
+ try {
405
+ await wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
406
+ stateStore.appendLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
407
+ return { status: "sent" };
408
+ }
409
+ catch (error) {
410
+ const message = error instanceof Error ? error.message : String(error);
411
+ stateStore.appendLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
412
+ logError(`Failed to send WeCom ${context}: ${message}`);
413
+ return { status: "context_unavailable", error, target };
414
+ }
415
+ }
416
+ const startedAtMs = Date.now();
417
+ stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
418
+ for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
419
+ try {
420
+ await transport.sendText(senderId, text);
421
+ stateStore.appendLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
422
+ return { status: "sent" };
423
+ }
424
+ catch (err) {
425
+ if (err instanceof Error && err.name === "AbortError") {
426
+ stateStore.appendLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
304
427
  }
305
- catch (err) {
428
+ if (isWechatContextUnavailableError(err)) {
306
429
  if (isWechatContextTokenStaleError(err)) {
307
430
  transport.clearCachedContextToken(senderId);
308
- const hint = "WeChat conversation context is stale. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
309
- logError(`Failed to send WeChat ${context}: ${hint}`);
310
- stateStore.appendLog(formatWechatContextTokenStaleLogEntry({
431
+ }
432
+ const hint = "WeChat conversation context is stale or unavailable. Ask the WeChat owner to send any message first, then local terminal replies can sync back to WeChat.";
433
+ logError(`Failed to send WeChat ${context}: ${hint}`);
434
+ stateStore.appendLog(isWechatContextTokenStaleError(err)
435
+ ? formatWechatContextTokenStaleLogEntry({
311
436
  context,
312
437
  recipientId: senderId,
313
438
  error: err,
314
- }));
315
- return false;
316
- }
317
- if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
318
- const delayMs = computeWechatSendRetryDelayMs(attempt);
319
- logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
320
- stateStore.appendLog(formatWechatSendRetryLogEntry({
439
+ })
440
+ : formatWechatSendFailureLogEntry({
321
441
  context,
322
442
  recipientId: senderId,
323
- attempt,
324
- delayMs,
325
443
  error: err,
326
444
  }));
327
- await delay(delayMs);
328
- continue;
329
- }
330
- logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
331
- stateStore.appendLog(formatWechatSendFailureLogEntry({
445
+ return { status: "context_unavailable", error: err };
446
+ }
447
+ if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
448
+ const delayMs = computeWechatSendRetryDelayMs(attempt);
449
+ logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
450
+ stateStore.appendLog(formatWechatSendRetryLogEntry({
332
451
  context,
333
452
  recipientId: senderId,
453
+ attempt,
454
+ delayMs,
334
455
  error: err,
335
456
  }));
336
- return false;
457
+ await delay(delayMs);
458
+ continue;
459
+ }
460
+ logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
461
+ stateStore.appendLog(formatWechatSendFailureLogEntry({
462
+ context,
463
+ recipientId: senderId,
464
+ error: err,
465
+ }));
466
+ return { status: "failed" };
467
+ }
468
+ }
469
+ return { status: "failed" };
470
+ };
471
+ const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
472
+ return queueWechatTextAction(async () => {
473
+ const result = await sendWechatMessageNow(senderId, text, context, targetOverride);
474
+ if (result.status === "context_unavailable") {
475
+ const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
476
+ if (pending) {
477
+ stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
337
478
  }
338
479
  }
339
- return false;
480
+ return result.status === "sent";
481
+ });
482
+ };
483
+ const flushPendingWechatMessages = () => {
484
+ return queueWechatTextAction(async () => {
485
+ for (const pending of pendingWechatMessages.list()) {
486
+ const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
487
+ if (result.status === "sent") {
488
+ pendingWechatMessages.remove(pending.id);
489
+ stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
490
+ continue;
491
+ }
492
+ if (result.status === "context_unavailable") {
493
+ break;
494
+ }
495
+ stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
496
+ break;
497
+ }
340
498
  });
341
499
  };
342
500
  const trackWechatForwardTask = (task) => {
@@ -382,6 +540,10 @@ async function main() {
382
540
  drainingDeferredInboundMessages = true;
383
541
  try {
384
542
  stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
543
+ if (nextDeferred.channelMessage) {
544
+ activeWecomConversation = nextDeferred.channelMessage.conversation;
545
+ lastWecomConversation = nextDeferred.channelMessage.conversation;
546
+ }
385
547
  const nextTask = await dispatchInboundWechatText({
386
548
  message: nextDeferred.message,
387
549
  options,
@@ -470,6 +632,12 @@ async function main() {
470
632
  catch {
471
633
  // Best effort shutdown.
472
634
  }
635
+ try {
636
+ wecomTransport?.stop();
637
+ }
638
+ catch {
639
+ // Best effort shutdown.
640
+ }
473
641
  controller.clearLocalClientEndpoint();
474
642
  stateStore.releaseLock();
475
643
  };
@@ -549,6 +717,7 @@ async function main() {
549
717
  maybeDrainDeferredInboundMessages,
550
718
  clearActiveTask: () => {
551
719
  activeTask = null;
720
+ activeWecomConversation = null;
552
721
  },
553
722
  syncSharedSessionState: () => {
554
723
  syncSharedSessionState(stateStore, adapter);
@@ -557,6 +726,8 @@ async function main() {
557
726
  controller.syncLocalClientEndpoint();
558
727
  },
559
728
  requestShutdown,
729
+ wecomTransport,
730
+ getWecomTarget: () => activeWecomConversation ?? lastWecomConversation,
560
731
  });
561
732
  await adapter.start();
562
733
  if (!ensureRuntimeOwnership()) {
@@ -564,13 +735,98 @@ async function main() {
564
735
  }
565
736
  syncSharedSessionState(stateStore, adapter);
566
737
  controller.syncLocalClientEndpoint();
567
- stateStore.appendLog(`Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
568
- log(`WeChat bridge is ready for adapter "${options.adapter}".`);
738
+ stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
739
+ if (wecomTransport) {
740
+ wecomTransport.setHandlers({
741
+ onMessage: async (channelMessage) => {
742
+ if (shutdownPromise || !ensureRuntimeOwnership()) {
743
+ return;
744
+ }
745
+ currentWecomConversation = channelMessage.conversation;
746
+ lastWecomConversation = channelMessage.conversation;
747
+ try {
748
+ if (pendingWechatMessages.list().length > 0) {
749
+ await flushPendingWechatMessages();
750
+ }
751
+ const message = toLegacyInboundWechatMessage(channelMessage);
752
+ stateStore.touchActivity(message.createdAt);
753
+ let nextTask = null;
754
+ try {
755
+ nextTask = await handleInboundMessage({
756
+ message,
757
+ channelMessage,
758
+ options,
759
+ stateStore,
760
+ adapter,
761
+ resumeCoordinator,
762
+ queueWechatMessage,
763
+ outputBatcher,
764
+ clearActiveTask: () => {
765
+ activeTask = null;
766
+ activeWecomConversation = null;
767
+ },
768
+ deferInboundMessage: async (nextMessage) => {
769
+ deferredInboundMessages.push({
770
+ message: nextMessage,
771
+ channelMessage,
772
+ });
773
+ stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
774
+ await queueWechatMessage(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
775
+ },
776
+ });
777
+ }
778
+ catch (error) {
779
+ const errorText = error instanceof Error ? error.message : String(error);
780
+ logError(errorText);
781
+ stateStore.appendLog(`inbound_error: ${errorText}`);
782
+ await queueWechatMessage(message.senderId, formatUserFacingInboundError({
783
+ adapter: options.adapter,
784
+ cwd: options.cwd,
785
+ errorText,
786
+ }), "inbound_error");
787
+ }
788
+ if (nextTask) {
789
+ activeTask = nextTask;
790
+ activeWecomConversation = channelMessage.conversation;
791
+ }
792
+ syncSharedSessionState(stateStore, adapter);
793
+ await maybeDrainDeferredInboundMessages();
794
+ }
795
+ finally {
796
+ currentWecomConversation = null;
797
+ }
798
+ },
799
+ onUnauthorized: async (senderId, chatType) => {
800
+ stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
801
+ if (chatType === "direct") {
802
+ await wecomTransport.sendText({
803
+ channelId: "wecom",
804
+ accountId: wecomAccount.botId,
805
+ conversationId: senderId,
806
+ recipientId: senderId,
807
+ metadata: { chatType: "direct" },
808
+ }, "Unauthorized.", "notice");
809
+ }
810
+ },
811
+ onFatal: async (error) => {
812
+ stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
813
+ requestShutdown(error.message, 1);
814
+ },
815
+ onConnected: async () => {
816
+ if (pendingWechatMessages.list().length > 0) {
817
+ await flushPendingWechatMessages();
818
+ }
819
+ },
820
+ });
821
+ wecomTransport.start();
822
+ await wecomTransport.waitUntilConnected();
823
+ }
824
+ log(`${options.channelId === "wecom" ? "WeCom" : "WeChat"} bridge is ready for adapter "${options.adapter}".`);
569
825
  log(`Working directory: ${options.cwd}`);
570
826
  if (options.profile) {
571
827
  log(`Profile: ${options.profile}`);
572
828
  }
573
- log(`Authorized WeChat user: ${credentials.userId}`);
829
+ log(`Authorized ${options.channelId === "wecom" ? "WeCom" : "WeChat"} user: ${credentials.userId}`);
574
830
  if (options.adapter === "codex") {
575
831
  log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
576
832
  }
@@ -583,15 +839,17 @@ async function main() {
583
839
  else if (options.adapter === "pi") {
584
840
  log("For source-mode debugging, open the visible Pi client with: npm run pi:companion");
585
841
  }
586
- loadEmojiBindings();
587
- // Keep the standalone bridge's WeChat welcome brief: command and emoji
588
- // reference material belongs to /status and /bindings, which users can
589
- // request on demand. The daemon sends its own, fuller welcome.
590
842
  const welcomeText = t("bridge.welcome", {
591
843
  adapter: options.adapter,
592
844
  cwd: options.cwd,
593
845
  });
594
846
  await queueWechatMessage(credentials.userId, welcomeText);
847
+ if (options.channelId === "wecom") {
848
+ while (!shutdownPromise && ensureRuntimeOwnership()) {
849
+ await delay(1_000);
850
+ }
851
+ return;
852
+ }
595
853
  while (true) {
596
854
  if (shutdownPromise) {
597
855
  // Shutdown (e.g. SIGINT) was requested while we were awaiting the
@@ -634,6 +892,9 @@ async function main() {
634
892
  log(`WeChat long poll recovered after ${recoveredFailures} transient error(s).`);
635
893
  stateStore.appendLog(`poll_recovered: failures=${recoveredFailures}`);
636
894
  }
895
+ if (pollResult.messages.length > 0 && pendingWechatMessages.list().length > 0) {
896
+ await flushPendingWechatMessages();
897
+ }
637
898
  if (pollResult.ignoredBacklogCount > 0) {
638
899
  stateStore.incrementIgnoredBacklog(pollResult.ignoredBacklogCount);
639
900
  stateStore.appendLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
@@ -719,7 +980,30 @@ function syncSharedSessionState(stateStore, adapter) {
719
980
  }
720
981
  }
721
982
  function wireAdapterEvents(params) {
722
- const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
983
+ const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
984
+ const channelPort = options.channelId === "wecom"
985
+ ? new WecomChannelPort({
986
+ transport: wecomTransport,
987
+ sendText: (target, text, kind) => queueWechatMessage(stateStore.getState().authorizedUserId, text, toWechatSendContext(kind), target),
988
+ onEmptyVisibleReply: (adapter, rawText) => {
989
+ stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
990
+ },
991
+ })
992
+ : new WechatChannelPort({
993
+ sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
994
+ sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
995
+ sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
996
+ sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
997
+ sendVideo: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
998
+ onEmptyVisibleReply: (adapter, rawText) => {
999
+ stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
1000
+ },
1001
+ onTextSent: (_adapter, text) => {
1002
+ stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
1003
+ },
1004
+ });
1005
+ let lastFinalReplyAtMs = 0;
1006
+ let eventForwardChain = Promise.resolve();
723
1007
  adapter.setEventSink((event) => {
724
1008
  syncSharedSessionState();
725
1009
  syncLocalClientEndpoint();
@@ -732,156 +1016,155 @@ function wireAdapterEvents(params) {
732
1016
  stateStore.clearPendingUserInput();
733
1017
  }
734
1018
  const authorizedUserId = stateStore.getState().authorizedUserId;
735
- switch (event.type) {
736
- case "stdout":
737
- case "stderr":
738
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
739
- outputBatcher.push(event.text);
1019
+ eventForwardChain = eventForwardChain
1020
+ .then(() => forwardBridgeEvent(event, {
1021
+ stdout: (next) => {
1022
+ if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
1023
+ outputBatcher.push(next.text);
740
1024
  }
741
- break;
742
- case "final_reply":
743
- stateStore.appendLog(`final_reply: ${truncatePreview(event.text)}`);
1025
+ },
1026
+ stderr: (next) => {
1027
+ if (shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
1028
+ outputBatcher.push(next.text);
1029
+ }
1030
+ },
1031
+ finalReply: (next) => {
1032
+ lastFinalReplyAtMs = Date.now();
1033
+ stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
744
1034
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
745
- await forwardWechatFinalReply({
746
- adapter: options.adapter,
747
- rawText: event.text,
748
- onEmptyVisibleReply: ({ rawVisibleText }) => {
749
- stateStore.appendLog(`empty_visible_final_reply: adapter=${options.adapter} raw=${truncatePreview(rawVisibleText)}`);
750
- },
751
- sender: {
752
- sendText: async (text) => {
753
- const sent = await queueWechatMessage(authorizedUserId, text, "final_reply");
754
- if (sent) {
755
- stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
756
- }
757
- return sent;
1035
+ await channelPort.send({
1036
+ target: options.channelId === "wecom"
1037
+ ? getWecomTarget()
1038
+ : {
1039
+ channelId: "wechat",
1040
+ conversationId: authorizedUserId,
1041
+ recipientId: authorizedUserId,
758
1042
  },
759
- sendImage: (imagePath) => queueWechatAttachmentAction(() => transport.sendImage(imagePath, { recipientId: authorizedUserId })),
760
- sendFile: (filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId: authorizedUserId })),
761
- sendVoice: (voicePath) => queueWechatAttachmentAction(() => transport.sendVoice(voicePath, authorizedUserId)),
762
- sendVideo: (videoPath) => queueWechatAttachmentAction(() => transport.sendVideo(videoPath, { recipientId: authorizedUserId })),
763
- },
1043
+ kind: "final_reply",
1044
+ text: next.text,
1045
+ adapter: options.adapter,
764
1046
  });
765
1047
  }));
766
- break;
767
- case "status":
768
- if (event.message) {
769
- log(`${event.status}: ${event.message}`);
770
- stateStore.appendLog(`${event.status}: ${event.message}`);
1048
+ },
1049
+ status: (next) => {
1050
+ if (next.message) {
1051
+ log(`${next.status}: ${next.message}`);
1052
+ stateStore.appendLog(`${next.status}: ${next.message}`);
771
1053
  }
772
1054
  void maybeDrainDeferredInboundMessages();
773
- break;
774
- case "notice":
775
- stateStore.appendLog(`${event.level}_notice: ${truncatePreview(event.text)}`);
776
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
1055
+ },
1056
+ notice: (next) => {
1057
+ stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
1058
+ if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
777
1059
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
778
- await queueWechatMessage(authorizedUserId, event.text, "notice");
1060
+ await queueWechatMessage(authorizedUserId, next.text, "notice");
779
1061
  }));
780
1062
  }
781
- break;
782
- case "thinking":
783
- if (event.text) {
784
- const thinkingPreview = formatThinkingForWechat(event.text, 500);
1063
+ },
1064
+ thinking: (next) => {
1065
+ if (next.text) {
1066
+ const thinkingPreview = formatThinkingForWechat(next.text, 500);
785
1067
  if (thinkingPreview) {
786
1068
  stateStore.appendLog(`thinking: ${thinkingPreview}`);
787
1069
  trackWechatForwardTask((async () => {
788
- await queueWechatMessage(authorizedUserId, `思考: ${thinkingPreview}`, "thinking");
1070
+ await queueWechatMessage(authorizedUserId, options.channelId === "wecom"
1071
+ ? `Processing: ${thinkingPreview}`
1072
+ : `思考: ${thinkingPreview}`, "thinking");
789
1073
  })());
790
1074
  }
791
1075
  }
792
- break;
793
- case "approval_required":
1076
+ },
1077
+ approvalRequired: (next) => {
794
1078
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
795
- const pending = toPendingApproval(event.request);
1079
+ const pending = toPendingApproval(next.request);
796
1080
  stateStore.setPendingConfirmation(pending);
797
1081
  stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
798
1082
  await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
799
1083
  }));
800
- break;
801
- case "user_input_required":
1084
+ },
1085
+ userInputRequired: (next) => {
802
1086
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
803
- const pending = toPendingUserInput(event.request);
1087
+ const pending = toPendingUserInput(next.request);
804
1088
  stateStore.setPendingUserInput(pending);
805
1089
  stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
806
1090
  await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
807
1091
  }));
808
- break;
809
- case "mirrored_user_input":
810
- stateStore.appendLog(`mirrored_local_input: ${truncatePreview(event.text)}`);
811
- if (shouldForwardBridgeEventToWechat(options.adapter, event.type, { text: event.text })) {
1092
+ },
1093
+ mirroredUserInput: (next) => {
1094
+ stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
1095
+ if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
812
1096
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
813
- await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, event.text), "mirrored_user_input");
1097
+ await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
814
1098
  }));
815
1099
  }
816
- break;
817
- case "session_switched":
818
- if (event.source === "local") {
1100
+ },
1101
+ sessionSwitched: (next) => {
1102
+ if (next.source === "local")
819
1103
  resumeCoordinator.clear();
820
- }
821
- stateStore.appendLog(`session_switched: ${event.sessionId} source=${event.source} reason=${event.reason}`);
822
- if (shouldForwardSessionSwitchEvent(event.reason) &&
823
- shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
1104
+ stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
1105
+ if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
824
1106
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
825
- await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
826
- adapter: options.adapter,
827
- sessionId: event.sessionId,
828
- source: event.source,
829
- reason: event.reason,
830
- }), "session_switched");
1107
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched");
831
1108
  }));
832
1109
  }
833
- break;
834
- case "thread_switched":
835
- if (event.source === "local") {
1110
+ },
1111
+ threadSwitched: (next) => {
1112
+ if (next.source === "local")
836
1113
  resumeCoordinator.clear();
1114
+ stateStore.appendLog(`thread_switched: ${next.threadId} source=${next.source} reason=${next.reason}`);
1115
+ if (shouldSuppressCodexLocalThreadNotice({
1116
+ adapter: options.adapter,
1117
+ source: next.source,
1118
+ activeTurnOrigin: adapter.getState().activeTurnOrigin,
1119
+ lastFinalReplyAtMs,
1120
+ })) {
1121
+ return;
837
1122
  }
838
- stateStore.appendLog(`thread_switched: ${event.threadId} source=${event.source} reason=${event.reason}`);
839
- if (shouldForwardSessionSwitchEvent(event.reason) &&
840
- shouldForwardBridgeEventToWechat(options.adapter, event.type)) {
1123
+ if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
841
1124
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
842
- await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({
843
- adapter: options.adapter,
844
- sessionId: event.threadId,
845
- source: event.source,
846
- reason: event.reason,
847
- }), "thread_switched");
1125
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched");
848
1126
  }));
849
1127
  }
850
1128
  void maybeDrainDeferredInboundMessages();
851
- break;
852
- case "task_complete":
1129
+ },
1130
+ taskComplete: () => {
853
1131
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
854
1132
  stateStore.clearPendingConfirmation();
855
1133
  stateStore.clearPendingUserInput();
856
1134
  clearActiveTask();
857
1135
  await maybeDrainDeferredInboundMessages();
858
1136
  }));
859
- break;
860
- case "task_failed":
1137
+ },
1138
+ taskFailed: (next) => {
861
1139
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
862
1140
  stateStore.clearPendingConfirmation();
863
1141
  stateStore.clearPendingUserInput();
864
1142
  clearActiveTask();
865
- await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, event.message), "task_failed");
1143
+ await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed");
866
1144
  await maybeDrainDeferredInboundMessages();
867
1145
  }));
868
- break;
869
- case "fatal_error":
870
- logError(event.message);
871
- stateStore.appendLog(`fatal_error: ${event.message}`);
1146
+ },
1147
+ fatalError: (next) => {
1148
+ logError(next.message);
1149
+ stateStore.appendLog(`fatal_error: ${next.message}`);
872
1150
  stateStore.clearPendingConfirmation();
873
1151
  stateStore.clearPendingUserInput();
874
1152
  clearActiveTask();
875
1153
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
876
- await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(event.message), "fatal_error");
1154
+ await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
877
1155
  await maybeDrainDeferredInboundMessages();
878
1156
  }));
879
- break;
880
- case "shutdown_requested":
881
- stateStore.appendLog(`shutdown_requested: ${event.reason}`);
882
- requestShutdown(event.message, event.exitCode ?? 0);
883
- break;
884
- }
1157
+ },
1158
+ shutdownRequested: (next) => {
1159
+ stateStore.appendLog(`shutdown_requested: ${next.reason}`);
1160
+ requestShutdown(next.message, next.exitCode ?? 0);
1161
+ },
1162
+ }))
1163
+ .catch((error) => {
1164
+ const message = error instanceof Error ? error.message : String(error);
1165
+ logError(`Bridge event handling failed: ${message}`);
1166
+ stateStore.appendLog(`event_forward_failed: ${message}`);
1167
+ });
885
1168
  });
886
1169
  }
887
1170
  function formatInboundMessagePreview(message) {
@@ -896,42 +1179,13 @@ function formatInboundMessagePreview(message) {
896
1179
  return "(empty)";
897
1180
  }
898
1181
  async function handleInboundMessage(params) {
899
- let { message, } = params;
1182
+ const { message, channelMessage, } = params;
900
1183
  const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
901
1184
  const state = stateStore.getState();
902
1185
  if (message.senderId !== state.authorizedUserId) {
903
- await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
904
- return null;
905
- }
906
- // Emoji resolution: rewrite message text if it starts with a bound emoji
907
- const emojiMatch = resolveEmojiCommand(message.text);
908
- if (emojiMatch) {
909
- const rewritten = emojiMatch.remainder
910
- ? `${emojiMatch.command} ${emojiMatch.remainder}`
911
- : emojiMatch.command;
912
- message = { ...message, text: rewritten };
913
- }
914
- // Emoji binding management commands
915
- const bindingsCmd = parseEmojiBindingsCommand(message.text);
916
- if (bindingsCmd) {
917
- switch (bindingsCmd.type) {
918
- case "list":
919
- await queueWechatMessage(message.senderId, formatBindingsListMessage(listBindings()));
920
- break;
921
- case "bind":
922
- setBinding(bindingsCmd.emoji, bindingsCmd.command);
923
- await queueWechatMessage(message.senderId, `Bound ${bindingsCmd.emoji} → ${bindingsCmd.command}`);
924
- break;
925
- case "unbind": {
926
- const removed = removeBinding(bindingsCmd.emoji);
927
- await queueWechatMessage(message.senderId, removed ? `Unbound ${bindingsCmd.emoji}` : `No binding found for ${bindingsCmd.emoji}`);
928
- break;
929
- }
930
- }
931
- return null;
932
- }
933
- if (isBindCommandPrefix(message.text)) {
934
- await queueWechatMessage(message.senderId, formatBindCommandUsage());
1186
+ await queueWechatMessage(message.senderId, options.channelId === "wecom"
1187
+ ? "Unauthorized. This bridge only accepts messages from the paired WeCom operator."
1188
+ : "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
935
1189
  return null;
936
1190
  }
937
1191
  const systemCommand = parseWechatControlCommand(message.text, {
@@ -945,7 +1199,7 @@ async function handleInboundMessage(params) {
945
1199
  return null;
946
1200
  case "resume": {
947
1201
  if (!isWechatResumeEnabled(options.adapter)) {
948
- await queueWechatMessage(message.senderId, `WeChat /resume is disabled in ${options.adapter} mode. Use /resume directly inside "wechat-${options.adapter}"; WeChat will follow the active local session.`);
1202
+ await queueWechatMessage(message.senderId, `${options.channelId === "wecom" ? "WeCom" : "WeChat"} /resume is disabled in ${options.adapter} mode. Use /resume directly inside "${options.channelId}-${options.adapter}"; the remote channel will follow the active local session.`);
949
1203
  return null;
950
1204
  }
951
1205
  try {
@@ -1056,40 +1310,54 @@ async function handleInboundMessage(params) {
1056
1310
  };
1057
1311
  }
1058
1312
  }
1059
- if (state.pendingConfirmation) {
1060
- await queueWechatMessage(message.senderId, formatPendingApprovalReminder(state.pendingConfirmation, adapter.getState()));
1061
- return null;
1062
- }
1063
- if (state.pendingUserInput) {
1064
- await queueWechatMessage(message.senderId, formatPendingUserInputReminder(state.pendingUserInput));
1065
- return null;
1066
- }
1067
1313
  const adapterState = adapter.getState();
1068
- if (shouldDeferCodexInboundMessage({
1069
- adapter: options.adapter,
1070
- status: adapterState.status,
1071
- activeTurnOrigin: adapterState.activeTurnOrigin,
1072
- hasPendingConfirmation: Boolean(state.pendingConfirmation),
1073
- hasSystemCommand: Boolean(systemCommand),
1074
- })) {
1075
- await deferInboundMessage(message);
1076
- return null;
1077
- }
1078
- if (adapterState.status === "busy") {
1079
- if ((options.adapter === "codex" || options.adapter === "opencode" || options.adapter === "pi") &&
1080
- adapterState.activeTurnOrigin === "local") {
1081
- await queueWechatMessage(message.senderId, `${options.adapter === "opencode" ? "OpenCode" : options.adapter === "pi" ? "Pi" : "codex"} is currently busy with a local terminal turn. Wait for it to finish or use /stop.`);
1082
- return null;
1083
- }
1084
- await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
1085
- return null;
1086
- }
1087
- return dispatchInboundWechatText({
1088
- message,
1089
- options,
1090
- stateStore,
1091
- adapter,
1314
+ const routeResult = await routeBridgeMessage({
1315
+ message: channelMessage ?? toChannelInboundMessage(message),
1316
+ authorized: true,
1317
+ command: null,
1318
+ adapterState,
1319
+ hasPendingApproval: Boolean(state.pendingConfirmation),
1320
+ hasPendingUserInput: Boolean(state.pendingUserInput),
1321
+ shouldDefer: shouldDeferCodexInboundMessage({
1322
+ adapter: options.adapter,
1323
+ status: adapterState.status,
1324
+ activeTurnOrigin: adapterState.activeTurnOrigin,
1325
+ hasPendingConfirmation: Boolean(state.pendingConfirmation),
1326
+ hasSystemCommand: Boolean(systemCommand),
1327
+ }),
1328
+ onUnauthorized: async () => undefined,
1329
+ handleCommand: async () => false,
1330
+ remindPendingApproval: async () => {
1331
+ await queueWechatMessage(message.senderId, formatPendingApprovalReminder(stateStore.getState().pendingConfirmation, adapter.getState()));
1332
+ },
1333
+ remindPendingUserInput: async () => {
1334
+ const pendingUserInput = stateStore.getState().pendingUserInput;
1335
+ await queueWechatMessage(message.senderId, pendingUserInput
1336
+ ? formatPendingUserInputReminder(pendingUserInput)
1337
+ : `${options.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`);
1338
+ },
1339
+ remindBusy: async () => {
1340
+ const currentState = adapter.getState();
1341
+ if ((options.adapter === "codex" || options.adapter === "opencode" || options.adapter === "pi") &&
1342
+ currentState.activeTurnOrigin === "local") {
1343
+ await queueWechatMessage(message.senderId, `${options.adapter === "opencode" ? "OpenCode" : options.adapter === "pi" ? "Pi" : "codex"} is currently busy with a local terminal turn. Wait for it to finish or use /stop.`);
1344
+ return;
1345
+ }
1346
+ await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
1347
+ },
1348
+ defer: async () => {
1349
+ await deferInboundMessage(message);
1350
+ },
1351
+ dispatch: async () => {
1352
+ return await dispatchInboundWechatText({
1353
+ message,
1354
+ options,
1355
+ stateStore,
1356
+ adapter,
1357
+ });
1358
+ },
1092
1359
  });
1360
+ return routeResult.kind === "dispatched" ? routeResult.result : null;
1093
1361
  }
1094
1362
  async function dispatchInboundWechatText(params) {
1095
1363
  const { message, options, stateStore, adapter } = params;
@@ -1099,10 +1367,12 @@ async function dispatchInboundWechatText(params) {
1099
1367
  inputPreview: truncatePreview(preview, 180),
1100
1368
  };
1101
1369
  stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
1102
- await adapter.sendInput(buildWechatInboundPrompt(message.text, message.attachments));
1370
+ await adapter.sendInput(options.channelId === "wecom"
1371
+ ? buildWecomInboundPrompt(message.text, message.attachments)
1372
+ : buildWechatInboundPrompt(message.text, message.attachments));
1103
1373
  return activeTask;
1104
1374
  }
1105
- const isDirectRun = Boolean(import.meta.main);
1375
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
1106
1376
  if (isDirectRun) {
1107
1377
  main().catch((err) => {
1108
1378
  logError(describeWechatTransportError(err));