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
@@ -8,17 +8,26 @@ import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
8
8
  import { t } from "../i18n/index.js";
9
9
  import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
10
10
  import { BridgeController } from "../bridge/bridge-controller.js";
11
- import { forwardWechatFinalReply } from "../bridge/bridge-final-reply.js";
12
11
  import { readBridgeLockFile, } from "../bridge/bridge-state.js";
13
12
  import { getProcessRecordByPid, isWechatDaemonCommandLine, killProcessTreeSync, listWechatDaemonProcesses, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "../bridge/bridge-process-reaper.js";
14
13
  import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, formatDuration, formatMirroredUserInputMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatSessionSwitchMessage, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "../bridge/bridge-utils.js";
15
14
  import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "../bridge/bridge-session-resume.js";
16
- import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../bridge/wechat-forwarding.js";
15
+ import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
17
16
  import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
18
17
  import { ensureWechatCredentials } from "../wechat/setup.js";
19
18
  import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
20
19
  import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
20
+ import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
21
21
  import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
22
+ import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
23
+ import { routeBridgeMessage } from "../core/bridge-message-router.js";
24
+ import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
25
+ import { isDirectModuleRun } from "../core/direct-run.js";
26
+ import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
27
+ import { ensureWecomAccount } from "../channels/wecom/setup.js";
28
+ import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
29
+ import { WecomTransport } from "../channels/wecom/wecom-transport.js";
30
+ import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
22
31
  import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
23
32
  import { attachDaemonRequestListener, buildDaemonToken, clearDaemonEndpoint, DAEMON_PROTOCOL_VERSION, isPidAlive, readDaemonEndpoint, sendDaemonRequest, sendDaemonResponse, writeDaemonEndpoint, } from "./daemon-link.js";
24
33
  const MODULE_FILE = fileURLToPath(import.meta.url);
@@ -36,11 +45,15 @@ const DAEMON_TAKEOVER_STOP_POLL_MS = 250;
36
45
  const VISIBLE_CLIENT_CONNECT_TIMEOUT_MS = 15_000;
37
46
  const VISIBLE_CLIENT_CONNECT_POLL_MS = 250;
38
47
  const DAEMON_ADAPTERS = ["codex", "claude", "opencode", "pi"];
48
+ function getCliChannelId(argv = process.argv.slice(2)) {
49
+ const index = argv.indexOf("--channel");
50
+ return index >= 0 && argv[index + 1] === "wecom" ? "wecom" : "wechat";
51
+ }
39
52
  function log(message) {
40
- process.stderr.write(`[wechat-daemon] ${message}\n`);
53
+ process.stderr.write(`[${getCliChannelId()}-daemon] ${message}\n`);
41
54
  }
42
55
  function logError(message) {
43
- process.stderr.write(`[wechat-daemon] ERROR: ${message}\n`);
56
+ process.stderr.write(`[${getCliChannelId()}-daemon] ERROR: ${message}\n`);
44
57
  }
45
58
  function appendDaemonLog(message) {
46
59
  ensureChannelDataDir();
@@ -51,6 +64,48 @@ function computePollRetryDelayMs(consecutiveFailures) {
51
64
  const exponent = Math.min(normalizedFailures - 1, 5);
52
65
  return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
53
66
  }
67
+ function toWecomOutputKind(context) {
68
+ switch (context) {
69
+ case "notice":
70
+ case "thinking":
71
+ case "final_reply":
72
+ case "approval_required":
73
+ case "user_input_required":
74
+ case "task_failed":
75
+ case "fatal_error":
76
+ return context;
77
+ case "mirrored_user_input":
78
+ return "mirrored_input";
79
+ default:
80
+ return "notice";
81
+ }
82
+ }
83
+ function toWechatSendContext(kind) {
84
+ if (kind === "mirrored_input") {
85
+ return "mirrored_user_input";
86
+ }
87
+ if (kind === "status") {
88
+ return "message";
89
+ }
90
+ return kind;
91
+ }
92
+ function toLegacyInboundWechatMessage(message) {
93
+ return {
94
+ senderId: message.senderId,
95
+ sender: message.senderId,
96
+ sessionId: message.conversation.conversationId,
97
+ text: message.text,
98
+ attachments: message.attachments.map((attachment) => ({
99
+ kind: attachment.kind === "image" ? "image" : "file",
100
+ path: attachment.path,
101
+ fileName: attachment.fileName || path.basename(attachment.path),
102
+ sizeBytes: attachment.sizeBytes ?? 0,
103
+ })),
104
+ contextToken: message.conversation.opaqueRef,
105
+ createdAt: message.createdAt,
106
+ createdAtMs: Date.parse(message.createdAt),
107
+ };
108
+ }
54
109
  function isDaemonAdapterKind(value) {
55
110
  return value === "codex" || value === "claude" || value === "opencode" || value === "pi";
56
111
  }
@@ -72,6 +127,7 @@ export function parseDaemonCliArgs(argv) {
72
127
  let profile;
73
128
  let initialAdapter;
74
129
  let openVisible = true;
130
+ let channelId = "wechat";
75
131
  for (let i = 0; i < argv.length; i += 1) {
76
132
  const arg = argv[i];
77
133
  if (!arg) {
@@ -80,10 +136,10 @@ export function parseDaemonCliArgs(argv) {
80
136
  const next = argv[i + 1];
81
137
  if (arg === "--help" || arg === "-h") {
82
138
  process.stdout.write([
83
- "Usage: wechat-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]",
139
+ `Usage: ${channelId}-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]`,
84
140
  "",
85
- "Keeps one WeChat connection alive and switches between Codex, Claude Code, OpenCode, and Pi from WeChat.",
86
- "Send /codex, /claude, /opencode, or /pi in WeChat to switch the active terminal.",
141
+ `Keeps one ${channelId === "wecom" ? "WeCom" : "WeChat"} connection alive and switches between Codex, Claude Code, OpenCode, and Pi.`,
142
+ `Send /codex, /claude, /opencode, or /pi in ${channelId === "wecom" ? "WeCom" : "WeChat"} to switch the active terminal.`,
87
143
  "",
88
144
  ].join("\n"));
89
145
  process.exit(0);
@@ -96,6 +152,14 @@ export function parseDaemonCliArgs(argv) {
96
152
  i += 1;
97
153
  continue;
98
154
  }
155
+ if (arg === "--channel") {
156
+ if (!next || (next !== "wechat" && next !== "wecom")) {
157
+ throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
158
+ }
159
+ channelId = next;
160
+ i += 1;
161
+ continue;
162
+ }
99
163
  if (arg === "--adapter") {
100
164
  if (!isDaemonAdapterKind(next)) {
101
165
  throw new Error(`Invalid adapter: ${next ?? "(missing)"}`);
@@ -118,7 +182,13 @@ export function parseDaemonCliArgs(argv) {
118
182
  }
119
183
  throw new Error(`Unknown argument: ${arg}`);
120
184
  }
121
- return { cwd, profile, initialAdapter, openVisible };
185
+ return {
186
+ cwd,
187
+ profile,
188
+ initialAdapter,
189
+ openVisible,
190
+ ...(channelId === "wecom" ? { channelId } : {}),
191
+ };
122
192
  }
123
193
  export function parseDaemonSwitchCommand(text) {
124
194
  const normalized = text.trim().toLowerCase();
@@ -217,7 +287,7 @@ export function buildVisibleClientLaunchArgs(params) {
217
287
  export function buildWindowsVisibleClientLaunchCommand(params) {
218
288
  return [
219
289
  "start",
220
- quoteWindowsCommandArg(`wechat-${params.adapter}`),
290
+ quoteWindowsCommandArg(`${params.channelId ?? "wechat"}-${params.adapter}`),
221
291
  "/D",
222
292
  quoteWindowsCommandArg(params.cwd),
223
293
  quoteWindowsCommandArg(process.execPath),
@@ -265,6 +335,7 @@ function openVisibleClient(params) {
265
335
  adapter: params.adapter,
266
336
  cwd: params.cwd,
267
337
  args,
338
+ channelId: params.channelId,
268
339
  }),
269
340
  ];
270
341
  const child = spawn(command, launchArgs, {
@@ -285,7 +356,7 @@ function openVisibleClient(params) {
285
356
  pid: child.pid,
286
357
  };
287
358
  }
288
- const title = `wechat-${params.adapter}`;
359
+ const title = `${params.channelId ?? "wechat"}-${params.adapter}`;
289
360
  const fullArgs = [process.execPath, ...args];
290
361
  if (process.platform === "darwin") {
291
362
  const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
@@ -482,6 +553,10 @@ class WechatDaemon {
482
553
  profile;
483
554
  authorizedUserId;
484
555
  transport;
556
+ channelId;
557
+ wecomTransport;
558
+ currentInboundConversation = null;
559
+ handlingInboundMessage = false;
485
560
  slots = new Map();
486
561
  // Per-adapter serialization chains for ensureSlot (see ensureSlot comment).
487
562
  slotEnsureChains = new Map();
@@ -493,6 +568,7 @@ class WechatDaemon {
493
568
  textSendChain = Promise.resolve();
494
569
  attachmentSendChain = Promise.resolve();
495
570
  pendingWechatForwardTasks = new Set();
571
+ pendingWechatMessages;
496
572
  shutdownPromise = null;
497
573
  ipcServer = null;
498
574
  endpointToken = "";
@@ -501,6 +577,19 @@ class WechatDaemon {
501
577
  this.profile = params.profile;
502
578
  this.authorizedUserId = params.authorizedUserId;
503
579
  this.transport = params.transport;
580
+ this.channelId = params.channelId ?? "wechat";
581
+ this.wecomTransport = params.wecomTransport ?? null;
582
+ if (this.channelId === "wecom") {
583
+ this.currentInboundConversation = {
584
+ channelId: "wecom",
585
+ conversationId: params.authorizedUserId,
586
+ recipientId: params.authorizedUserId,
587
+ metadata: { chatType: "direct" },
588
+ };
589
+ }
590
+ this.pendingWechatMessages = new PendingWechatMessageStore(this.channelId === "wechat"
591
+ ? getPendingWechatMessagesFile(this.cwd)
592
+ : getPendingChannelMessagesFile(this.cwd, this.channelId));
504
593
  }
505
594
  async startIpcServer() {
506
595
  this.endpointToken = buildDaemonToken();
@@ -548,6 +637,7 @@ class WechatDaemon {
548
637
  token: this.endpointToken,
549
638
  cwd: this.cwd,
550
639
  startedAt: this.startedAt,
640
+ channelId: this.channelId,
551
641
  });
552
642
  resolve();
553
643
  });
@@ -556,6 +646,7 @@ class WechatDaemon {
556
646
  getStatus() {
557
647
  return {
558
648
  cwd: this.cwd,
649
+ channelId: this.channelId,
559
650
  activeAdapter: this.activeAdapter ?? undefined,
560
651
  startedAt: this.startedAt,
561
652
  slots: Array.from(this.slots.values()).map((slot) => {
@@ -584,10 +675,52 @@ class WechatDaemon {
584
675
  }
585
676
  async runPollLoop() {
586
677
  let consecutivePollFailures = 0;
587
- log("WeChat daemon is ready.");
678
+ if (this.wecomTransport) {
679
+ this.wecomTransport.setHandlers({
680
+ onMessage: async (channelMessage) => {
681
+ if (this.shutdownPromise) {
682
+ return;
683
+ }
684
+ this.currentInboundConversation = channelMessage.conversation;
685
+ this.handlingInboundMessage = true;
686
+ try {
687
+ if (this.pendingWechatMessages.list().length > 0) {
688
+ await this.flushPendingWechatMessages();
689
+ }
690
+ await this.handleInboundMessage(toLegacyInboundWechatMessage(channelMessage));
691
+ }
692
+ finally {
693
+ this.handlingInboundMessage = false;
694
+ }
695
+ },
696
+ onUnauthorized: async (senderId, chatType) => {
697
+ appendDaemonLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
698
+ if (chatType === "direct") {
699
+ await this.wecomTransport.sendText({
700
+ channelId: "wecom",
701
+ conversationId: senderId,
702
+ recipientId: senderId,
703
+ metadata: { chatType: "direct" },
704
+ }, "Unauthorized.", "notice");
705
+ }
706
+ },
707
+ onFatal: async (error) => {
708
+ appendDaemonLog(`wecom_fatal_error: ${error.message}`);
709
+ await this.shutdown();
710
+ },
711
+ onConnected: async () => {
712
+ if (this.pendingWechatMessages.list().length > 0) {
713
+ await this.flushPendingWechatMessages();
714
+ }
715
+ },
716
+ });
717
+ this.wecomTransport.start();
718
+ await this.wecomTransport.waitUntilConnected();
719
+ }
720
+ log(`${this.channelId === "wecom" ? "WeCom" : "WeChat"} daemon is ready.`);
588
721
  log(`Working directory: ${this.cwd}`);
589
- log("Switch from WeChat with /codex, /claude, /opencode, or /pi.");
590
- appendDaemonLog(`started: cwd=${this.cwd}`);
722
+ log(`Switch from ${this.channelId === "wecom" ? "WeCom" : "WeChat"} with /codex, /claude, /opencode, or /pi.`);
723
+ appendDaemonLog(`started: channel=${this.channelId} cwd=${this.cwd}`);
591
724
  const activeSlot = this.getActiveSlot();
592
725
  const welcomeText = t("daemon.welcome", {
593
726
  cwd: this.cwd,
@@ -595,6 +728,13 @@ class WechatDaemon {
595
728
  bindings: formatBindingsListMessage(listBindings()),
596
729
  });
597
730
  await this.queueWechatMessage(this.authorizedUserId, welcomeText);
731
+ if (this.channelId === "wecom") {
732
+ while (!this.shutdownPromise) {
733
+ await delay(1_000);
734
+ }
735
+ await this.shutdownPromise;
736
+ return;
737
+ }
598
738
  while (!this.shutdownPromise) {
599
739
  let pollResult;
600
740
  try {
@@ -624,6 +764,9 @@ class WechatDaemon {
624
764
  appendDaemonLog(`poll_recovered: failures=${consecutivePollFailures}`);
625
765
  consecutivePollFailures = 0;
626
766
  }
767
+ if (pollResult.messages.length > 0 && this.pendingWechatMessages.list().length > 0) {
768
+ await this.flushPendingWechatMessages();
769
+ }
627
770
  if (pollResult.ignoredBacklogCount > 0) {
628
771
  appendDaemonLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
629
772
  if (!this.backlogNoticeSent) {
@@ -696,6 +839,12 @@ class WechatDaemon {
696
839
  await this.waitForPendingWechatForwardTasks();
697
840
  await this.textSendChain.catch(() => undefined);
698
841
  await this.attachmentSendChain.catch(() => undefined);
842
+ try {
843
+ this.wecomTransport?.stop();
844
+ }
845
+ catch {
846
+ // Best effort shutdown.
847
+ }
699
848
  for (const slot of this.slots.values()) {
700
849
  try {
701
850
  await slot.runtime.dispose();
@@ -727,7 +876,7 @@ class WechatDaemon {
727
876
  return { shuttingDown: true };
728
877
  case "ensure_slot":
729
878
  if (!isSameWorkspaceCwd(request.cwd, this.cwd)) {
730
- throw new Error(`wechat-daemon is bound to ${this.cwd}; requested cwd was ${request.cwd}.`);
879
+ throw new Error(`${this.channelId}-daemon is bound to ${this.cwd}; requested cwd was ${request.cwd}.`);
731
880
  }
732
881
  return await this.ensureSlot(request.adapter, {
733
882
  profile: request.profile,
@@ -819,6 +968,7 @@ class WechatDaemon {
819
968
  cwd: this.cwd,
820
969
  sessionStartMode,
821
970
  cliArgs: options.cliArgs,
971
+ channelId: this.channelId,
822
972
  onError: (error) => {
823
973
  appendDaemonLog(`visible_client_open_error: adapter=${adapter} error=${truncatePreview(error.message, 400)}`);
824
974
  },
@@ -899,6 +1049,8 @@ class WechatDaemon {
899
1049
  }),
900
1050
  activeTask: null,
901
1051
  lastOutputAt: 0,
1052
+ lastFinalReplyAtMs: 0,
1053
+ eventForwardChain: Promise.resolve(),
902
1054
  };
903
1055
  runtime.setEventSink((event) => {
904
1056
  this.handleSlotEvent(slot, event);
@@ -925,166 +1077,192 @@ class WechatDaemon {
925
1077
  }
926
1078
  slot.controller.syncLocalClientEndpoint();
927
1079
  }
1080
+ createWechatChannelPort(adapter) {
1081
+ if (this.channelId === "wecom") {
1082
+ return new WecomChannelPort({
1083
+ transport: this.wecomTransport,
1084
+ sendText: (target, text, kind) => this.queueWechatMessage(this.authorizedUserId, text, toWechatSendContext(kind), target),
1085
+ prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
1086
+ onEmptyVisibleReply: (currentAdapter, rawText) => {
1087
+ appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
1088
+ },
1089
+ });
1090
+ }
1091
+ return new WechatChannelPort({
1092
+ sendText: (recipientId, text, context) => this.queueWechatMessage(recipientId, text, context),
1093
+ sendImage: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(filePath, { recipientId })),
1094
+ sendFile: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, { recipientId })),
1095
+ sendVoice: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(filePath, recipientId)),
1096
+ sendVideo: (recipientId, filePath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(filePath, { recipientId })),
1097
+ prefixText: (currentAdapter, text) => prefixDaemonAdapterMessage(currentAdapter ?? adapter, text),
1098
+ onEmptyVisibleReply: (currentAdapter, rawText) => {
1099
+ appendDaemonLog(`empty_visible_final_reply: adapter=${currentAdapter ?? adapter} raw=${truncatePreview(rawText)}`);
1100
+ },
1101
+ onTextSent: (currentAdapter, text) => {
1102
+ appendDaemonLog(`final_reply_sent: adapter=${currentAdapter ?? adapter} chars=${Array.from(text).length}`);
1103
+ },
1104
+ });
1105
+ }
928
1106
  handleSlotEvent(slot, event) {
929
1107
  slot.controller.syncLocalClientEndpoint();
930
1108
  const adapterState = slot.runtime.getState();
1109
+ const channelPort = this.createWechatChannelPort(slot.adapter);
931
1110
  if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
932
1111
  slot.pendingConfirmations = [];
933
1112
  }
934
1113
  if (slot.pendingUserInput && !adapterState.pendingUserInput) {
935
1114
  slot.pendingUserInput = null;
936
1115
  }
937
- switch (event.type) {
938
- case "stdout":
939
- case "stderr":
1116
+ slot.eventForwardChain = slot.eventForwardChain
1117
+ .then(() => forwardBridgeEvent(event, {
1118
+ stdout: (next) => {
940
1119
  slot.lastOutputAt = Date.now();
941
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
942
- slot.outputBatcher.push(event.text);
1120
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1121
+ slot.outputBatcher.push(next.text);
943
1122
  }
944
- break;
945
- case "final_reply":
946
- appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(event.text)}`);
1123
+ },
1124
+ stderr: (next) => {
1125
+ slot.lastOutputAt = Date.now();
1126
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1127
+ slot.outputBatcher.push(next.text);
1128
+ }
1129
+ },
1130
+ finalReply: (next) => {
1131
+ slot.lastFinalReplyAtMs = Date.now();
1132
+ appendDaemonLog(`final_reply: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
947
1133
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
948
- await forwardWechatFinalReply({
949
- adapter: slot.adapter,
950
- rawText: event.text,
951
- onEmptyVisibleReply: ({ rawVisibleText }) => {
952
- appendDaemonLog(`empty_visible_final_reply: adapter=${slot.adapter} raw=${truncatePreview(rawVisibleText)}`);
953
- },
954
- sender: {
955
- sendText: async (text) => {
956
- const sent = await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, text), "final_reply");
957
- if (sent) {
958
- appendDaemonLog(`final_reply_sent: adapter=${slot.adapter} chars=${Array.from(text).length}`);
959
- }
960
- return sent;
961
- },
962
- sendImage: (imagePath) => this.queueWechatAttachmentAction(() => this.transport.sendImage(imagePath, {
963
- recipientId: this.authorizedUserId,
964
- })),
965
- sendFile: (filePath) => this.queueWechatAttachmentAction(() => this.transport.sendFile(filePath, {
966
- recipientId: this.authorizedUserId,
967
- })),
968
- sendVoice: (voicePath) => this.queueWechatAttachmentAction(() => this.transport.sendVoice(voicePath, this.authorizedUserId)),
969
- sendVideo: (videoPath) => this.queueWechatAttachmentAction(() => this.transport.sendVideo(videoPath, {
1134
+ await channelPort.send({
1135
+ target: this.channelId === "wecom"
1136
+ ? slot.activeConversation ??
1137
+ slot.lastConversation ??
1138
+ this.currentInboundConversation
1139
+ : {
1140
+ channelId: "wechat",
1141
+ conversationId: this.authorizedUserId,
970
1142
  recipientId: this.authorizedUserId,
971
- })),
972
- },
1143
+ },
1144
+ kind: "final_reply",
1145
+ text: next.text,
1146
+ adapter: slot.adapter,
973
1147
  });
974
1148
  }));
975
- break;
976
- case "status":
977
- if (event.message) {
978
- log(`${slot.adapter} ${event.status}: ${event.message}`);
979
- appendDaemonLog(`${slot.adapter}_${event.status}: ${event.message}`);
1149
+ },
1150
+ status: (next) => {
1151
+ if (next.message) {
1152
+ log(`${slot.adapter} ${next.status}: ${next.message}`);
1153
+ appendDaemonLog(`${slot.adapter}_${next.status}: ${next.message}`);
980
1154
  }
981
- break;
982
- case "notice":
1155
+ },
1156
+ notice: (next) => {
983
1157
  slot.lastOutputAt = Date.now();
984
- appendDaemonLog(`${slot.adapter}_${event.level}_notice: ${truncatePreview(event.text)}`);
985
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type, { text: event.text })) {
1158
+ appendDaemonLog(`${slot.adapter}_${next.level}_notice: ${truncatePreview(next.text)}`);
1159
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
986
1160
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
987
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, event.text), "notice");
1161
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, next.text), "notice");
988
1162
  }));
989
1163
  }
990
- break;
991
- case "approval_required":
1164
+ },
1165
+ approvalRequired: (next) => {
992
1166
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
993
- const pending = toPendingApproval(event);
1167
+ const pending = toPendingApproval(next);
994
1168
  slot.pendingConfirmations.push(pending);
995
1169
  appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
996
1170
  await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
997
1171
  }));
998
- break;
999
- case "user_input_required": {
1000
- // Mirror the pending request synchronously, before the async WeChat
1001
- // send (which retries with backoff). Otherwise a fast user reply can
1002
- // race past the still-unwritten mirror and be dispatched as plain
1003
- // input into a CLI that is waiting for a structured form answer.
1004
- const pending = toPendingUserInput(event.request);
1172
+ },
1173
+ userInputRequired: (next) => {
1174
+ const pending = toPendingUserInput(next.request);
1005
1175
  slot.pendingUserInput = pending;
1006
1176
  appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
1007
1177
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1008
1178
  await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
1009
1179
  }));
1010
- break;
1011
- }
1012
- case "mirrored_user_input":
1013
- appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(event.text)}`);
1014
- if (shouldForwardBridgeEventToWechat(slot.adapter, event.type, { text: event.text })) {
1180
+ },
1181
+ mirroredUserInput: (next) => {
1182
+ appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(next.text)}`);
1183
+ if (shouldForwardBridgeEventToWechat(slot.adapter, next.type, { text: next.text })) {
1015
1184
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1016
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, event.text)), "mirrored_user_input");
1185
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatMirroredUserInputMessage(slot.adapter, next.text)), "mirrored_user_input");
1017
1186
  }));
1018
1187
  }
