@unlinearity/cli-wechat-bridge 1.1.6 → 1.2.1

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 (88) hide show
  1. package/LICENSE.txt +661 -661
  2. package/README.md +468 -475
  3. package/bin/_run-entry.mjs +29 -6
  4. package/bin/wechat-check-update.mjs +6 -1
  5. package/bin/wechat-claude.mjs +6 -1
  6. package/bin/wechat-codex.mjs +6 -1
  7. package/bin/wechat-daemon.mjs +6 -1
  8. package/bin/wechat-opencode.mjs +6 -1
  9. package/bin/wechat-pi.mjs +6 -1
  10. package/bin/wechat-setup.mjs +6 -1
  11. package/bin/wecom-check-update.mjs +10 -0
  12. package/bin/wecom-claude.mjs +10 -0
  13. package/bin/wecom-codex.mjs +10 -0
  14. package/bin/wecom-daemon.mjs +10 -0
  15. package/bin/wecom-opencode.mjs +10 -0
  16. package/bin/wecom-pi.mjs +10 -0
  17. package/bin/wecom-setup.mjs +10 -0
  18. package/dist/bridge/adapter-control.js +58 -0
  19. package/dist/bridge/bridge-adapters.claude.js +161 -4
  20. package/dist/bridge/bridge-adapters.codex.js +206 -14
  21. package/dist/bridge/bridge-adapters.core.js +25 -1
  22. package/dist/bridge/bridge-adapters.opencode.js +298 -20
  23. package/dist/bridge/bridge-adapters.pi.js +45 -0
  24. package/dist/bridge/bridge-adapters.shared.js +6 -0
  25. package/dist/bridge/bridge-controller.js +6 -0
  26. package/dist/bridge/bridge-final-reply.js +6 -0
  27. package/dist/bridge/bridge-process-reaper.js +41 -2
  28. package/dist/bridge/bridge-state.js +16 -3
  29. package/dist/bridge/bridge-utils.js +6 -31
  30. package/dist/bridge/claude-hook.js +6 -0
  31. package/dist/bridge/claude-native-controls.js +145 -0
  32. package/dist/bridge/codex-visible-thread-proxy.js +140 -0
  33. package/dist/bridge/native-terminal-control.js +271 -0
  34. package/dist/bridge/opencode-v2-compat.js +450 -0
  35. package/dist/bridge/wechat-bridge.js +380 -171
  36. package/dist/bridge/wechat-forwarding.js +6 -0
  37. package/dist/bridge/wechat-outbound-queue.js +6 -0
  38. package/dist/channels/wechat/bridge-final-reply.js +13 -22
  39. package/dist/channels/wechat/index.js +6 -0
  40. package/dist/channels/wechat/wechat-driver.js +191 -0
  41. package/dist/channels/wechat/wechat-forwarding.js +18 -0
  42. package/dist/channels/wechat/wechat-outbound-queue.js +49 -3
  43. package/dist/channels/wecom/bridge-final-reply.js +32 -0
  44. package/dist/channels/wecom/setup-cli.js +12 -0
  45. package/dist/channels/wecom/setup.js +182 -0
  46. package/dist/channels/wecom/wecom-channel-port.js +45 -0
  47. package/dist/channels/wecom/wecom-config.js +72 -0
  48. package/dist/channels/wecom/wecom-driver.js +106 -0
  49. package/dist/channels/wecom/wecom-media.js +147 -0
  50. package/dist/channels/wecom/wecom-message.js +149 -0
  51. package/dist/channels/wecom/wecom-transport.js +314 -0
  52. package/dist/commands/check-update.js +6 -0
  53. package/dist/companion/codex-remote-client.js +30 -3
  54. package/dist/companion/codex-visible-client-link.js +107 -0
  55. package/dist/companion/local-companion-link.js +6 -0
  56. package/dist/companion/local-companion-start.js +62 -11
  57. package/dist/companion/local-companion.js +25 -1
  58. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  59. package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
  60. package/dist/companion/pi-tui-bridge-extension.js +62 -3
  61. package/dist/core/bridge-control.js +10 -0
  62. package/dist/core/bridge-defer.js +25 -0
  63. package/dist/core/channel-driver.js +1 -0
  64. package/dist/core/channel-types.js +3 -1
  65. package/dist/core/conversation-routing.js +58 -0
  66. package/dist/core/direct-run.js +23 -0
  67. package/dist/core/inbound-message-claims.js +67 -0
  68. package/dist/core/outbound-target.js +32 -0
  69. package/dist/core/text-utils.js +6 -0
  70. package/dist/core/turn-coordinator.js +85 -0
  71. package/dist/daemon/daemon-link.js +9 -0
  72. package/dist/daemon/emoji-bindings.js +6 -0
  73. package/dist/daemon/wechat-daemon.js +675 -122
  74. package/dist/i18n/index.js +6 -0
  75. package/dist/i18n/messages-en.js +9 -1
  76. package/dist/i18n/messages-zh.js +9 -1
  77. package/dist/runtime/create-runtime-host.js +6 -0
  78. package/dist/runtime/legacy-adapter-runtime.js +15 -0
  79. package/dist/runtime/runtime-types.js +3 -0
  80. package/dist/utils/atomic-file.js +6 -0
  81. package/dist/utils/doctor.js +36 -10
  82. package/dist/utils/version-checker.js +17 -11
  83. package/dist/wechat/channel-config.js +6 -0
  84. package/dist/wechat/setup.js +119 -13
  85. package/dist/wechat/wechat-channel.js +6 -0
  86. package/dist/wechat/wechat-transport.js +169 -58
  87. package/package.json +16 -4
  88. package/scripts/ensure-node-pty-permissions.mjs +73 -73
