cli-wechat-bridge 1.1.8 → 1.2.0

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 (76) hide show
  1. package/README.md +17 -4
  2. package/bin/_run-entry.mjs +6 -1
  3. package/bin/wechat-check-update.mjs +6 -1
  4. package/bin/wechat-claude.mjs +6 -1
  5. package/bin/wechat-codex.mjs +6 -1
  6. package/bin/wechat-daemon.mjs +6 -1
  7. package/bin/wechat-opencode.mjs +6 -1
  8. package/bin/wechat-pi.mjs +6 -1
  9. package/bin/wechat-setup.mjs +6 -1
  10. package/bin/wecom-check-update.mjs +6 -1
  11. package/bin/wecom-claude.mjs +6 -1
  12. package/bin/wecom-codex.mjs +6 -1
  13. package/bin/wecom-daemon.mjs +6 -1
  14. package/bin/wecom-opencode.mjs +6 -1
  15. package/bin/wecom-pi.mjs +6 -1
  16. package/bin/wecom-setup.mjs +6 -1
  17. package/dist/bridge/bridge-adapters.claude.js +6 -0
  18. package/dist/bridge/bridge-adapters.codex.js +6 -0
  19. package/dist/bridge/bridge-adapters.core.js +6 -0
  20. package/dist/bridge/bridge-adapters.opencode.js +6 -0
  21. package/dist/bridge/bridge-adapters.pi.js +6 -0
  22. package/dist/bridge/bridge-adapters.shared.js +6 -0
  23. package/dist/bridge/bridge-controller.js +6 -0
  24. package/dist/bridge/bridge-final-reply.js +6 -0
  25. package/dist/bridge/bridge-process-reaper.js +6 -0
  26. package/dist/bridge/bridge-state.js +6 -0
  27. package/dist/bridge/bridge-utils.js +6 -31
  28. package/dist/bridge/claude-hook.js +6 -0
  29. package/dist/bridge/native-terminal-control.js +6 -0
  30. package/dist/bridge/wechat-bridge.js +148 -241
  31. package/dist/bridge/wechat-forwarding.js +6 -0
  32. package/dist/bridge/wechat-outbound-queue.js +6 -0
  33. package/dist/channels/wechat/bridge-final-reply.js +13 -22
  34. package/dist/channels/wechat/index.js +6 -0
  35. package/dist/channels/wechat/wechat-driver.js +191 -0
  36. package/dist/channels/wechat/wechat-forwarding.js +18 -0
  37. package/dist/channels/wechat/wechat-outbound-queue.js +39 -0
  38. package/dist/channels/wecom/bridge-final-reply.js +6 -0
  39. package/dist/channels/wecom/setup-cli.js +6 -0
  40. package/dist/channels/wecom/setup.js +6 -0
  41. package/dist/channels/wecom/wecom-config.js +6 -0
  42. package/dist/channels/wecom/wecom-driver.js +106 -0
  43. package/dist/channels/wecom/wecom-media.js +6 -0
  44. package/dist/channels/wecom/wecom-transport.js +6 -0
  45. package/dist/commands/check-update.js +6 -0
  46. package/dist/companion/codex-remote-client.js +6 -0
  47. package/dist/companion/codex-visible-client-link.js +6 -0
  48. package/dist/companion/local-companion-link.js +6 -0
  49. package/dist/companion/local-companion-start.js +6 -0
  50. package/dist/companion/local-companion.js +6 -0
  51. package/dist/companion/opencode-tui-bridge-plugin.js +6 -0
  52. package/dist/companion/pi-tui-bridge-extension.js +6 -0
  53. package/dist/core/bridge-control.js +2 -0
  54. package/dist/core/bridge-defer.js +25 -0
  55. package/dist/core/channel-driver.js +1 -0
  56. package/dist/core/conversation-routing.js +11 -1
  57. package/dist/core/direct-run.js +6 -0
  58. package/dist/core/inbound-message-claims.js +6 -0
  59. package/dist/core/outbound-target.js +32 -0
  60. package/dist/core/text-utils.js +6 -0
  61. package/dist/core/turn-coordinator.js +85 -0
  62. package/dist/daemon/daemon-link.js +6 -0
  63. package/dist/daemon/emoji-bindings.js +6 -0
  64. package/dist/daemon/wechat-daemon.js +451 -173
  65. package/dist/i18n/index.js +6 -0
  66. package/dist/i18n/messages-en.js +7 -1
  67. package/dist/i18n/messages-zh.js +7 -1
  68. package/dist/runtime/create-runtime-host.js +6 -0
  69. package/dist/utils/atomic-file.js +6 -0
  70. package/dist/utils/doctor.js +6 -0
  71. package/dist/utils/version-checker.js +6 -0
  72. package/dist/wechat/channel-config.js +6 -0
  73. package/dist/wechat/setup.js +6 -0
  74. package/dist/wechat/wechat-channel.js +6 -0
  75. package/dist/wechat/wechat-transport.js +73 -0
  76. package/package.json +1 -1