1019
- break;
1020
- case "session_switched":
1021
- if (event.source === "local") {
1188
+ },
1189
+ sessionSwitched: (next) => {
1190
+ if (next.source === "local")
1022
1191
  slot.resumeCoordinator.clear();
1023
- }
1024
- appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${event.sessionId} source=${event.source} reason=${event.reason}`);
1025
- if (shouldForwardSessionSwitchEvent(event.reason) &&
1026
- shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
1192
+ appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${next.sessionId} source=${next.source} reason=${next.reason}`);
1193
+ if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1027
1194
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1028
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
1029
- adapter: slot.adapter,
1030
- sessionId: event.sessionId,
1031
- source: event.source,
1032
- reason: event.reason,
1033
- })), "session_switched");
1195
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason })), "session_switched");
1034
1196
  }));
1035
1197
  }
1036
- break;
1037
- case "thread_switched":
1038
- if (event.source === "local") {
1198
+ },
1199
+ threadSwitched: (next) => {
1200
+ if (next.source === "local")
1039
1201
  slot.resumeCoordinator.clear();
1202
+ appendDaemonLog(`thread_switched: adapter=${slot.adapter} thread=${next.threadId} source=${next.source} reason=${next.reason}`);
1203
+ if (shouldSuppressCodexLocalThreadNotice({
1204
+ adapter: slot.adapter,
1205
+ source: next.source,
1206
+ activeTurnOrigin: slot.runtime.getState().activeTurnOrigin,
1207
+ lastFinalReplyAtMs: slot.lastFinalReplyAtMs,
1208
+ })) {
1209
+ return;
1040
1210
  }
1041
- appendDaemonLog(`thread_switched: adapter=${slot.adapter} thread=${event.threadId} source=${event.source} reason=${event.reason}`);
1042
- if (shouldForwardSessionSwitchEvent(event.reason) &&
1043
- shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
1211
+ if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(slot.adapter, next.type)) {
1044
1212
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1045
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
1046
- adapter: slot.adapter,
1047
- sessionId: event.threadId,
1048
- source: event.source,
1049
- reason: event.reason,
1050
- })), "thread_switched");
1213
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({ adapter: slot.adapter, sessionId: next.threadId, source: next.source, reason: next.reason })), "thread_switched");
1051
1214
  }));