@@ -1,22 +1,38 @@
1
1
  #!/usr/bin/env bun
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+ // Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
4
+ // Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
5
+ // This file is part of CLI-WeChat-Bridge. Modifications and derivative works
6
+ // must be released under AGPL-3.0-or-later with full source code; see
7
+ // LICENSE.txt. Network services built on it must offer source to users.
2
8
  import path from "node:path";
3
9
  import { resolveDefaultAdapterCommand, } from "./bridge-adapters.js";
4
10
  import { delay } from "./bridge-adapters.shared.js";
5
11
  import { t } from "../i18n/index.js";
6
12
  import { BridgeController } from "./bridge-controller.js";
7
13
  import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "./bridge-session-resume.js";
8
- import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, formatWechatSendRetryLogEntry, isWechatContextUnavailableError, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
14
+ import { formatUserFacingBridgeFatalError, formatUserFacingInboundError, shouldForwardBridgeEventToWechat, shouldSuppressCodexLocalThreadNotice, } from "../channels/wechat/wechat-forwarding.js";
9
15
  import { ensureWechatCredentials } from "../wechat/setup.js";
10
16
  import { BridgeStateStore } from "./bridge-state.js";
11
17
  import { getProcessRecordByPid, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "./bridge-process-reaper.js";
12
18
  import { createRuntimeHost } from "../runtime/create-runtime-host.js";
13
19
  import { toChannelInboundMessage } from "../channels/wechat/channel-message.js";
14
20
  import { routeBridgeMessage } from "../core/bridge-message-router.js";
21
+ import { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
22
+ import { TurnCoordinator } from "../core/turn-coordinator.js";
23
+ import { resolveOutboundConversationTarget } from "../core/outbound-target.js";
24
+ import { handleAdapterControl } from "./adapter-control.js";
15
25
  import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
26
+ import { isDirectModuleRun } from "../core/direct-run.js";
16
27
  import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
28
+ import { ensureWecomAccount } from "../channels/wecom/setup.js";
29
+ import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
30
+ import { WecomTransport } from "../channels/wecom/wecom-transport.js";
31
+ import { WecomChannelDriver } from "../channels/wecom/wecom-driver.js";
32
+ import { WechatChannelDriver } from "../channels/wechat/wechat-driver.js";
17
33
  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";
18
- import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
19
- import { getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
34
+ import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, } from "../wechat/wechat-transport.js";
35
+ import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
20
36
  import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
21
37
  import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
22
38
  const POLL_RETRY_BASE_MS = 1_000;
@@ -73,31 +89,7 @@ function toPendingUserInput(request) {
73
89
  createdAt: nowIso(),
74
90
  };
75
91
  }
76
- export function shouldDeferCodexInboundMessage(params) {
77
- return (params.adapter === "codex" &&
78
- !params.hasPendingConfirmation &&
79
- !params.hasSystemCommand &&
80
- params.activeTurnOrigin === "local" &&
81
- (params.status === "busy" || params.status === "awaiting_approval"));
82
- }
83
- export function canDrainDeferredCodexInboundQueue(params) {
84
- return (params.adapter === "codex" &&
85
- params.deferredCount > 0 &&
86
- !params.hasPendingConfirmation &&
87
- !params.hasPendingUserInput &&
88
- !params.hasPendingApproval &&
89
- !params.hasActiveTask &&
90
- !params.activeTurnId &&
91
- params.status !== "busy" &&
92
- params.status !== "awaiting_approval" &&
93
- params.status !== "awaiting_input");
94
- }
95
- export function formatDeferredCodexInboundQueueMessage(queuePosition) {
96
- return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
97
- }
98
- export function isRetryableDeferredCodexDrainError(errorText) {
99
- return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
100
- }
92
+ export { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
101
93
  export function parseCliArgs(argv) {
102
94
  let adapter = null;
103
95
  let commandOverride;
@@ -105,6 +97,7 @@ export function parseCliArgs(argv) {
105
97
  let profile;
106
98
  let lifecycle = "persistent";
107
99
  let sessionStartMode = "restore";
100
+ let channelId = "wechat";
108
101
  for (let i = 0; i < argv.length; i += 1) {
109
102
  const arg = argv[i];
110
103
  const next = argv[i + 1];
@@ -151,6 +144,13 @@ export function parseCliArgs(argv) {
151
144
  sessionStartMode = next;
152
145
  i += 1;
153
146
  break;
147
+ case "--channel":
148
+ if (!next || (next !== "wechat" && next !== "wecom")) {
149
+ throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
150
+ }
151
+ channelId = next;
152
+ i += 1;
153
+ break;
154
154
  case "--shutdown-on-parent-exit":
155
155
  lifecycle = "companion_bound";
156
156
  break;
@@ -173,6 +173,33 @@ export function parseCliArgs(argv) {
173
173
  profile,
174
174
  lifecycle,
175
175
  sessionStartMode,
176
+ channelId,
177
+ };
178
+ }
179
+ function toWechatSendContext(kind) {
180
+ if (kind === "mirrored_input") {
181
+ return "mirrored_user_input";
182
+ }
183
+ if (kind === "status") {
184
+ return "message";
185
+ }
186
+ return kind;
187
+ }
188
+ function toLegacyInboundWechatMessage(message) {
189
+ return {
190
+ senderId: message.senderId,
191
+ sender: message.senderId,
192
+ sessionId: message.conversation.conversationId,
193
+ text: message.text,
194
+ attachments: message.attachments.map((attachment) => ({
195
+ kind: attachment.kind === "image" ? "image" : "file",
196
+ path: attachment.path,
197
+ fileName: attachment.fileName || path.basename(attachment.path),
198
+ sizeBytes: attachment.sizeBytes ?? 0,
199
+ })),
200
+ contextToken: message.conversation.opaqueRef,
201
+ createdAt: message.createdAt,
202
+ createdAtMs: Date.parse(message.createdAt),
176
203
  };
177
204
  }
178
205
  function printUsageAndExit() {
@@ -180,7 +207,7 @@ function printUsageAndExit() {
180
207
  "Internal bridge runtime usage:",
181
208
  " 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>]",
182
209
  "",
183
- "This entry is internal. Users should run wechat-codex, wechat-claude, wechat-opencode, wechat-pi, or wechat-daemon.",
210
+ "This entry is internal. Users should run a wechat-* or wecom-* launcher.",
184
211
  "",
185
212
  ].join("\n"));
186
213
  process.exit(0);
@@ -194,21 +221,49 @@ async function main() {
194
221
  const options = parseCliArgs(process.argv.slice(2));
195
222
  const daemonEndpoint = readDaemonEndpoint();
196
223
  if (daemonEndpoint && await isDaemonEndpointAlive(daemonEndpoint, { timeoutMs: 500 })) {
197
- throw new Error(`wechat-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
224
+ throw new Error(`${daemonEndpoint.channelId ?? "wechat"}-daemon is already running (pid=${daemonEndpoint.pid}, cwd=${daemonEndpoint.cwd}). Stop it before starting a standalone bridge.`);
198
225
  }
199
226
  if (daemonEndpoint) {
200
227
  clearDaemonEndpoint(daemonEndpoint.pid);
201
228
  log(`Cleared stale wechat-daemon endpoint for pid=${daemonEndpoint.pid}.`);
202
229
  }
203
- const credentials = await ensureWechatCredentials({
204
- requireUserId: true,
205
- validateExisting: true,
206
- log,
207
- });
230
+ const wecomAccount = options.channelId === "wecom"
231
+ ? await ensureWecomAccount({ log })
232
+ : null;
233
+ const credentials = wecomAccount
234
+ ? { userId: wecomAccount.operatorUserId }
235
+ : await ensureWechatCredentials({
236
+ requireUserId: true,
237
+ validateExisting: true,
238
+ log,
239
+ });
208
240
  if (!credentials.userId) {
209
- throw new Error("Saved WeChat credentials are missing userId.");
241
+ throw new Error(options.channelId === "wecom"
242
+ ? "Saved WeCom credentials are missing operatorUserId."
243
+ : "Saved WeChat credentials are missing userId.");
210
244
  }
211
245
  const transport = new WeChatTransport({ log, logError });
246
+ const wecomTransport = wecomAccount
247
+ ? new WecomTransport({
248
+ account: wecomAccount,
249
+ logger: {
250
+ log: (message) => log(message),
251
+ error: (message) => logError(message),
252
+ },
253
+ })
254
+ : null;
255
+ const channelDriver = wecomTransport
256
+ ? new WecomChannelDriver({
257
+ transport: wecomTransport,
258
+ accountId: wecomAccount?.botId,
259
+ operatorId: credentials.userId,
260
+ logError,
261
+ })
262
+ : new WechatChannelDriver({
263
+ transport,
264
+ logError,
265
+ buildInboundPrompt: (text, attachments) => buildWechatInboundPrompt(text, attachments.filter((attachment) => attachment.kind === "image" || attachment.kind === "file")),
266
+ });
212
267
  // 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
213
268
  // unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
214
269
  // 否则会与强制退出 teardown 竞态。
@@ -227,6 +282,7 @@ async function main() {
227
282
  const stateStore = new BridgeStateStore({
228
283
  ...options,
229
284
  authorizedUserId: credentials.userId,
285
+ channelId: options.channelId,
230
286
  });
231
287
  const reapedPeerPids = await reapPeerBridgeProcesses({
232
288
  logger: (message) => stateStore.appendLog(message),
@@ -273,7 +329,9 @@ async function main() {
273
329
  initialResumeConversationId: stateStore.getState().resumeConversationId,
274
330
  initialTranscriptPath: stateStore.getState().transcriptPath,
275
331
  });
276
- const pendingWechatMessages = new PendingWechatMessageStore(getPendingWechatMessagesFile(options.cwd));
332
+ const pendingWechatMessages = new PendingWechatMessageStore(options.channelId === "wechat"
333
+ ? getPendingWechatMessagesFile(options.cwd)
334
+ : getPendingChannelMessagesFile(options.cwd, options.channelId));
277
335
  const resumeCoordinator = new ResumeSessionCoordinator({
278
336
  adapter: options.adapter,
279
337
  runtime: adapter,
@@ -284,7 +342,13 @@ async function main() {
284
342
  let textSendChain = Promise.resolve();
285
343
  let attachmentSendChain = Promise.resolve();
286
344
  const pendingWechatForwardTasks = new Set();
287
- let activeTask = null;
345
+ const defaultConversation = channelDriver.defaultConversation();
346
+ const operatorId = credentials.userId;
347
+ const operatorConversation = defaultConversation
348
+ ?? channelDriver.directConversation(operatorId);
349
+ const turns = new TurnCoordinator({
350
+ initialLastConversation: defaultConversation,
351
+ });
288
352
  const deferredInboundMessages = [];
289
353
  let drainingDeferredInboundMessages = false;
290
354
  let consecutivePollFailures = 0;
@@ -299,67 +363,30 @@ async function main() {
299
363
  attachmentSendChain = run.then(() => undefined, () => undefined);
300
364
  return run;
301
365
  };
302
- const sendWechatMessageNow = async (senderId, text, context = "message") => {
303
- const startedAtMs = Date.now();
304
- stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
305
- for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
306
- try {
307
- await transport.sendText(senderId, text);
308
- stateStore.appendLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
309
- return { status: "sent" };
310
- }
311
- catch (err) {
312
- if (err instanceof Error && err.name === "AbortError") {
313
- stateStore.appendLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
314
- }
315
- if (isWechatContextUnavailableError(err)) {
316
- if (isWechatContextTokenStaleError(err)) {
317
- transport.clearCachedContextToken(senderId);
318
- }
319
- 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.";
320
- logError(`Failed to send WeChat ${context}: ${hint}`);
321
- stateStore.appendLog(isWechatContextTokenStaleError(err)
322
- ? formatWechatContextTokenStaleLogEntry({
323
- context,
324
- recipientId: senderId,
325
- error: err,
326
- })
327
- : formatWechatSendFailureLogEntry({
328
- context,
329
- recipientId: senderId,
330
- error: err,
331
- }));
332
- return { status: "context_unavailable", error: err };
333
- }
334
- if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
335
- const delayMs = computeWechatSendRetryDelayMs(attempt);
336
- logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
337
- stateStore.appendLog(formatWechatSendRetryLogEntry({
338
- context,
339
- recipientId: senderId,
340
- attempt,
341
- delayMs,
342
- error: err,
343
- }));
344
- await delay(delayMs);
345
- continue;
346
- }
347
- logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
348
- stateStore.appendLog(formatWechatSendFailureLogEntry({
349
- context,
350
- recipientId: senderId,
351
- error: err,
352
- }));
353
- return { status: "failed" };
354
- }
355
- }
356
- return { status: "failed" };
366
+ const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
367
+ const target = resolveOutboundConversationTarget({
368
+ senderId,
369
+ operatorId,
370
+ override: targetOverride,
371
+ multiConversation: channelDriver.capabilities.multiConversation,
372
+ operatorTarget: turns.resolveTarget(operatorConversation),
373
+ directConversation: (id) => channelDriver.directConversation(id),
374
+ });
375
+ return channelDriver.sendText({
376
+ target,
377
+ text,
378
+ context,
379
+ log: (entry) => stateStore.appendLog(entry),
380
+ });
357
381
  };
358
- const queueWechatMessage = (senderId, text, context = "message") => {
382
+ const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
383
+ const queuedTarget = senderId === operatorId && channelDriver.capabilities.multiConversation
384
+ ? targetOverride ?? turns.resolveTarget(operatorConversation)
385
+ : targetOverride;
359
386
  return queueWechatTextAction(async () => {
360
- const result = await sendWechatMessageNow(senderId, text, context);
361
- if (result.status === "context_unavailable") {
362
- const pending = pendingWechatMessages.enqueue(senderId, text, context);
387
+ const result = await sendWechatMessageNow(senderId, text, context, queuedTarget);
388
+ if (result.status === "target_stale") {
389
+ const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
363
390
  if (pending) {
364
391
  stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
365
392
  }
@@ -370,13 +397,13 @@ async function main() {
370
397
  const flushPendingWechatMessages = () => {
371
398
  return queueWechatTextAction(async () => {
372
399
  for (const pending of pendingWechatMessages.list()) {
373
- const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context);
400
+ const result = await sendWechatMessageNow(pending.recipientId, pending.text, pending.context, pending.target);
374
401
  if (result.status === "sent") {
375
402
  pendingWechatMessages.remove(pending.id);
376
403
  stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
377
404
  continue;
378
405
  }
379
- if (result.status === "context_unavailable") {
406
+ if (result.status === "target_stale") {
380
407
  break;
381
408
  }
382
409
  stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
@@ -401,7 +428,9 @@ async function main() {
401
428
  }
402
429
  };
403
430
  const outputBatcher = new OutputBatcher(async (text) => {
404
- await queueWechatMessage(stateStore.getState().authorizedUserId, text);
431
+ await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", channelDriver.capabilities.multiConversation
432
+ ? turns.resolveTarget(operatorConversation)
433
+ : undefined);
405
434
  });
406
435
  const maybeDrainDeferredInboundMessages = async () => {
407
436
  if (drainingDeferredInboundMessages || !ensureRuntimeOwnership()) {
@@ -416,7 +445,7 @@ async function main() {
416
445
  hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
417
446
  hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
418
447
  hasPendingApproval: Boolean(adapterState.pendingApproval),
419
- hasActiveTask: Boolean(activeTask),
448
+ hasActiveTask: turns.hasActiveTask,
420
449
  })) {
421
450
  return;
422
451
  }
@@ -427,13 +456,26 @@ async function main() {
427
456
  drainingDeferredInboundMessages = true;
428
457
  try {
429
458
  stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
430
- const nextTask = await dispatchInboundWechatText({
431
- message: nextDeferred.message,
432
- options,
433
- stateStore,
434
- adapter,
435
- });
436
- activeTask = nextTask;
459
+ const nextTask = createActiveTask(nextDeferred.message);
460
+ const lease = turns.beginTurn(nextTask, nextDeferred.channelMessage?.conversation);
461
+ if (!lease) {
462
+ deferredInboundMessages.unshift(nextDeferred);
463
+ return;
464
+ }
465
+ try {
466
+ await dispatchInboundWechatText({
467
+ message: nextDeferred.message,
468
+ channelDriver,
469
+ options,
470
+ stateStore,
471
+ adapter,
472
+ activeTask: nextTask,
473
+ });
474
+ }
475
+ catch (error) {
476
+ turns.rollback(lease);
477
+ throw error;
478
+ }
437
479
  }
438
480
  catch (err) {
439
481
  const errorText = err instanceof Error ? err.message : String(err);
@@ -448,7 +490,7 @@ async function main() {
448
490
  adapter: options.adapter,
449
491
  cwd: options.cwd,
450
492
  errorText,
451
- }), "inbound_error");
493
+ }), "inbound_error", nextDeferred.channelMessage?.conversation);
452
494
  }
453
495
  finally {
454
496
  drainingDeferredInboundMessages = false;
@@ -515,6 +557,18 @@ async function main() {
515
557
  catch {
516
558
  // Best effort shutdown.
517
559
  }
560
+ try {
561
+ wecomTransport?.stop();
562
+ }
563
+ catch {
564
+ // Best effort shutdown.
565
+ }
566
+ try {
567
+ await channelDriver.endAllTyping?.();
568
+ }
569
+ catch {
570
+ // Best effort shutdown.
571
+ }
518
572
  controller.clearLocalClientEndpoint();
519
573
  stateStore.releaseLock();
520
574
  };
@@ -583,6 +637,7 @@ async function main() {
583
637
  try {
584
638
  wireAdapterEvents({
585
639
  adapter,
640
+ channelDriver,
586
641
  resumeCoordinator,
587
642
  options,
588
643
  transport,
@@ -592,8 +647,11 @@ async function main() {
592
647
  queueWechatMessage,
593
648
  trackWechatForwardTask,
594
649
  maybeDrainDeferredInboundMessages,
595
- clearActiveTask: () => {
596
- activeTask = null;
650
+ getActiveTask: () => turns.activeTask,
651
+ clearActiveTask: (expectedTask) => {
652
+ if (expectedTask) {
653
+ turns.complete(expectedTask);
654
+ }
597
655
  },
598
656
  syncSharedSessionState: () => {
599
657
  syncSharedSessionState(stateStore, adapter);
@@ -602,6 +660,8 @@ async function main() {
602
660
  controller.syncLocalClientEndpoint();
603
661
  },
604
662
  requestShutdown,
663
+ wecomTransport,
664
+ getWecomTarget: () => turns.resolveTarget(operatorConversation),
605
665
  });
606
666
  await adapter.start();
607
667
  if (!ensureRuntimeOwnership()) {
@@ -609,13 +669,116 @@ async function main() {
609
669
  }
610
670
  syncSharedSessionState(stateStore, adapter);
611
671
  controller.syncLocalClientEndpoint();
612
- stateStore.appendLog(`Bridge started with adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
613
- log(`WeChat bridge is ready for adapter "${options.adapter}".`);
672
+ stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
673
+ if (channelDriver.start) {
674
+ await channelDriver.start({
675
+ onInboundMessage: async (channelMessage) => {
676
+ if (shutdownPromise || !ensureRuntimeOwnership()) {
677
+ return;
678
+ }
679
+ turns.observeConversation(channelMessage.conversation);
680
+ const queueInboundReply = (senderId, text, context) => queueWechatMessage(senderId, text, context, channelMessage.conversation);
681
+ if (pendingWechatMessages.list().length > 0) {
682
+ await flushPendingWechatMessages();
683
+ }
684
+ const message = toLegacyInboundWechatMessage(channelMessage);
685
+ const taskAtMessageStart = turns.activeTask;
686
+ stateStore.touchActivity(message.createdAt);
687
+ try {
688
+ await handleInboundMessage({
689
+ message,
690
+ channelMessage,
691
+ channelDriver,
692
+ options,
693
+ stateStore,
694
+ adapter,
695
+ resumeCoordinator,
696
+ queueWechatMessage: queueInboundReply,
697
+ outputBatcher,
698
+ clearActiveTask: () => {
699
+ if (taskAtMessageStart) {
700
+ turns.complete(taskAtMessageStart);
701
+ }
702
+ },
703
+ dispatchInboundText: async () => {
704
+ const nextActiveTask = createActiveTask(message);
705
+ const result = await turns.dispatch({
706
+ task: nextActiveTask,
707
+ conversation: channelMessage.conversation,
708
+ onBusy: async () => {
709
+ await queueInboundReply(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
710
+ },
711
+ forward: async () => {
712
+ await dispatchInboundWechatText({
713
+ message,
714
+ channelDriver,
715
+ options,
716
+ stateStore,
717
+ adapter,
718
+ activeTask: nextActiveTask,
719
+ });
720
+ },
721
+ });
722
+ if (result.status === "dispatched") {
723
+ void channelDriver.beginTyping?.(message.senderId);
724
+ }
725
+ return result.status === "dispatched" ? nextActiveTask : null;
726
+ },
727
+ deferInboundMessage: async (nextMessage) => {
728
+ deferredInboundMessages.push({
729
+ message: nextMessage,
730
+ channelMessage,
731
+ });
732
+ stateStore.appendLog(`deferred_inbound_input: position=${deferredInboundMessages.length} text=${truncatePreview(nextMessage.text)}`);
733
+ await queueInboundReply(nextMessage.senderId, formatDeferredCodexInboundQueueMessage(deferredInboundMessages.length));
734
+ },
735
+ });
736
+ }
737
+ catch (error) {
738
+ const errorText = error instanceof Error ? error.message : String(error);
739
+ logError(errorText);
740
+ stateStore.appendLog(`inbound_error: ${errorText}`);
741
+ await queueInboundReply(message.senderId, formatUserFacingInboundError({
742
+ adapter: options.adapter,
743
+ cwd: options.cwd,
744
+ errorText,
745
+ }), "inbound_error");
746
+ }
747
+ syncSharedSessionState(stateStore, adapter);
748
+ await maybeDrainDeferredInboundMessages();
749
+ },
750
+ onUnauthorizedSender: async (senderId, chatType) => {
751
+ stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
752
+ if (chatType === "direct") {
753
+ const result = await channelDriver.sendText({
754
+ target: channelDriver.directConversation(senderId),
755
+ text: "Unauthorized.",
756
+ context: "notice",
757
+ log: (entry) => stateStore.appendLog(entry),
758
+ });
759
+ if (result.status !== "sent") {
760
+ logError(`Failed to reply unauthorized notice to ${senderId}`);
761
+ }
762
+ }
763
+ },
764
+ onChannelFatal: async (error) => {
765
+ stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
766
+ requestShutdown(error.message, 1);
767
+ },
768
+ onChannelConnected: async () => {
769
+ if (pendingWechatMessages.list().length > 0) {
770
+ await flushPendingWechatMessages();
771
+ }
772
+ },
773
+ });
774
+ await channelDriver.waitUntilReady?.();
775
+ }
776
+ log(`${channelDriver.displayName} bridge is ready for adapter "${options.adapter}".`);
614
777
  log(`Working directory: ${options.cwd}`);
615
778
  if (options.profile) {
616
779
  log(`Profile: ${options.profile}`);
617
780
  }
618
- log(`Authorized WeChat user: ${credentials.userId}`);
781
+ log(`Authorized ${channelDriver.displayName} user: ${credentials.userId}`);
619
782
  if (options.adapter === "codex") {
620
783
  log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
621
784
  }
@@ -633,6 +796,12 @@ async function main() {
633
796
  cwd: options.cwd,
634
797
  });
635
798
  await queueWechatMessage(credentials.userId, welcomeText);
799
+ if (channelDriver.capabilities.pushInbound) {
800
+ while (!shutdownPromise && ensureRuntimeOwnership()) {
801
+ await delay(1_000);
802
+ }
803
+ return;
804
+ }
636
805
  while (true) {
637
806
  if (shutdownPromise) {
638
807
  // Shutdown (e.g. SIGINT) was requested while we were awaiting the
@@ -693,11 +862,12 @@ async function main() {
693
862
  if (!ensureRuntimeOwnership()) {
694
863
  break;
695
864
  }
865
+ const taskAtMessageStart = turns.activeTask;
696
866
  stateStore.touchActivity(message.createdAt);
697
- let nextTask = null;
698
867
  try {
699
- nextTask = await handleInboundMessage({
868
+ await handleInboundMessage({
700
869
  message,
870
+ channelDriver,
701
871
  options,
702
872
  stateStore,
703
873
  adapter,
@@ -705,7 +875,32 @@ async function main() {
705
875
  queueWechatMessage,
706
876
  outputBatcher,
707
877
  clearActiveTask: () => {
708
- activeTask = null;
878
+ if (taskAtMessageStart) {
879
+ turns.complete(taskAtMessageStart);
880
+ }
881
+ },
882
+ dispatchInboundText: async () => {
883
+ const nextActiveTask = createActiveTask(message);
884
+ const result = await turns.dispatch({
885
+ task: nextActiveTask,
886
+ onBusy: async () => {
887
+ await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
888
+ },
889
+ forward: async () => {
890
+ await dispatchInboundWechatText({
891
+ message,
892
+ channelDriver,
893
+ options,
894
+ stateStore,
895
+ adapter,
896
+ activeTask: nextActiveTask,
897
+ });
898
+ },
899
+ });
900
+ if (result.status === "dispatched") {
901
+ void channelDriver.beginTyping?.(message.senderId);
902
+ }
903
+ return result.status === "dispatched" ? nextActiveTask : null;
709
904
  },
710
905
  deferInboundMessage: async (nextMessage) => {
711
906
  deferredInboundMessages.push({
@@ -726,9 +921,6 @@ async function main() {
726
921
  errorText,
727
922
  }), "inbound_error");
728
923
  }
729
- if (nextTask) {
730
- activeTask = nextTask;
731
- }
732
924
  syncSharedSessionState(stateStore, adapter);
733
925
  await maybeDrainDeferredInboundMessages();
734
926
  }
@@ -763,20 +955,28 @@ function syncSharedSessionState(stateStore, adapter) {
763
955
  }
764
956
  }
765
957
  function wireAdapterEvents(params) {
766
- const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, } = params;
767
- const channelPort = new WechatChannelPort({
768
- sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
769
- sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
770
- sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
771
- sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
772
- sendVideo: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
773
- onEmptyVisibleReply: (adapter, rawText) => {
774
- stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
775
- },
776
- onTextSent: (_adapter, text) => {
777
- stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
778
- },
779
- });
958
+ const { adapter, channelDriver, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
959
+ const channelPort = options.channelId === "wecom"
960
+ ? new WecomChannelPort({
961
+ transport: wecomTransport,
962
+ sendText: (target, text, kind) => queueWechatMessage(stateStore.getState().authorizedUserId, text, toWechatSendContext(kind), target),
963
+ onEmptyVisibleReply: (adapter, rawText) => {
964
+ stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
965
+ },
966
+ })
967
+ : new WechatChannelPort({
968
+ sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
969
+ sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
970
+ sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
971
+ sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
972
+ sendVideo: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVideo(filePath, { recipientId })),
973
+ onEmptyVisibleReply: (adapter, rawText) => {
974
+ stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
975
+ },
976
+ onTextSent: (_adapter, text) => {
977
+ stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
978
+ },
979
+ });
780
980
  let lastFinalReplyAtMs = 0;
781
981
  let eventForwardChain = Promise.resolve();
782
982
  adapter.setEventSink((event) => {
@@ -791,6 +991,10 @@ function wireAdapterEvents(params) {
791
991
  stateStore.clearPendingUserInput();
792
992
  }
793
993
  const authorizedUserId = stateStore.getState().authorizedUserId;
994
+ const eventTarget = channelDriver.capabilities.multiConversation
995
+ ? getWecomTarget()
996
+ : channelDriver.directConversation(authorizedUserId);
997
+ const eventTask = getActiveTask();
794
998
  eventForwardChain = eventForwardChain
795
999
  .then(() => forwardBridgeEvent(event, {
796
1000
  stdout: (next) => {
@@ -804,15 +1008,12 @@ function wireAdapterEvents(params) {
804
1008
  }
805
1009
  },
806
1010
  finalReply: (next) => {
1011
+ void channelDriver.endTyping?.(authorizedUserId);
807
1012
  lastFinalReplyAtMs = Date.now();
808
1013
  stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
809
1014
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
810
1015
  await channelPort.send({
811
- target: {
812
- channelId: "wechat",
813
- conversationId: authorizedUserId,
814
- recipientId: authorizedUserId,
815
- },
1016
+ target: eventTarget,
816
1017
  kind: "final_reply",
817
1018
  text: next.text,
818
1019
  adapter: options.adapter,
@@ -830,7 +1031,7 @@ function wireAdapterEvents(params) {
830
1031
  stateStore.appendLog(`${next.level}_notice: ${truncatePreview(next.text)}`);
831
1032
  if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
832
1033
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
833
- await queueWechatMessage(authorizedUserId, next.text, "notice");
1034
+ await queueWechatMessage(authorizedUserId, next.text, "notice", eventTarget);
834
1035
  }));
835
1036
  }
836
1037
  },
@@ -840,7 +1041,9 @@ function wireAdapterEvents(params) {
840
1041
  if (thinkingPreview) {
841
1042
  stateStore.appendLog(`thinking: ${thinkingPreview}`);
842
1043
  trackWechatForwardTask((async () => {
843
- await queueWechatMessage(authorizedUserId, `思考: ${thinkingPreview}`, "thinking");
1044
+ await queueWechatMessage(authorizedUserId, channelDriver.id === "wecom"
1045
+ ? `Processing: ${thinkingPreview}`
1046
+ : `思考: ${thinkingPreview}`, "thinking", eventTarget);
844
1047
  })());
845
1048
  }
846
1049
  }
@@ -850,7 +1053,7 @@ function wireAdapterEvents(params) {
850
1053
  const pending = toPendingApproval(next.request);
851
1054
  stateStore.setPendingConfirmation(pending);
852
1055
  stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
853
- await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required");
1056
+ await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required", eventTarget);
854
1057
  }));
855
1058
  },
856
1059
  userInputRequired: (next) => {
@@ -858,14 +1061,14 @@ function wireAdapterEvents(params) {
858
1061
  const pending = toPendingUserInput(next.request);
859
1062
  stateStore.setPendingUserInput(pending);
860
1063
  stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
861
- await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required");
1064
+ await queueWechatMessage(authorizedUserId, formatUserInputRequestMessage(pending, adapterState), "user_input_required", eventTarget);
862
1065
  }));
863
1066
  },
864
1067
  mirroredUserInput: (next) => {
865
1068
  stateStore.appendLog(`mirrored_local_input: ${truncatePreview(next.text)}`);
866
1069
  if (shouldForwardBridgeEventToWechat(options.adapter, next.type, { text: next.text })) {
867
1070
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
868
- await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input");
1071
+ await queueWechatMessage(authorizedUserId, formatMirroredUserInputMessage(options.adapter, next.text), "mirrored_user_input", eventTarget);
869
1072
  }));
870
1073
  }
871
1074
  },
@@ -875,7 +1078,7 @@ function wireAdapterEvents(params) {
875
1078
  stateStore.appendLog(`session_switched: ${next.sessionId} source=${next.source} reason=${next.reason}`);
876
1079
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
877
1080
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
878
- await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched");
1081
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.sessionId, source: next.source, reason: next.reason }), "session_switched", eventTarget);
879
1082
  }));
880
1083
  }
881
1084
  },
@@ -893,36 +1096,39 @@ function wireAdapterEvents(params) {
893
1096
  }
894
1097
  if (shouldForwardSessionSwitchEvent(next.reason) && shouldForwardBridgeEventToWechat(options.adapter, next.type)) {
895
1098
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
896
- await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched");
1099
+ await queueWechatMessage(authorizedUserId, formatSessionSwitchMessage({ adapter: options.adapter, sessionId: next.threadId, source: next.source, reason: next.reason }), "thread_switched", eventTarget);
897
1100
  }));
898
1101
  }
899
1102
  void maybeDrainDeferredInboundMessages();
900
1103
  },