@@ -1,18 +1,26 @@
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";
15
- import { clearTurn, resolveConversationTarget, rollbackTurn, tryBeginTurn, } from "../core/conversation-routing.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";
16
24
  import { handleAdapterControl } from "./adapter-control.js";
17
25
  import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
18
26
  import { isDirectModuleRun } from "../core/direct-run.js";
@@ -20,9 +28,10 @@ import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
20
28
  import { ensureWecomAccount } from "../channels/wecom/setup.js";
21
29
  import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
22
30
  import { WecomTransport } from "../channels/wecom/wecom-transport.js";
23
- import { buildWecomInboundPrompt, formatWecomVisibleText, } from "../channels/wecom/wecom-message.js";
31
+ import { WecomChannelDriver } from "../channels/wecom/wecom-driver.js";
32
+ import { WechatChannelDriver } from "../channels/wechat/wechat-driver.js";
24
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";
25
- import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, isWechatContextTokenStaleError, } from "../wechat/wechat-transport.js";
34
+ import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, } from "../wechat/wechat-transport.js";
26
35
  import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
27
36
  import { checkForUpdate, formatUpdateMessage, } from "../utils/version-checker.js";
28
37
  import { clearDaemonEndpoint, isDaemonEndpointAlive, readDaemonEndpoint, } from "../daemon/daemon-link.js";
@@ -80,31 +89,7 @@ function toPendingUserInput(request) {
80
89
  createdAt: nowIso(),
81
90
  };
82
91
  }
83
- export function shouldDeferCodexInboundMessage(params) {
84
- return (params.adapter === "codex" &&
85
- !params.hasPendingConfirmation &&
86
- !params.hasSystemCommand &&
87
- params.activeTurnOrigin === "local" &&
88
- (params.status === "busy" || params.status === "awaiting_approval"));
89
- }
90
- export function canDrainDeferredCodexInboundQueue(params) {
91
- return (params.adapter === "codex" &&
92
- params.deferredCount > 0 &&
93
- !params.hasPendingConfirmation &&
94
- !params.hasPendingUserInput &&
95
- !params.hasPendingApproval &&
96
- !params.hasActiveTask &&
97
- !params.activeTurnId &&
98
- params.status !== "busy" &&
99
- params.status !== "awaiting_approval" &&
100
- params.status !== "awaiting_input");
101
- }
102
- export function formatDeferredCodexInboundQueueMessage(queuePosition) {
103
- return `Queued for delivery after the current local Codex turn finishes. Queue position: ${queuePosition}.`;
104
- }
105
- export function isRetryableDeferredCodexDrainError(errorText) {
106
- return /still working|approval request is pending|waiting for local terminal input/i.test(errorText);
107
- }
92
+ export { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessage, isRetryableDeferredCodexDrainError, shouldDeferCodexInboundMessage, } from "../core/bridge-defer.js";
108
93
  export function parseCliArgs(argv) {
109
94
  let adapter = null;
110
95
  let commandOverride;
@@ -191,24 +176,6 @@ export function parseCliArgs(argv) {
191
176
  channelId,
192
177
  };
193
178
  }