1052
1215
  }
1053
- break;
1054
- case "task_complete":
1216
+ },
1217
+ taskComplete: () => {
1055
1218
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
1056
1219
  slot.pendingConfirmations = [];
1057
1220
  slot.pendingUserInput = null;
1058
1221
  slot.activeTask = null;
1222
+ slot.activeConversation = undefined;
1059
1223
  }));
1060
- break;
1061
- case "task_failed":
1224
+ },
1225
+ taskFailed: (next) => {
1062
1226
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1063
1227
  slot.pendingConfirmations = [];
1064
1228
  slot.pendingUserInput = null;
1065
1229
  slot.activeTask = null;
1066
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, event.message)), "task_failed");
1230
+ slot.activeConversation = undefined;
1231
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, next.message)), "task_failed");
1067
1232
  }));
1068
- break;
1069
- case "fatal_error":
1070
- logError(`${slot.adapter}: ${event.message}`);
1071
- appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${event.message}`);
1233
+ },
1234
+ fatalError: (next) => {
1235
+ logError(`${slot.adapter}: ${next.message}`);
1236
+ appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${next.message}`);
1072
1237
  slot.pendingConfirmations = [];
1073
1238
  slot.pendingUserInput = null;
1074
1239
  slot.activeTask = null;
1240
+ slot.activeConversation = undefined;
1075
1241
  this.disposeDeadSlot(slot);