901
1104
  taskComplete: () => {
1105
+ void channelDriver.endTyping?.(authorizedUserId);
902
1106
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
903
1107
  stateStore.clearPendingConfirmation();
904
1108
  stateStore.clearPendingUserInput();
905
- clearActiveTask();
1109
+ clearActiveTask(eventTask);
906
1110
  await maybeDrainDeferredInboundMessages();
907
1111
  }));
908
1112
  },
909
1113
  taskFailed: (next) => {
1114
+ void channelDriver.endTyping?.(authorizedUserId);
910
1115
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
911
1116
  stateStore.clearPendingConfirmation();
912
1117
  stateStore.clearPendingUserInput();
913
- clearActiveTask();
914
- await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed");
1118
+ clearActiveTask(eventTask);
1119
+ await queueWechatMessage(authorizedUserId, formatTaskFailedMessage(options.adapter, next.message), "task_failed", eventTarget);
915
1120
  await maybeDrainDeferredInboundMessages();
916
1121
  }));
917
1122
  },
918
1123
  fatalError: (next) => {
1124
+ void channelDriver.endTyping?.(authorizedUserId);
919
1125
  logError(next.message);
920
1126
  stateStore.appendLog(`fatal_error: ${next.message}`);
921
1127
  stateStore.clearPendingConfirmation();
922
1128
  stateStore.clearPendingUserInput();
923
- clearActiveTask();
1129
+ clearActiveTask(eventTask);
924
1130
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
925
- await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error");
1131
+ await queueWechatMessage(authorizedUserId, formatUserFacingBridgeFatalError(next.message), "fatal_error", eventTarget);
926
1132
  await maybeDrainDeferredInboundMessages();
927
1133
  }));