194
- function toWecomOutputKind(context) {
195
- const known = new Set([
196
- "status",
197
- "notice",
198
- "thinking",
199
- "final_reply",
200
- "approval_required",
201
- "user_input_required",
202
- "mirrored_input",
203
- "task_failed",
204
- "fatal_error",
205
- ]);
206
- return known.has(context)
207
- ? context
208
- : context === "mirrored_user_input"
209
- ? "mirrored_input"
210
- : "notice";
211
- }
212
179
  function toWechatSendContext(kind) {
213
180
  if (kind === "mirrored_input") {
214
181
  return "mirrored_user_input";
@@ -285,6 +252,18 @@ async function main() {
285
252
  },
286
253
  })
287
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
+ });
288
267
  // 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
289
268
  // unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
290
269
  // 否则会与强制退出 teardown 竞态。
@@ -363,17 +342,13 @@ async function main() {
363
342
  let textSendChain = Promise.resolve();
364
343
  let attachmentSendChain = Promise.resolve();
365
344
  const pendingWechatForwardTasks = new Set();
366
- const defaultWecomConversation = {
367
- channelId: "wecom",
368
- accountId: wecomAccount?.botId,
369
- conversationId: credentials.userId,
370
- recipientId: credentials.userId,
371
- metadata: { chatType: "direct" },
372
- };
373
- const turnState = {
374
- activeTask: null,
375
- lastConversation: defaultWecomConversation,
376
- };
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
+ });
377
352
  const deferredInboundMessages = [];
378
353
  let drainingDeferredInboundMessages = false;
379
354
  let consecutivePollFailures = 0;
@@ -389,101 +364,28 @@ async function main() {
389
364
  return run;
390
365
  };