1076
1242
  this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
1077
- await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(event.message)), "fatal_error");
1243
+ await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(next.message)), "fatal_error");
1078
1244
  }));
1079
- break;
1080
- case "shutdown_requested":
1081
- appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${event.reason}`);
1082
- break;
1245
+ },
1246
+ shutdownRequested: (next) => {
1247
+ appendDaemonLog(`slot_shutdown_requested: adapter=${slot.adapter} reason=${next.reason}`);
1248
+ },
1249
+ }))
1250
+ .catch((error) => {
1251
+ const message = error instanceof Error ? error.message : String(error);
1252
+ appendDaemonLog(`event_forward_failed: adapter=${slot.adapter} message=${message}`);
1253
+ });
1254
+ }
1255
+ bindCurrentWecomConversation(slot) {
1256
+ if (this.channelId !== "wecom" || !slot || !this.currentInboundConversation) {
1257
+ return;
1083
1258
  }
1259
+ slot.lastConversation = this.currentInboundConversation;
1084
1260
  }
1085
1261
  async handleInboundMessage(message) {
1086
1262
  if (message.senderId !== this.authorizedUserId) {
1087
- await this.queueWechatMessage(message.senderId, "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
1263
+ await this.queueWechatMessage(message.senderId, this.channelId === "wecom"
1264
+ ? "Unauthorized. This daemon only accepts messages from the paired WeCom operator."
1265
+ : "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
1088
1266
  return;
1089
1267
  }
1090
1268
  const emojiMatch = resolveEmojiCommand(message.text);
@@ -1096,6 +1274,7 @@ class WechatDaemon {
1096
1274
  reuseExistingVisible: true,
1097
1275
  });
1098
1276
  if (result.activated) {
1277
+ this.bindCurrentWecomConversation(this.getActiveSlot());
1099
1278
  message = { ...message, text: emojiMatch.remainder };
1100
1279
  }
1101
1280
  else {
@@ -1122,6 +1301,7 @@ class WechatDaemon {
1122
1301
  await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchDirective.adapter}.\n${detail}`);