928
1134
  },
@@ -950,11 +1156,11 @@ function formatInboundMessagePreview(message) {
950
1156
  return "(empty)";
951
1157
  }
952
1158
  async function handleInboundMessage(params) {
953
- const { message, } = params;
954
- const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, deferInboundMessage, } = params;
1159
+ const { message, channelMessage, channelDriver, } = params;
1160
+ const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, dispatchInboundText, deferInboundMessage, } = params;
955
1161
  const state = stateStore.getState();
956
1162
  if (message.senderId !== state.authorizedUserId) {
957
- await queueWechatMessage(message.senderId, "Unauthorized. This bridge only accepts messages from the configured WeChat owner.");
1163
+ await queueWechatMessage(message.senderId, `Unauthorized. This bridge only accepts messages from ${channelDriver.operatorDescription}.`);
958
1164
  return null;
959
1165
  }
960
1166
  const systemCommand = parseWechatControlCommand(message.text, {
@@ -963,12 +1169,19 @@ async function handleInboundMessage(params) {
963
1169
  hasPendingUserInput: Boolean(state.pendingUserInput),
964
1170
  });
965
1171
  switch (systemCommand?.type) {
1172
+ case "model":
1173
+ case "plan":
1174
+ await queueWechatMessage(message.senderId, await handleAdapterControl(adapter, message.senderId, systemCommand));
1175
+ return null;
1176
+ case "broadcast":
1177
+ await queueWechatMessage(message.senderId, "/all is only available in daemon mode. Start the daemon with wechat-daemon or wecom-daemon to broadcast to all workers.");
1178
+ return null;
966
1179
  case "status":
967
1180
  await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
968
1181
  return null;
969
1182
  case "resume": {
970
1183
  if (!isWechatResumeEnabled(options.adapter)) {
971
- 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.`);
1184
+ await queueWechatMessage(message.senderId, `${channelDriver.displayName} /resume is disabled in ${options.adapter} mode. Use /resume directly inside "${channelDriver.id}-${options.adapter}"; the remote channel will follow the active local session.`);
972
1185
  return null;
973
1186
  }
974
1187
  try {
@@ -1081,7 +1294,7 @@ async function handleInboundMessage(params) {
1081
1294
  }
1082
1295
  const adapterState = adapter.getState();
1083
1296
  const routeResult = await routeBridgeMessage({
1084
- message: toChannelInboundMessage(message),
1297
+ message: channelMessage ?? toChannelInboundMessage(message),
1085
1298
  authorized: true,
1086
1299
  command: null,
1087
1300
  adapterState,
@@ -1117,29 +1330,25 @@ async function handleInboundMessage(params) {
1117
1330
  defer: async () => {
1118
1331
  await deferInboundMessage(message);
1119
1332
  },
1120
- dispatch: async () => {
1121
- return await dispatchInboundWechatText({
1122
- message,
1123
- options,
1124
- stateStore,
1125
- adapter,
1126
- });
1127
- },
1333
+ dispatch: dispatchInboundText,
1128
1334
  });
1129
1335
  return routeResult.kind === "dispatched" ? routeResult.result : null;
1130
1336
  }
1131
- async function dispatchInboundWechatText(params) {
1132
- const { message, options, stateStore, adapter } = params;
1133
- const preview = formatInboundMessagePreview(message);
1134
- const activeTask = {
1337
+ function createActiveTask(message) {
1338
+ return {
1135
1339
  startedAt: Date.now(),
1136
- inputPreview: truncatePreview(preview, 180),
1340
+ inputPreview: truncatePreview(formatInboundMessagePreview(message), 180),
1137
1341
  };
1342
+ }
1343
+ async function dispatchInboundWechatText(params) {
1344
+ const { message, options, channelDriver, stateStore, adapter } = params;
1345
+ const preview = formatInboundMessagePreview(message);
1346
+ const activeTask = params.activeTask ?? createActiveTask(message);
1138
1347
  stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
1139
- await adapter.sendInput(buildWechatInboundPrompt(message.text, message.attachments));
1348
+ await adapter.sendInput(channelDriver.buildInboundPrompt(message.text, message.attachments));
1140
1349
  return activeTask;
1141
1350
  }
1142
- const isDirectRun = Boolean(import.meta.main);
1351
+ const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);
1143
1352
  if (isDirectRun) {
1144
1353
  main().catch((err) => {
1145
1354
  logError(describeWechatTransportError(err));