391
366
  const sendWechatMessageNow = async (senderId, text, context = "message", targetOverride) => {
392
- if (options.channelId === "wecom") {
393
- const target = targetOverride ??
394
- (senderId === credentials.userId
395
- ? resolveConversationTarget({
396
- active: turnState.activeConversation,
397
- last: turnState.lastConversation,
398
- fallback: defaultWecomConversation,
399
- })
400
- : {
401
- channelId: "wecom",
402
- accountId: wecomAccount?.botId,
403
- conversationId: senderId,
404
- recipientId: senderId,
405
- metadata: { chatType: "direct" },
406
- });
407
- const startedAtMs = Date.now();
408
- stateStore.appendLog(`wecom_send_started: context=${context} recipient=${target.recipientId} chars=${Array.from(text).length}`);
409
- try {
410
- await wecomTransport.sendText(target, formatWecomVisibleText(text), toWecomOutputKind(context));
411
- stateStore.appendLog(`wecom_send_completed: context=${context} recipient=${target.recipientId} elapsed_ms=${Date.now() - startedAtMs}`);
412
- return { status: "sent" };
413
- }
414
- catch (error) {
415
- const message = error instanceof Error ? error.message : String(error);
416
- stateStore.appendLog(`wecom_send_failed: context=${context} recipient=${target.recipientId} error=${truncatePreview(message, 400)}`);
417
- logError(`Failed to send WeCom ${context}: ${message}`);
418
- return { status: "context_unavailable", error, target };
419
- }
420
- }
421
- const startedAtMs = Date.now();
422
- stateStore.appendLog(`wechat_send_started: context=${context} recipient=${senderId} chars=${Array.from(text).length}`);
423
- for (let attempt = 1; attempt <= WECHAT_SEND_MAX_ATTEMPTS; attempt += 1) {
424
- try {
425
- await transport.sendText(senderId, text);
426
- stateStore.appendLog(`wechat_send_completed: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
427
- return { status: "sent" };
428
- }
429
- catch (err) {
430
- if (err instanceof Error && err.name === "AbortError") {
431
- stateStore.appendLog(`wechat_send_timeout: context=${context} recipient=${senderId} attempt=${attempt} elapsed_ms=${Date.now() - startedAtMs}`);
432
- }
433
- if (isWechatContextUnavailableError(err)) {
434
- if (isWechatContextTokenStaleError(err)) {
435
- transport.clearCachedContextToken(senderId);
436
- }
437
- 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.";
438
- logError(`Failed to send WeChat ${context}: ${hint}`);
439
- stateStore.appendLog(isWechatContextTokenStaleError(err)
440
- ? formatWechatContextTokenStaleLogEntry({
441
- context,
442
- recipientId: senderId,
443
- error: err,
444
- })
445
- : formatWechatSendFailureLogEntry({
446
- context,
447
- recipientId: senderId,
448
- error: err,
449
- }));
450
- return { status: "context_unavailable", error: err };
451
- }
452
- if (attempt < WECHAT_SEND_MAX_ATTEMPTS && isRetryableWechatSendError(err)) {
453
- const delayMs = computeWechatSendRetryDelayMs(attempt);
454
- logError(`Failed to send WeChat ${context} (attempt ${attempt}). Retrying in ${formatDuration(delayMs)}. ${describeWechatTransportError(err)}`);
455
- stateStore.appendLog(formatWechatSendRetryLogEntry({
456
- context,
457
- recipientId: senderId,
458
- attempt,
459
- delayMs,
460
- error: err,
461
- }));
462
- await delay(delayMs);
463
- continue;
464
- }
465
- logError(`Failed to send WeChat ${context}: ${describeWechatTransportError(err)}`);
466
- stateStore.appendLog(formatWechatSendFailureLogEntry({
467
- context,
468
- recipientId: senderId,
469
- error: err,
470
- }));
471
- return { status: "failed" };
472
- }
473
- }
474
- return { status: "failed" };
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
+ });
475
381
  };
476
382
  const queueWechatMessage = (senderId, text, context = "message", targetOverride) => {
477
- const queuedTarget = options.channelId === "wecom" && senderId === credentials.userId
478
- ? targetOverride ?? resolveConversationTarget({
479
- active: turnState.activeConversation,
480
- last: turnState.lastConversation,
481
- fallback: defaultWecomConversation,
482
- })
383
+ const queuedTarget = senderId === operatorId && channelDriver.capabilities.multiConversation
384
+ ? targetOverride ?? turns.resolveTarget(operatorConversation)
483
385
  : targetOverride;
484
386
  return queueWechatTextAction(async () => {
485
387
  const result = await sendWechatMessageNow(senderId, text, context, queuedTarget);
486
- if (result.status === "context_unavailable") {
388
+ if (result.status === "target_stale") {
487
389
  const pending = pendingWechatMessages.enqueue(senderId, text, context, result.target);
488
390
  if (pending) {
489
391
  stateStore.appendLog(`wechat_send_queued: id=${pending.id} context=${context} recipient=${senderId} pending=${pendingWechatMessages.list().length}`);
@@ -501,7 +403,7 @@ async function main() {
501
403
  stateStore.appendLog(`wechat_pending_sent: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
502
404
  continue;
503
405
  }
504
- if (result.status === "context_unavailable") {
406
+ if (result.status === "target_stale") {
505
407
  break;
506
408
  }
507
409
  stateStore.appendLog(`wechat_pending_retryable_failure: id=${pending.id} context=${pending.context} recipient=${pending.recipientId}`);
@@ -526,12 +428,8 @@ async function main() {
526
428
  }
527
429
  };
528
430
  const outputBatcher = new OutputBatcher(async (text) => {
529
- await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", options.channelId === "wecom"
530
- ? resolveConversationTarget({
531
- active: turnState.activeConversation,
532
- last: turnState.lastConversation,
533
- fallback: defaultWecomConversation,
534
- })
431
+ await queueWechatMessage(stateStore.getState().authorizedUserId, text, "message", channelDriver.capabilities.multiConversation
432
+ ? turns.resolveTarget(operatorConversation)
535
433
  : undefined);
536
434
  });
537
435
  const maybeDrainDeferredInboundMessages = async () => {
@@ -547,7 +445,7 @@ async function main() {
547
445
  hasPendingConfirmation: Boolean(stateStore.getState().pendingConfirmation),
548
446
  hasPendingUserInput: Boolean(stateStore.getState().pendingUserInput),
549
447
  hasPendingApproval: Boolean(adapterState.pendingApproval),
550
- hasActiveTask: Boolean(turnState.activeTask),
448
+ hasActiveTask: turns.hasActiveTask,
551
449
  })) {
552
450
  return;
553
451
  }
@@ -559,7 +457,7 @@ async function main() {
559
457
  try {
560
458
  stateStore.appendLog(`draining_deferred_inbound_input: remaining=${deferredInboundMessages.length} text=${truncatePreview(nextDeferred.message.text)}`);
561
459
  const nextTask = createActiveTask(nextDeferred.message);
562
- const lease = tryBeginTurn(turnState, nextTask, nextDeferred.channelMessage?.conversation);
460
+ const lease = turns.beginTurn(nextTask, nextDeferred.channelMessage?.conversation);
563
461
  if (!lease) {
564
462
  deferredInboundMessages.unshift(nextDeferred);
565
463
  return;
@@ -567,6 +465,7 @@ async function main() {
567
465
  try {
568
466
  await dispatchInboundWechatText({
569
467
  message: nextDeferred.message,
468
+ channelDriver,
570
469
  options,
571
470
  stateStore,
572
471
  adapter,
@@ -574,7 +473,7 @@ async function main() {
574
473
  });
575
474
  }
576
475
  catch (error) {
577
- rollbackTurn(turnState, lease);
476
+ turns.rollback(lease);
578
477
  throw error;
579
478
  }
580
479
  }
@@ -664,6 +563,12 @@ async function main() {
664
563
  catch {
665
564
  // Best effort shutdown.
666
565
  }
566
+ try {
567
+ await channelDriver.endAllTyping?.();
568
+ }
569
+ catch {
570
+ // Best effort shutdown.
571
+ }
667
572
  controller.clearLocalClientEndpoint();
668
573
  stateStore.releaseLock();
669
574
  };
@@ -732,6 +637,7 @@ async function main() {
732
637
  try {
733
638
  wireAdapterEvents({
734
639
  adapter,
640
+ channelDriver,
735
641
  resumeCoordinator,
736
642
  options,
737
643
  transport,
@@ -741,10 +647,10 @@ async function main() {
741
647
  queueWechatMessage,
742
648
  trackWechatForwardTask,
743
649
  maybeDrainDeferredInboundMessages,
744
- getActiveTask: () => turnState.activeTask,
650
+ getActiveTask: () => turns.activeTask,
745
651
  clearActiveTask: (expectedTask) => {
746
652
  if (expectedTask) {
747
- clearTurn(turnState, expectedTask);
653
+ turns.complete(expectedTask);
748
654
  }
749
655
  },
750
656
  syncSharedSessionState: () => {
@@ -755,11 +661,7 @@ async function main() {
755
661
  },
756
662
  requestShutdown,
757
663
  wecomTransport,
758
- getWecomTarget: () => resolveConversationTarget({
759
- active: turnState.activeConversation,
760
- last: turnState.lastConversation,
761
- fallback: defaultWecomConversation,
762
- }),
664
+ getWecomTarget: () => turns.resolveTarget(operatorConversation),
763
665
  });
764
666
  await adapter.start();
765
667
  if (!ensureRuntimeOwnership()) {
@@ -768,24 +670,25 @@ async function main() {
768
670
  syncSharedSessionState(stateStore, adapter);
769
671
  controller.syncLocalClientEndpoint();
770
672
  stateStore.appendLog(`Bridge started with channel=${options.channelId} adapter=${options.adapter} command=${options.command} cwd=${options.cwd}`);
771
- if (wecomTransport) {
772
- wecomTransport.setHandlers({
773
- onMessage: async (channelMessage) => {
673
+ if (channelDriver.start) {
674
+ await channelDriver.start({
675
+ onInboundMessage: async (channelMessage) => {
774
676
  if (shutdownPromise || !ensureRuntimeOwnership()) {
775
677
  return;
776
678
  }
777
- turnState.lastConversation = channelMessage.conversation;
679
+ turns.observeConversation(channelMessage.conversation);
778
680
  const queueInboundReply = (senderId, text, context) => queueWechatMessage(senderId, text, context, channelMessage.conversation);
779
681
  if (pendingWechatMessages.list().length > 0) {
780
682
  await flushPendingWechatMessages();
781
683
  }
782
684
  const message = toLegacyInboundWechatMessage(channelMessage);
783
- const taskAtMessageStart = turnState.activeTask;
685
+ const taskAtMessageStart = turns.activeTask;
784
686
  stateStore.touchActivity(message.createdAt);
785
687
  try {
786
688
  await handleInboundMessage({
787
689
  message,
788
690
  channelMessage,
691
+ channelDriver,
789
692
  options,
790
693
  stateStore,
791
694
  adapter,
@@ -794,30 +697,32 @@ async function main() {
794
697
  outputBatcher,
795
698
  clearActiveTask: () => {
796
699
  if (taskAtMessageStart) {
797
- clearTurn(turnState, taskAtMessageStart);
700
+ turns.complete(taskAtMessageStart);
798
701
  }
799
702
  },
800
703
  dispatchInboundText: async () => {
801
704
  const nextActiveTask = createActiveTask(message);
802
- const lease = tryBeginTurn(turnState, nextActiveTask, channelMessage.conversation);
803
- if (!lease) {
804
- await queueInboundReply(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
805
- return null;
806
- }
807
- try {
808
- await dispatchInboundWechatText({
809
- message,
810
- options,
811
- stateStore,
812
- adapter,
813
- activeTask: nextActiveTask,
814
- });
815
- return nextActiveTask;
816
- }
817
- catch (error) {
818
- rollbackTurn(turnState, lease);
819
- throw error;
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);
820
724
  }
725
+ return result.status === "dispatched" ? nextActiveTask : null;
821
726
  },
822
727
  deferInboundMessage: async (nextMessage) => {
823
728
  deferredInboundMessages.push({
@@ -842,37 +747,38 @@ async function main() {
842
747
  syncSharedSessionState(stateStore, adapter);
843
748
  await maybeDrainDeferredInboundMessages();
844
749
  },
845
- onUnauthorized: async (senderId, chatType) => {
750
+ onUnauthorizedSender: async (senderId, chatType) => {
846
751
  stateStore.appendLog(`wecom_unauthorized: sender=${senderId} chat_type=${chatType}`);
847
752
  if (chatType === "direct") {
848
- await wecomTransport.sendText({
849
- channelId: "wecom",
850
- accountId: wecomAccount.botId,
851
- conversationId: senderId,
852
- recipientId: senderId,
853
- metadata: { chatType: "direct" },
854
- }, "Unauthorized.", "notice");
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
+ }
855
762
  }
856
763
  },
857
- onFatal: async (error) => {
764
+ onChannelFatal: async (error) => {
858
765
  stateStore.appendLog(`wecom_fatal_error: ${error.message}`);
859
766
  requestShutdown(error.message, 1);
860
767
  },
861
- onConnected: async () => {
768
+ onChannelConnected: async () => {
862
769
  if (pendingWechatMessages.list().length > 0) {
863
770
  await flushPendingWechatMessages();
864
771
  }
865
772
  },
866
773
  });
867
- wecomTransport.start();
868
- await wecomTransport.waitUntilConnected();
774
+ await channelDriver.waitUntilReady?.();
869
775
  }
870
- log(`${options.channelId === "wecom" ? "WeCom" : "WeChat"} bridge is ready for adapter "${options.adapter}".`);
776
+ log(`${channelDriver.displayName} bridge is ready for adapter "${options.adapter}".`);
871
777
  log(`Working directory: ${options.cwd}`);
872
778
  if (options.profile) {
873
779
  log(`Profile: ${options.profile}`);
874
780
  }
875
- log(`Authorized ${options.channelId === "wecom" ? "WeCom" : "WeChat"} user: ${credentials.userId}`);
781
+ log(`Authorized ${channelDriver.displayName} user: ${credentials.userId}`);
876
782
  if (options.adapter === "codex") {
877
783
  log("For source-mode debugging, open the visible Codex client with: npm run codex:panel");
878
784
  }
@@ -890,7 +796,7 @@ async function main() {
890
796
  cwd: options.cwd,
891
797
  });
892
798
  await queueWechatMessage(credentials.userId, welcomeText);
893
- if (options.channelId === "wecom") {
799
+ if (channelDriver.capabilities.pushInbound) {
894
800
  while (!shutdownPromise && ensureRuntimeOwnership()) {
895
801
  await delay(1_000);
896
802
  }
@@ -956,11 +862,12 @@ async function main() {
956
862
  if (!ensureRuntimeOwnership()) {
957
863
  break;
958
864
  }
959
- const taskAtMessageStart = turnState.activeTask;
865
+ const taskAtMessageStart = turns.activeTask;
960
866
  stateStore.touchActivity(message.createdAt);
961
867
  try {
962
868
  await handleInboundMessage({
963
869
  message,
870
+ channelDriver,
964
871
  options,
965
872
  stateStore,
966
873
  adapter,
@@ -969,30 +876,31 @@ async function main() {
969
876
  outputBatcher,
970
877
  clearActiveTask: () => {
971
878
  if (taskAtMessageStart) {
972
- clearTurn(turnState, taskAtMessageStart);
879
+ turns.complete(taskAtMessageStart);
973
880
  }
974
881
  },
975
882
  dispatchInboundText: async () => {
976
883
  const nextActiveTask = createActiveTask(message);
977
- const lease = tryBeginTurn(turnState, nextActiveTask);
978
- if (!lease) {
979
- await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
980
- return null;
981
- }
982
- try {
983
- await dispatchInboundWechatText({
984
- message,
985
- options,
986
- stateStore,
987
- adapter,
988
- activeTask: nextActiveTask,
989
- });
990
- return nextActiveTask;
991
- }
992
- catch (error) {
993
- rollbackTurn(turnState, lease);
994
- throw error;
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);
995
902
  }
903
+ return result.status === "dispatched" ? nextActiveTask : null;
996
904
  },
997
905
  deferInboundMessage: async (nextMessage) => {
998
906
  deferredInboundMessages.push({
@@ -1047,7 +955,7 @@ function syncSharedSessionState(stateStore, adapter) {
1047
955
  }
1048
956
  }
1049
957
  function wireAdapterEvents(params) {
1050
- const { adapter, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
958
+ const { adapter, channelDriver, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
1051
959
  const channelPort = options.channelId === "wecom"
1052
960
  ? new WecomChannelPort({
1053
961
  transport: wecomTransport,
@@ -1083,13 +991,9 @@ function wireAdapterEvents(params) {
1083
991
  stateStore.clearPendingUserInput();
1084
992
  }
1085
993
  const authorizedUserId = stateStore.getState().authorizedUserId;
1086
- const eventTarget = options.channelId === "wecom"
994
+ const eventTarget = channelDriver.capabilities.multiConversation
1087
995
  ? getWecomTarget()
1088
- : {
1089
- channelId: "wechat",
1090
- conversationId: authorizedUserId,
1091
- recipientId: authorizedUserId,
1092
- };
996
+ : channelDriver.directConversation(authorizedUserId);
1093
997
  const eventTask = getActiveTask();
1094
998
  eventForwardChain = eventForwardChain
1095
999
  .then(() => forwardBridgeEvent(event, {
@@ -1104,6 +1008,7 @@ function wireAdapterEvents(params) {
1104
1008
  }
1105
1009
  },
1106
1010
  finalReply: (next) => {
1011
+ void channelDriver.endTyping?.(authorizedUserId);
1107
1012
  lastFinalReplyAtMs = Date.now();
1108
1013
  stateStore.appendLog(`final_reply: ${truncatePreview(next.text)}`);
1109
1014
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
@@ -1136,7 +1041,7 @@ function wireAdapterEvents(params) {
1136
1041
  if (thinkingPreview) {
1137
1042
  stateStore.appendLog(`thinking: ${thinkingPreview}`);
1138
1043
  trackWechatForwardTask((async () => {
1139
- await queueWechatMessage(authorizedUserId, options.channelId === "wecom"
1044
+ await queueWechatMessage(authorizedUserId, channelDriver.id === "wecom"
1140
1045
  ? `Processing: ${thinkingPreview}`
1141
1046
  : `思考: ${thinkingPreview}`, "thinking", eventTarget);
1142
1047
  })());
@@ -1197,6 +1102,7 @@ function wireAdapterEvents(params) {
1197
1102
  void maybeDrainDeferredInboundMessages();
1198
1103
  },
1199
1104
  taskComplete: () => {
1105
+ void channelDriver.endTyping?.(authorizedUserId);
1200
1106
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1201
1107
  stateStore.clearPendingConfirmation();
1202
1108
  stateStore.clearPendingUserInput();
@@ -1205,6 +1111,7 @@ function wireAdapterEvents(params) {
1205
1111
  }));
1206
1112
  },
1207
1113
  taskFailed: (next) => {
1114
+ void channelDriver.endTyping?.(authorizedUserId);
1208
1115
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1209
1116
  stateStore.clearPendingConfirmation();
1210
1117
  stateStore.clearPendingUserInput();
@@ -1214,6 +1121,7 @@ function wireAdapterEvents(params) {
1214
1121
  }));
1215
1122
  },
1216
1123
  fatalError: (next) => {
1124
+ void channelDriver.endTyping?.(authorizedUserId);
1217
1125
  logError(next.message);
1218
1126
  stateStore.appendLog(`fatal_error: ${next.message}`);
1219
1127
  stateStore.clearPendingConfirmation();
@@ -1248,13 +1156,11 @@ function formatInboundMessagePreview(message) {
1248
1156
  return "(empty)";
1249
1157
  }
1250
1158
  async function handleInboundMessage(params) {
1251
- const { message, channelMessage, } = params;
1159
+ const { message, channelMessage, channelDriver, } = params;
1252
1160
  const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, dispatchInboundText, deferInboundMessage, } = params;
1253
1161
  const state = stateStore.getState();
1254
1162
  if (message.senderId !== state.authorizedUserId) {
1255
- await queueWechatMessage(message.senderId, options.channelId === "wecom"
1256
- ? "Unauthorized. This bridge only accepts messages from the paired WeCom operator."
1257
- : "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}.`);
1258
1164
  return null;
1259
1165
  }
1260
1166
  const systemCommand = parseWechatControlCommand(message.text, {
@@ -1267,12 +1173,15 @@ async function handleInboundMessage(params) {
1267
1173
  case "plan":
1268
1174
  await queueWechatMessage(message.senderId, await handleAdapterControl(adapter, message.senderId, systemCommand));
1269
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;
1270
1179
  case "status":
1271
1180
  await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
1272
1181
  return null;
1273
1182
  case "resume": {
1274
1183
  if (!isWechatResumeEnabled(options.adapter)) {
1275
- await queueWechatMessage(message.senderId, `${options.channelId === "wecom" ? "WeCom" : "WeChat"} /resume is disabled in ${options.adapter} mode. Use /resume directly inside "${options.channelId}-${options.adapter}"; the remote channel will follow the active local session.`);
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.`);
1276
1185
  return null;
1277
1186
  }
1278
1187
  try {
@@ -1432,13 +1341,11 @@ function createActiveTask(message) {
1432
1341
  };
1433
1342
  }
1434
1343
  async function dispatchInboundWechatText(params) {
1435
- const { message, options, stateStore, adapter } = params;
1344
+ const { message, options, channelDriver, stateStore, adapter } = params;
1436
1345
  const preview = formatInboundMessagePreview(message);
1437
1346
  const activeTask = params.activeTask ?? createActiveTask(message);
1438
1347
  stateStore.appendLog(`Forwarded input to ${options.adapter}: ${truncatePreview(preview)}`);
1439
- await adapter.sendInput(options.channelId === "wecom"
1440
- ? buildWecomInboundPrompt(message.text, message.attachments)
1441
- : buildWechatInboundPrompt(message.text, message.attachments));
1348
+ await adapter.sendInput(channelDriver.buildInboundPrompt(message.text, message.attachments));
1442
1349
  return activeTask;
1443
1350
  }
1444
1351
  const isDirectRun = isDirectModuleRun(import.meta.url, process.argv, import.meta.main);