1123
1302
  return;
1124
1303
  }
1304
+ this.bindCurrentWecomConversation(this.getActiveSlot());
1125
1305
  if (switchDirective.remainder) {
1126
1306
  message = { ...message, text: switchDirective.remainder };
1127
1307
  }
@@ -1131,7 +1311,7 @@ class WechatDaemon {
1131
1311
  }
1132
1312
  }
1133
1313
  if (message.text.trim().toLowerCase() === "/daemon-stop") {
1134
- await this.queueWechatMessage(message.senderId, "Stopping wechat-daemon...");
1314
+ await this.queueWechatMessage(message.senderId, `Stopping ${this.channelId}-daemon...`);
1135
1315
  setTimeout(() => {
1136
1316
  void this.shutdown().finally(() => process.exit(0));
1137
1317
  }, 0);
@@ -1151,21 +1331,56 @@ class WechatDaemon {
1151
1331
  await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
1152
1332
  return;
1153
1333
  }
1334
+ this.bindCurrentWecomConversation(slot);
1335
+ const routeCurrentMessage = async (currentSlot, command) => {
1336
+ const previousTask = currentSlot.activeTask;
1337
+ await routeBridgeMessage({
1338
+ message: toChannelInboundMessage(message),
1339
+ authorized: true,
1340
+ command,
1341
+ adapterState: currentSlot.runtime.getState(),
1342
+ hasPendingApproval: currentSlot.pendingConfirmations.length > 0,
1343
+ hasPendingUserInput: Boolean(currentSlot.pendingUserInput),
1344
+ onUnauthorized: async () => undefined,
1345
+ handleCommand: async (nextCommand) => {
1346
+ await this.handleSystemCommand(message, currentSlot, nextCommand);
1347
+ return true;
1348
+ },
1349
+ remindPendingApproval: async () => {
1350
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, formatPendingApprovalReminder(currentSlot.pendingConfirmations[0], currentSlot.runtime.getState())));
1351
+ },
1352
+ remindPendingUserInput: async () => {
1353
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, currentSlot.pendingUserInput
1354
+ ? formatPendingUserInputReminder(currentSlot.pendingUserInput)
1355
+ : `${currentSlot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
1356
+ },
1357
+ remindBusy: async () => {
1358
+ await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(currentSlot.adapter, `${currentSlot.adapter} is still working. Wait for the current reply or use /stop.`));
1359
+ },
1360
+ defer: async () => undefined,
1361
+ dispatch: async () => {
1362
+ await this.dispatchInboundWechatText(message, currentSlot);
1363
+ },
1364
+ });
1365
+ if (this.channelId === "wecom" &&
1366
+ this.currentInboundConversation &&
1367
+ currentSlot.activeTask &&
1368
+ currentSlot.activeTask !== previousTask) {
1369
+ currentSlot.activeConversation = this.currentInboundConversation;
1370
+ currentSlot.lastConversation = this.currentInboundConversation;
1371
+ }
1372
+ };
1154
1373
  const command = parseWechatControlCommand(message.text, {
1155
1374
  adapter: slot.adapter,
1156
1375
  hasPendingConfirmation: slot.pendingConfirmations.length > 0,
1157
1376
  hasPendingUserInput: Boolean(slot.pendingUserInput),
1158
1377
  });
1159
- if (command) {
1160
- await this.handleSystemCommand(message, slot, command);
1161
- return;
1162
- }
1163
- if (slot.pendingConfirmations.length > 0) {
1164
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingApprovalReminder(slot.pendingConfirmations[0], slot.runtime.getState())));
1165
- return;
1166
- }
1167
- if (slot.pendingUserInput) {
1168
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingUserInputReminder(slot.pendingUserInput)));
1378
+ const slotState = slot.runtime.getState();
1379
+ if (command ||
1380
+ slot.pendingConfirmations.length > 0 ||
1381
+ slot.pendingUserInput ||
1382
+ slotState.status === "awaiting_input") {
1383
+ await routeCurrentMessage(slot, command);
1169
1384
  return;
1170
1385
  }
1171
1386
  const visibleResult = await this.ensureSlot(slot.adapter, {
@@ -1177,21 +1392,8 @@ class WechatDaemon {
1177
1392
  return;
1178
1393
  }
1179
1394
  slot = this.getActiveSlot() ?? slot;
1180
- const adapterState = slot.runtime.getState();
1181
- if (adapterState.status === "awaiting_input") {
1182
- // Belt-and-braces with the pendingUserInput mirror above: the adapter
1183
- // state is authoritative even when the mirror was cleared or the WeChat
1184
- // notification is still queued.
1185
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, slot.pendingUserInput
1186
- ? formatPendingUserInputReminder(slot.pendingUserInput)
1187
- : `${slot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
1188
- return;
1189
- }
1190
- if (adapterState.status === "busy" || adapterState.status === "awaiting_approval") {
1191
- await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
1192
- return;
1193
- }
1194
- await this.dispatchInboundWechatText(message, slot);
1395
+ this.bindCurrentWecomConversation(slot);
1396
+ await routeCurrentMessage(slot, null);
1195
1397
  }
1196
1398
  async handleEmojiBindingsCommand(senderId, cmd) {
1197
1399
  switch (cmd.type) {
@@ -1220,7 +1422,7 @@ class WechatDaemon {
1220
1422
  return;
1221
1423
  case "resume": {
1222
1424
  if (!isWechatResumeEnabled(activeSlot.adapter)) {
1223
- await this.queueWechatMessage(message.senderId, `WeChat /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; WeChat will follow that local session.`);
1425
+ await this.queueWechatMessage(message.senderId, `${this.channelId === "wecom" ? "WeCom" : "WeChat"} /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; the remote channel will follow that local session.`);
1224
1426
  return;
1225
1427
  }
1226
1428
  try {
@@ -1384,7 +1586,9 @@ class WechatDaemon {
1384
1586
  inputPreview: truncatePreview(preview, 180),
1385
1587
  };
1386
1588
  appendDaemonLog(`forwarded_input: adapter=${slot.adapter} text=${truncatePreview(preview)}`);
1387
- await slot.runtime.sendInput(buildWechatInboundPrompt(message.text, message.attachments));
1589
+ await slot.runtime.sendInput(this.channelId === "wecom"
1590
+ ? buildWecomInboundPrompt(message.text, message.attachments)
1591
+ : buildWechatInboundPrompt(message.text, message.attachments));
1388
1592
  }
1389
1593
  queueWechatTextAction(action) {
1390
1594
  const run = this.textSendChain.then(action);
@@ -1396,39 +1600,110 @@ class WechatDaemon {
1396
1600
  this.attachmentSendChain = run.then(() => undefined, () => undefined);
1397
1601
  return run;
1398
1602
  }
1399
- queueWechatMessage(senderId, text, context = "message") {
1400
- return this.queueWechatTextAction(async () => {
1401
- for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
1402
- try {
1403
- await this.transport.sendText(senderId, text);
1404
- return true;
1603
+ async sendWechatMessageNow(senderId, text, context = "message", targetOverride) {
1604
+ if (this.channelId === "wecom") {
1605
+ const activeSlot = this.getActiveSlot();
1606
+ const target = targetOverride ??
1607
+ (senderId === this.authorizedUserId
1608
+ ? (this.handlingInboundMessage
1609
+ ? this.currentInboundConversation
1610
+ : null) ??
1611
+ activeSlot?.activeConversation ??
1612
+ activeSlot?.lastConversation ??
1613
+ this.currentInboundConversation
1614
+ : {
1615
+ channelId: "wecom",
1616
+ conversationId: senderId,
1617
+ recipientId: senderId,
1618
+ metadata: { chatType: "direct" },
1619
+ });
1620
+ const startedAtMs = Date.now();
1621
+ appendDaemonLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
1622
+ try {
1623
+ await this.wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
1624
+ appendDaemonLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
1625
+ return { status: "sent" };
1626
+ }
1627
+ catch (error) {
1628
+ const message = error instanceof Error ? error.message : String(error);
1629
+ appendDaemonLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
1630
+ logError(`Failed to send WeCom ${context}: ${message}`);
1631
+ return { status: "context_unavailable", error, target };
1632
+ }
1633
+ }
1634
+ const startedAtMs = Date.now();
1635
+ appendDaemonLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
1636
+ for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
1637
+ try {
1638
+ await this.transport.sendText(senderId, text);
1639
+ appendDaemonLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
1640
+ return { status: "sent" };
1641
+ }
1642
+ catch (error) {
1643
+ if (error instanceof Error && error.name === "AbortError") {
1644
+ appendDaemonLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
1405
1645
  }
1406
- catch (error) {
1646
+ if (isWechatContextUnavailableError(error)) {
1407
1647
  if (isWechatContextTokenStaleError(error)) {
1408
1648
  this.transport.clearCachedContextToken(senderId);
1409
- appendDaemonLog(formatWechatContextTokenStaleLogEntry({
1649
+ }
1650
+ appendDaemonLog(isWechatContextTokenStaleError(error)
1651
+ ? formatWechatContextTokenStaleLogEntry({
1652
+ context,
1653
+ recipientId: senderId,
1654
+ error,
1655
+ })
1656
+ : formatWechatSendFailureLogEntry({
1410
1657
  context,
1411
1658
  recipientId: senderId,
1412
1659
  error,
1413
1660
  }));
1414
- return false;
1415
- }
1416
- if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(error)) {
1417
- const delayMs = computeWechatSendRetryDelayMs(attempt);
1418
- appendDaemonLog(`wechat_send_retry: context=${context} recipient=${senderId} attempt=${attempt} delay_ms=${delayMs} error=${truncatePreview(describeWechatTransportError(error), 400)}`);
1419
- await delay(delayMs);
1420
- continue;
1421
- }
1422
- logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
1423
- appendDaemonLog(formatWechatSendFailureLogEntry({
1424
- context,
1425
- recipientId: senderId,
1426
- error,
1427
- }));
1428
- return false;
1661
+ return { status: "context_unavailable", error };
1662
+ }
1663
+ if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(error)) {
1664
+ const delayMs = computeWechatSendRetryDelayMs(attempt);
1665
+ appendDaemonLog(`wechat_send_retry: context=${context} recipient=${senderId} attempt=${attempt} delay_ms=${delayMs} error=${truncatePreview(describeWechatTransportError(error), 400)}`);
1666
+ await delay(delayMs);
1667
+ continue;
1429
1668
  }
1669
+ logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(error)}`);
1670
+ appendDaemonLog(formatWechatSendFailureLogEntry({
1671
+ context,
1672
+ recipientId: senderId,
1673
+ error,
1674
+ }));
1675
+ return { status: "failed" };
1676
+ }
1677
+ }
1678
+ return { status: "failed" };
1679
+ }
1680
+ queueWechatMessage(senderId, text, context = "message", targetOverride) {
1681
+ return this.queueWechatTextAction(async () => {
1682
+ const result = await this.sendWechatMessageNow(senderId, text, context, targetOverride);
1683
+ if (result.status === "context_unavailable") {
1684
+ const pending = this.pendingWechatMessages.enqueue(senderId, text, context, result.target);
1685
+ if (pending) {
1686
+ appendDaemonLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${this.pendingWechatMessages.list().length}`);
1687
+ }
1688
+ }
1689
+ return result.status === "sent";
1690
+ });
1691
+ }
1692
+ flushPendingWechatMessages() {
1693
+ return this.queueWechatTextAction(async () => {
1694
+ for (const pending of this.pendingWechatMessages.list()) {
1695
+ const result = await this.sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
1696
+ if (result.status === "sent") {
1697
+ this.pendingWechatMessages.remove(pending.id);
1698
+ appendDaemonLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
1699
+ continue;
1700
+ }
1701
+ if (result.status === "context_unavailable") {
1702
+ break;
1703
+ }
1704
+ appendDaemonLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
1705
+ break;
1430
1706
  }
1431
- return false;
1432
1707
  });
1433
1708
  }
1434
1709
  trackWechatForwardTask(task) {
@@ -1728,7 +2003,9 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
1728
2003
  return { action: "stopped", lock, forced };
1729
2004
  }
1730
2005
  export async function runDaemon(options) {
1731
- migrateLegacyChannelFiles((message) => log(message));
2006
+ if ((options.channelId ?? "wechat") === "wechat") {
2007
+ migrateLegacyChannelFiles((message) => log(message));
2008
+ }
1732
2009
  loadEmojiBindings();
1733
2010
  await cleanupDaemonBeforeStart({ cwd: options.cwd });
1734
2011
  const cleanupResult = await cleanupSingleBridgeBeforeDaemon();
@@ -1744,19 +2021,36 @@ export async function runDaemon(options) {
1744
2021
  if (reapedOpencodePids.length > 0) {
1745
2022
  log(`Cleaned ${reapedOpencodePids.length} orphaned OpenCode process(es): ${reapedOpencodePids.join(", ")}`);
1746
2023
  }
1747
- const credentials = await ensureWechatCredentials({
1748
- requireUserId: true,
1749
- validateExisting: true,
1750
- log,
1751
- });
2024
+ const wecomAccount = options.channelId === "wecom"
2025
+ ? await ensureWecomAccount({ log })
2026
+ : null;
2027
+ const credentials = wecomAccount
2028
+ ? { userId: wecomAccount.operatorUserId }
2029
+ : await ensureWechatCredentials({
2030
+ requireUserId: true,
2031
+ validateExisting: true,
2032
+ log,
2033
+ });
1752
2034
  if (!credentials.userId) {
1753
- throw new Error("Saved WeChat credentials are missing userId.");
2035
+ throw new Error(options.channelId === "wecom"
2036
+ ? "Saved WeCom credentials are missing operatorUserId."
2037
+ : "Saved WeChat credentials are missing userId.");
1754
2038
  }
1755
2039
  const daemon = new WechatDaemon({
1756
2040
  cwd: options.cwd,
1757
2041
  profile: options.profile,
1758
2042
  authorizedUserId: credentials.userId,
1759
2043
  transport: new WeChatTransport({ log, logError }),
2044
+ channelId: options.channelId,
2045
+ wecomTransport: wecomAccount
2046
+ ? new WecomTransport({
2047
+ account: wecomAccount,
2048
+ logger: {
2049
+ log: (message) => log(message),
2050
+ error: (message) => logError(message),
2051
+ },
2052
+ })
2053
+ : null,
1760
2054
  });
1761
2055
  if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
1762
2056
  daemon.takenOverAdapter = cleanupResult.lock.adapter;
@@ -1806,7 +2100,7 @@ export async function main(argv = process.argv.slice(2)) {
1806
2100
  process.exit(1);
1807
2101
  }
1808
2102
  }
1809
- const isDirectRun = Boolean(import.meta.main);
2103
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
1810
2104
  if (isDirectRun) {
1811
2105
  void main();
1812
2106
  }