@unlinearity/cli-wechat-bridge 1.2.1 → 1.2.3

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 (54) hide show
  1. package/README.md +146 -73
  2. package/README_EN.md +544 -0
  3. package/bin/telegram-check-update.mjs +10 -0
  4. package/bin/telegram-claude.mjs +10 -0
  5. package/bin/telegram-codex.mjs +10 -0
  6. package/bin/telegram-daemon.mjs +10 -0
  7. package/bin/telegram-opencode.mjs +10 -0
  8. package/bin/telegram-pi.mjs +10 -0
  9. package/bin/telegram-setup.mjs +10 -0
  10. package/dist/bridge/adapter-control.js +19 -12
  11. package/dist/bridge/bridge-adapters.claude.js +219 -115
  12. package/dist/bridge/bridge-adapters.codex.js +532 -99
  13. package/dist/bridge/bridge-adapters.core.js +84 -7
  14. package/dist/bridge/bridge-adapters.opencode.js +202 -55
  15. package/dist/bridge/bridge-adapters.pi.js +23 -18
  16. package/dist/bridge/bridge-adapters.shared.js +9 -9
  17. package/dist/bridge/bridge-session-resume.js +13 -6
  18. package/dist/bridge/bridge-state.js +3 -1
  19. package/dist/bridge/bridge-utils.js +94 -144
  20. package/dist/bridge/claude-hooks.js +60 -3
  21. package/dist/bridge/claude-native-controls.js +19 -12
  22. package/dist/bridge/language-control.js +33 -0
  23. package/dist/bridge/native-terminal-control.js +26 -10
  24. package/dist/bridge/opencode-v2-compat.js +11 -3
  25. package/dist/bridge/wechat-bridge.js +154 -72
  26. package/dist/channels/channel-driver-factory.js +56 -0
  27. package/dist/channels/local/local-channel-port.js +40 -0
  28. package/dist/channels/local/local-driver.js +217 -0
  29. package/dist/channels/telegram/setup-cli.js +12 -0
  30. package/dist/channels/telegram/setup.js +163 -0
  31. package/dist/channels/telegram/telegram-channel-port.js +47 -0
  32. package/dist/channels/telegram/telegram-config.js +66 -0
  33. package/dist/channels/telegram/telegram-driver.js +106 -0
  34. package/dist/channels/telegram/telegram-message.js +41 -0
  35. package/dist/channels/telegram/telegram-transport.js +378 -0
  36. package/dist/channels/wechat/wechat-driver.js +11 -2
  37. package/dist/channels/wecom/wecom-driver.js +8 -1
  38. package/dist/companion/local-companion-start.js +180 -61
  39. package/dist/companion/local-companion.js +27 -7
  40. package/dist/companion/pi-tui-bridge-extension.js +10 -4
  41. package/dist/core/bridge-control.js +3 -0
  42. package/dist/core/bridge-defer.js +3 -2
  43. package/dist/core/bridge-event-forwarder.js +3 -0
  44. package/dist/core/channel-types.js +13 -1
  45. package/dist/daemon/wechat-daemon.js +239 -126
  46. package/dist/i18n/index.js +25 -11
  47. package/dist/i18n/messages-en.js +245 -1
  48. package/dist/i18n/messages-zh.js +245 -1
  49. package/dist/runtime/legacy-adapter-runtime.js +7 -4
  50. package/dist/wechat/channel-config.js +3 -2
  51. package/package.json +14 -4
  52. package/skills/codex-bridge/SKILL.md +35 -0
  53. package/skills/codex-bridge/agents/openai.yaml +4 -0
  54. package/skills/codex-bridge/scripts/install.mjs +28 -0
@@ -22,14 +22,21 @@ import { canDrainDeferredCodexInboundQueue, formatDeferredCodexInboundQueueMessa
22
22
  import { TurnCoordinator } from "../core/turn-coordinator.js";
23
23
  import { resolveOutboundConversationTarget } from "../core/outbound-target.js";
24
24
  import { handleAdapterControl } from "./adapter-control.js";
25
+ import { handleLanguageControl } from "./language-control.js";
26
+ import { LANGUAGE_FILE } from "../wechat/channel-config.js";
25
27
  import { forwardBridgeEvent } from "../core/bridge-event-forwarder.js";
26
28
  import { isDirectModuleRun } from "../core/direct-run.js";
27
29
  import { WechatChannelPort } from "../channels/wechat/wechat-channel-port.js";
28
30
  import { ensureWecomAccount } from "../channels/wecom/setup.js";
31
+ import { ensureTelegramAccount } from "../channels/telegram/setup.js";
32
+ import { TelegramTransport } from "../channels/telegram/telegram-transport.js";
33
+ import { TelegramChannelDriver } from "../channels/telegram/telegram-driver.js";
34
+ import { TelegramChannelPort } from "../channels/telegram/telegram-channel-port.js";
29
35
  import { WecomChannelPort } from "../channels/wecom/wecom-channel-port.js";
30
36
  import { WecomTransport } from "../channels/wecom/wecom-transport.js";
31
37
  import { WecomChannelDriver } from "../channels/wecom/wecom-driver.js";
32
38
  import { WechatChannelDriver } from "../channels/wechat/wechat-driver.js";
39
+ import { isBridgeChannelId } from "../core/channel-types.js";
33
40
  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";
34
41
  import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, WeChatTransport, describeWechatTransportError, } from "../wechat/wechat-transport.js";
35
42
  import { getPendingChannelMessagesFile, getPendingWechatMessagesFile, PendingWechatMessageStore, } from "../channels/wechat/wechat-outbound-queue.js";
@@ -98,6 +105,8 @@ export function parseCliArgs(argv) {
98
105
  let lifecycle = "persistent";
99
106
  let sessionStartMode = "restore";
100
107
  let channelId = "wechat";
108
+ let codexAttachDaemon = false;
109
+ let codexAttachThreadId;
101
110
  for (let i = 0; i < argv.length; i += 1) {
102
111
  const arg = argv[i];
103
112
  const next = argv[i + 1];
@@ -145,7 +154,7 @@ export function parseCliArgs(argv) {
145
154
  i += 1;
146
155
  break;
147
156
  case "--channel":
148
- if (!next || (next !== "wechat" && next !== "wecom")) {
157
+ if (!isBridgeChannelId(next) || next === "local") {
149
158
  throw new Error(`Invalid channel: ${next ?? "(missing)"}`);
150
159
  }
151
160
  channelId = next;
@@ -154,6 +163,16 @@ export function parseCliArgs(argv) {
154
163
  case "--shutdown-on-parent-exit":
155
164
  lifecycle = "companion_bound";
156
165
  break;
166
+ case "--codex-attach-daemon":
167
+ codexAttachDaemon = true;
168
+ break;
169
+ case "--codex-thread-id":
170
+ if (!next) {
171
+ throw new Error("--codex-thread-id requires a value");
172
+ }
173
+ codexAttachThreadId = next;
174
+ i += 1;
175
+ break;
157
176
  case "--help":
158
177
  case "-h":
159
178
  printUsageAndExit();
@@ -165,6 +184,9 @@ export function parseCliArgs(argv) {
165
184
  if (!adapter) {
166
185
  throw new Error("Missing required --adapter <codex|claude|opencode|pi>");
167
186
  }
187
+ if (codexAttachDaemon && adapter !== "codex") {
188
+ throw new Error("--codex-attach-daemon requires --adapter codex");
189
+ }
168
190
  const defaultCommand = resolveDefaultAdapterCommand(adapter);
169
191
  return {
170
192
  adapter,
@@ -174,6 +196,8 @@ export function parseCliArgs(argv) {
174
196
  lifecycle,
175
197
  sessionStartMode,
176
198
  channelId,
199
+ codexAttachDaemon,
200
+ codexAttachThreadId,
177
201
  };
178
202
  }
179
203
  function toWechatSendContext(kind) {
@@ -205,7 +229,7 @@ function toLegacyInboundWechatMessage(message) {
205
229
  function printUsageAndExit() {
206
230
  process.stdout.write([
207
231
  "Internal bridge runtime usage:",
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>]",
232
+ " 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>] [--codex-attach-daemon [--codex-thread-id <id>]]",
209
233
  "",
210
234
  "This entry is internal. Users should run a wechat-* or wecom-* launcher.",
211
235
  "",
@@ -230,17 +254,24 @@ async function main() {
230
254
  const wecomAccount = options.channelId === "wecom"
231
255
  ? await ensureWecomAccount({ log })
232
256
  : null;
257
+ const telegramAccount = options.channelId === "telegram"
258
+ ? await ensureTelegramAccount({ log })
259
+ : null;
233
260
  const credentials = wecomAccount
234
261
  ? { userId: wecomAccount.operatorUserId }
235
- : await ensureWechatCredentials({
236
- requireUserId: true,
237
- validateExisting: true,
238
- log,
239
- });
262
+ : telegramAccount
263
+ ? { userId: telegramAccount.operatorUserId }
264
+ : await ensureWechatCredentials({
265
+ requireUserId: true,
266
+ validateExisting: true,
267
+ log,
268
+ });
240
269
  if (!credentials.userId) {
241
270
  throw new Error(options.channelId === "wecom"
242
271
  ? "Saved WeCom credentials are missing operatorUserId."
243
- : "Saved WeChat credentials are missing userId.");
272
+ : options.channelId === "telegram"
273
+ ? "Saved Telegram credentials are missing operatorUserId."
274
+ : "Saved WeChat credentials are missing userId.");
244
275
  }
245
276
  const transport = new WeChatTransport({ log, logError });
246
277
  const wecomTransport = wecomAccount
@@ -252,6 +283,15 @@ async function main() {
252
283
  },
253
284
  })
254
285
  : null;
286
+ const telegramTransport = telegramAccount
287
+ ? new TelegramTransport({
288
+ account: telegramAccount,
289
+ logger: {
290
+ log: (message) => log(message),
291
+ error: (message) => logError(message),
292
+ },
293
+ })
294
+ : null;
255
295
  const channelDriver = wecomTransport
256
296
  ? new WecomChannelDriver({
257
297
  transport: wecomTransport,
@@ -259,11 +299,18 @@ async function main() {
259
299
  operatorId: credentials.userId,
260
300
  logError,
261
301
  })
262
- : new WechatChannelDriver({
263
- transport,
264
- logError,
265
- buildInboundPrompt: (text, attachments) => buildWechatInboundPrompt(text, attachments.filter((attachment) => attachment.kind === "image" || attachment.kind === "file")),
266
- });
302
+ : telegramTransport
303
+ ? new TelegramChannelDriver({
304
+ transport: telegramTransport,
305
+ accountId: telegramAccount?.botUsername,
306
+ operatorId: credentials.userId,
307
+ logError,
308
+ })
309
+ : new WechatChannelDriver({
310
+ transport,
311
+ logError,
312
+ buildInboundPrompt: (text, attachments) => buildWechatInboundPrompt(text, attachments.filter((attachment) => attachment.kind === "image" || attachment.kind === "file")),
313
+ });
267
314
  // 非阻塞地检查更新(不影响启动速度,也避免首次登录时打断二维码输出)
268
315
  // unref:不能让这个延迟检查把 event loop 挂活(如 --doctor 或快速退出场景),
269
316
  // 否则会与强制退出 teardown 竞态。
@@ -325,9 +372,15 @@ async function main() {
325
372
  profile: options.profile,
326
373
  lifecycle: options.lifecycle,
327
374
  sessionStartMode: options.sessionStartMode,
328
- initialSharedSessionId: stateStore.getState().sharedSessionId ?? stateStore.getState().sharedThreadId,
375
+ // Daemon attach binds an explicit or discovered live thread; a stale
376
+ // persisted session id must not leak into the attach target.
377
+ initialSharedSessionId: options.codexAttachDaemon
378
+ ? options.codexAttachThreadId
379
+ : stateStore.getState().sharedSessionId ?? stateStore.getState().sharedThreadId,
329
380
  initialResumeConversationId: stateStore.getState().resumeConversationId,
330
381
  initialTranscriptPath: stateStore.getState().transcriptPath,
382
+ codexAttachDaemon: options.codexAttachDaemon,
383
+ codexAttachThreadId: options.codexAttachThreadId,
331
384
  });
332
385
  const pendingWechatMessages = new PendingWechatMessageStore(options.channelId === "wechat"
333
386
  ? getPendingWechatMessagesFile(options.cwd)
@@ -414,8 +467,8 @@ async function main() {
414
467
  const trackWechatForwardTask = (task) => {
415
468
  const tracked = task
416
469
  .catch((error) => {
417
- logError(`WeChat forward task failed: ${describeWechatTransportError(error)}`);
418
- stateStore.appendLog(`wechat_forward_failed: error=${truncatePreview(describeWechatTransportError(error), 400)}`);
470
+ logError(`${channelDriver.displayName} forward task failed: ${describeWechatTransportError(error)}`);
471
+ stateStore.appendLog(`${channelDriver.id}_forward_failed: error=${truncatePreview(describeWechatTransportError(error), 400)}`);
419
472
  })
420
473
  .finally(() => {
421
474
  pendingWechatForwardTasks.delete(tracked);
@@ -661,6 +714,7 @@ async function main() {
661
714
  },
662
715
  requestShutdown,
663
716
  wecomTransport,
717
+ telegramTransport,
664
718
  getWecomTarget: () => turns.resolveTarget(operatorConversation),
665
719
  });
666
720
  await adapter.start();
@@ -706,7 +760,7 @@ async function main() {
706
760
  task: nextActiveTask,
707
761
  conversation: channelMessage.conversation,
708
762
  onBusy: async () => {
709
- await queueInboundReply(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
763
+ await queueInboundReply(message.senderId, t("bridge.busy.remote", { adapter: options.adapter }));
710
764
  },
711
765
  forward: async () => {
712
766
  await dispatchInboundWechatText({
@@ -752,7 +806,7 @@ async function main() {
752
806
  if (chatType === "direct") {
753
807
  const result = await channelDriver.sendText({
754
808
  target: channelDriver.directConversation(senderId),
755
- text: "Unauthorized.",
809
+ text: t("channel.unauthorized"),
756
810
  context: "notice",
757
811
  log: (entry) => stateStore.appendLog(entry),
758
812
  });
@@ -884,7 +938,7 @@ async function main() {
884
938
  const result = await turns.dispatch({
885
939
  task: nextActiveTask,
886
940
  onBusy: async () => {
887
- await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
941
+ await queueWechatMessage(message.senderId, t("bridge.busy.remote", { adapter: options.adapter }));
888
942
  },
889
943
  forward: async () => {
890
944
  await dispatchInboundWechatText({
@@ -954,29 +1008,44 @@ function syncSharedSessionState(stateStore, adapter) {
954
1008
  }
955
1009
  }
956
1010
  }
957
- function wireAdapterEvents(params) {
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({
1011
+ export function wireAdapterEvents(params) {
1012
+ const { adapter, channelDriver, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, telegramTransport, getWecomTarget, } = params;
1013
+ const remotePortCallbacks = {
1014
+ sendText: (target, text, kind) => queueWechatMessage(stateStore.getState().authorizedUserId, text, toWechatSendContext(kind), target),
1015
+ onEmptyVisibleReply: (adapter, rawText) => {
1016
+ stateStore.appendLog(`empty_visible_final_reply: adapter=${adapter ?? options.adapter} raw=${truncatePreview(rawText)}`);
1017
+ },
1018
+ };
1019
+ // Every remote channel must provide its own port; never fall back to WeChat.
1020
+ const portFactories = {
1021
+ wecom: () => {
1022
+ if (!wecomTransport) {
1023
+ throw new Error("The wecom channel port requires a WeCom transport.");
1024
+ }
1025
+ return new WecomChannelPort({ transport: wecomTransport, ...remotePortCallbacks });
1026
+ },
1027
+ telegram: () => {
1028
+ if (!telegramTransport) {
1029
+ throw new Error("The telegram channel port requires a Telegram transport.");
1030
+ }
1031
+ return new TelegramChannelPort({ transport: telegramTransport, ...remotePortCallbacks });
1032
+ },
1033
+ wechat: () => new WechatChannelPort({
968
1034
  sendText: (recipientId, text, context) => queueWechatMessage(recipientId, text, context),
969
1035
  sendImage: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendImage(filePath, { recipientId })),
970
1036
  sendFile: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendFile(filePath, { recipientId })),
971
1037
  sendVoice: (recipientId, filePath) => queueWechatAttachmentAction(() => transport.sendVoice(filePath, recipientId)),
972
1038
  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
- },
1039
+ onEmptyVisibleReply: remotePortCallbacks.onEmptyVisibleReply,
976
1040
  onTextSent: (_adapter, text) => {
977
1041
  stateStore.appendLog(`final_reply_sent: chars=${Array.from(text).length}`);
978
1042
  },
979
- });
1043
+ }),
1044
+ };
1045
+ if (options.channelId === "local") {
1046
+ throw new Error("The local channel does not support standalone bridge output.");
1047
+ }
1048
+ const channelPort = portFactories[options.channelId]();
980
1049
  let lastFinalReplyAtMs = 0;
981
1050
  let eventForwardChain = Promise.resolve();
982
1051
  adapter.setEventSink((event) => {
@@ -984,10 +1053,12 @@ function wireAdapterEvents(params) {
984
1053
  syncLocalClientEndpoint();
985
1054
  const adapterState = adapter.getState();
986
1055
  const bridgeState = stateStore.getState();
987
- if (bridgeState.pendingConfirmation && !adapterState.pendingApproval) {
1056
+ if (bridgeState.pendingConfirmation && (!adapterState.pendingApproval
1057
+ || (adapterState.pendingApprovals !== undefined && bridgeState.pendingConfirmation.requestId !== adapterState.pendingApproval.requestId))) {
988
1058
  stateStore.clearPendingConfirmation();
989
1059
  }
990
- if (bridgeState.pendingUserInput && !adapterState.pendingUserInput) {
1060
+ if (bridgeState.pendingUserInput && (!adapterState.pendingUserInput
1061
+ || (bridgeState.pendingUserInput.requestId !== undefined && bridgeState.pendingUserInput.requestId !== adapterState.pendingUserInput.requestId))) {
991
1062
  stateStore.clearPendingUserInput();
992
1063
  }
993
1064
  const authorizedUserId = stateStore.getState().authorizedUserId;
@@ -1050,14 +1121,21 @@ function wireAdapterEvents(params) {
1050
1121
  },
1051
1122
  approvalRequired: (next) => {
1052
1123
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1124
+ const liveState = adapter.getState();
1125
+ if (liveState.pendingApprovals !== undefined && liveState.pendingApproval?.requestId !== next.request.requestId)
1126
+ return;
1053
1127
  const pending = toPendingApproval(next.request);
1054
- stateStore.setPendingConfirmation(pending);
1055
1128
  stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
1056
- await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, adapterState), "approval_required", eventTarget);
1129
+ await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, liveState), "approval_required", eventTarget);
1130
+ if (adapter.getState().pendingApproval?.requestId === pending.requestId) {
1131
+ stateStore.setPendingConfirmation(pending);
1132
+ }
1057
1133
  }));
1058
1134
  },
1059
1135
  userInputRequired: (next) => {
1060
1136
  trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
1137
+ if (next.request.requestId !== undefined && adapter.getState().pendingUserInput?.requestId !== next.request.requestId)
1138
+ return;
1061
1139
  const pending = toPendingUserInput(next.request);
1062
1140
  stateStore.setPendingUserInput(pending);
1063
1141
  stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
@@ -1155,12 +1233,12 @@ function formatInboundMessagePreview(message) {
1155
1233
  }
1156
1234
  return "(empty)";
1157
1235
  }
1158
- async function handleInboundMessage(params) {
1236
+ export async function handleInboundMessage(params) {
1159
1237
  const { message, channelMessage, channelDriver, } = params;
1160
1238
  const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, dispatchInboundText, deferInboundMessage, } = params;
1161
1239
  const state = stateStore.getState();
1162
1240
  if (message.senderId !== state.authorizedUserId) {
1163
- await queueWechatMessage(message.senderId, `Unauthorized. This bridge only accepts messages from ${channelDriver.operatorDescription}.`);
1241
+ await queueWechatMessage(message.senderId, t("bridge.unauthorized", { mode: t("bridge.mode.bridge"), operator: channelDriver.operatorDescription }));
1164
1242
  return null;
1165
1243
  }
1166
1244
  const systemCommand = parseWechatControlCommand(message.text, {
@@ -1169,19 +1247,24 @@ async function handleInboundMessage(params) {
1169
1247
  hasPendingUserInput: Boolean(state.pendingUserInput),
1170
1248
  });
1171
1249
  switch (systemCommand?.type) {
1250
+ case "language":
1251
+ await queueWechatMessage(message.senderId, await handleLanguageControl({
1252
+ target: systemCommand.target, settingsFile: params.localeSettingsFile ?? LANGUAGE_FILE, adapters: [adapter],
1253
+ }));
1254
+ return null;
1172
1255
  case "model":
1173
1256
  case "plan":
1174
1257
  await queueWechatMessage(message.senderId, await handleAdapterControl(adapter, message.senderId, systemCommand));
1175
1258
  return null;
1176
1259
  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.");
1260
+ await queueWechatMessage(message.senderId, t("bridge.broadcast.daemonOnly"));
1178
1261
  return null;
1179
1262
  case "status":
1180
1263
  await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
1181
1264
  return null;
1182
1265
  case "resume": {
1183
1266
  if (!isWechatResumeEnabled(options.adapter)) {
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.`);
1267
+ await queueWechatMessage(message.senderId, t("bridge.resume.disabled", { channel: channelDriver.displayName, adapter: options.adapter, command: `${channelDriver.id}-${options.adapter}` }));
1185
1268
  return null;
1186
1269
  }
1187
1270
  try {
@@ -1201,7 +1284,7 @@ async function handleInboundMessage(params) {
1201
1284
  }
1202
1285
  case "new_session": {
1203
1286
  if (!adapter.createSession) {
1204
- await queueWechatMessage(message.senderId, `/new is not available in ${options.adapter} mode.`);
1287
+ await queueWechatMessage(message.senderId, t("bridge.newSession.unavailable", { adapter: options.adapter }));
1205
1288
  return null;
1206
1289
  }
1207
1290
  await outputBatcher.flushNow();
@@ -1217,8 +1300,8 @@ async function handleInboundMessage(params) {
1217
1300
  case "stop": {
1218
1301
  const interrupted = await adapter.interrupt();
1219
1302
  await queueWechatMessage(message.senderId, interrupted
1220
- ? "Interrupt signal sent to the active worker."
1221
- : "No running worker was available to interrupt.");
1303
+ ? t("bridge.interrupt.workerSent")
1304
+ : t("bridge.interrupt.workerIdle"));
1222
1305
  return null;
1223
1306
  }
1224
1307
  case "reset":
@@ -1230,47 +1313,48 @@ async function handleInboundMessage(params) {
1230
1313
  resumeCoordinator.clear();
1231
1314
  await adapter.reset();
1232
1315
  stateStore.appendLog("Worker reset by owner.");
1233
- await queueWechatMessage(message.senderId, "Worker session has been reset.");
1316
+ await queueWechatMessage(message.senderId, t("bridge.reset.done"));
1234
1317
  return null;
1235
1318
  case "confirm": {
1236
1319
  const pending = state.pendingConfirmation;
1237
1320
  if (!pending) {
1238
- await queueWechatMessage(message.senderId, "No pending approval request.");
1321
+ await queueWechatMessage(message.senderId, t("approval.noPending"));
1239
1322
  return null;
1240
1323
  }
1241
- const confirmed = await adapter.resolveApproval("confirm");
1324
+ const confirmed = await adapter.resolveApproval("confirm", pending.requestId);
1242
1325
  if (!confirmed) {
1243
- await queueWechatMessage(message.senderId, "The worker could not apply this approval request.");
1326
+ await queueWechatMessage(message.senderId, t("bridge.approval.applyFailed"));
1244
1327
  return null;
1245
1328
  }
1246
- stateStore.clearPendingConfirmation();
1329
+ if (stateStore.getState().pendingConfirmation?.requestId === pending.requestId) {
1330
+ stateStore.clearPendingConfirmation();
1331
+ }
1247
1332
  stateStore.appendLog(`Approval confirmed: ${pending.commandPreview}`);
1248
- await queueWechatMessage(message.senderId, "Approval confirmed. Continuing...");
1249
- return {
1250
- startedAt: Date.now(),
1251
- inputPreview: pending.commandPreview,
1252
- };
1333
+ await queueWechatMessage(message.senderId, t("approval.confirmed"));
1334
+ return null;
1253
1335
  }
1254
1336
  case "deny": {
1255
1337
  const pending = state.pendingConfirmation;
1256
1338
  if (!pending) {
1257
- await queueWechatMessage(message.senderId, "No pending approval request.");
1339
+ await queueWechatMessage(message.senderId, t("approval.noPending"));
1258
1340
  return null;
1259
1341
  }
1260
- const denied = await adapter.resolveApproval("deny");
1342
+ const denied = await adapter.resolveApproval("deny", pending.requestId);
1261
1343
  if (!denied) {
1262
- await queueWechatMessage(message.senderId, "The worker could not deny this approval request cleanly.");
1344
+ await queueWechatMessage(message.senderId, t("bridge.approval.denyFailed"));
1263
1345
  return null;
1264
1346
  }
1265
- stateStore.clearPendingConfirmation();
1347
+ if (stateStore.getState().pendingConfirmation?.requestId === pending.requestId) {
1348
+ stateStore.clearPendingConfirmation();
1349
+ }
1266
1350
  stateStore.appendLog(`Approval denied: ${pending.commandPreview}`);
1267
- await queueWechatMessage(message.senderId, "Approval denied.");
1351
+ await queueWechatMessage(message.senderId, t("approval.denied"));
1268
1352
  return null;
1269
1353
  }
1270
1354
  case "answer": {
1271
1355
  const pending = state.pendingUserInput;
1272
1356
  if (!pending) {
1273
- await queueWechatMessage(message.senderId, "No pending user input request.");
1357
+ await queueWechatMessage(message.senderId, t("bridge.answer.none"));
1274
1358
  return null;
1275
1359
  }
1276
1360
  const parsed = parsePendingUserInputAnswerCommand(systemCommand.raw, pending);
@@ -1278,18 +1362,16 @@ async function handleInboundMessage(params) {
1278
1362
  await queueWechatMessage(message.senderId, parsed.error);
1279
1363
  return null;
1280
1364
  }
1281
- const submitted = await adapter.submitUserInput(parsed.answers);
1365
+ const submitted = await adapter.submitUserInput(parsed.answers, pending.requestId);
1282
1366
  if (!submitted) {
1283
- await queueWechatMessage(message.senderId, "The worker could not apply this answer.");
1367
+ await queueWechatMessage(message.senderId, t("bridge.answer.failed"));
1284
1368
  return null;
1285
1369
  }
1286
- stateStore.clearPendingUserInput();
1370
+ if (stateStore.getState().pendingUserInput?.requestId === pending.requestId)
1371
+ stateStore.clearPendingUserInput();
1287
1372
  stateStore.appendLog(`User input answered: ${parsed.preview}`);
1288
- await queueWechatMessage(message.senderId, "Answer submitted. Continuing...");
1289
- return {
1290
- startedAt: Date.now(),
1291
- inputPreview: parsed.preview,
1292
- };
1373
+ await queueWechatMessage(message.senderId, t("bridge.answer.sent"));
1374
+ return null;
1293
1375
  }
1294
1376
  }
1295
1377
  const adapterState = adapter.getState();
@@ -1316,16 +1398,16 @@ async function handleInboundMessage(params) {
1316
1398
  const pendingUserInput = stateStore.getState().pendingUserInput;
1317
1399
  await queueWechatMessage(message.senderId, pendingUserInput
1318
1400
  ? formatPendingUserInputReminder(pendingUserInput)
1319
- : `${options.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`);
1401
+ : t("bridge.input.waiting", { adapter: options.adapter }));
1320
1402
  },
1321
1403
  remindBusy: async () => {
1322
1404
  const currentState = adapter.getState();
1323
1405
  if ((options.adapter === "codex" || options.adapter === "opencode" || options.adapter === "pi") &&
1324
1406
  currentState.activeTurnOrigin === "local") {
1325
- await queueWechatMessage(message.senderId, `${options.adapter === "opencode" ? "OpenCode" : options.adapter === "pi" ? "Pi" : "codex"} is currently busy with a local terminal turn. Wait for it to finish or use /stop.`);
1407
+ await queueWechatMessage(message.senderId, t("bridge.busy.local", { adapter: options.adapter === "opencode" ? "OpenCode" : options.adapter === "pi" ? "Pi" : "codex" }));
1326
1408
  return;
1327
1409
  }
1328
- await queueWechatMessage(message.senderId, `${options.adapter} is still working. Wait for the current reply or use /stop.`);
1410
+ await queueWechatMessage(message.senderId, t("bridge.busy.remote", { adapter: options.adapter }));
1329
1411
  },
1330
1412
  defer: async () => {
1331
1413
  await deferInboundMessage(message);
@@ -0,0 +1,56 @@
1
+ import { WecomChannelDriver } from "./wecom/wecom-driver.js";
2
+ import { WechatChannelDriver } from "./wechat/wechat-driver.js";
3
+ import { TelegramChannelDriver } from "./telegram/telegram-driver.js";
4
+ import { LocalChannelDriver } from "./local/local-driver.js";
5
+ /**
6
+ * Single construction point for channel drivers. Adding a channel means
7
+ * adding a driver implementation and one branch here; orchestration layers
8
+ * stay channel-neutral.
9
+ */
10
+ export function createChannelDriver(input) {
11
+ if (input.channelId === "wecom") {
12
+ if (!input.wecomTransport) {
13
+ throw new Error("The wecom channel requires a WeCom transport.");
14
+ }
15
+ return new WecomChannelDriver({
16
+ transport: input.wecomTransport,
17
+ accountId: input.accountId,
18
+ operatorId: input.authorizedUserId,
19
+ logError: input.logError,
20
+ });
21
+ }
22
+ if (input.channelId === "telegram") {
23
+ if (!input.telegramTransport) {
24
+ throw new Error("The telegram channel requires a Telegram transport.");
25
+ }
26
+ return new TelegramChannelDriver({
27
+ transport: input.telegramTransport,
28
+ accountId: input.accountId,
29
+ operatorId: input.authorizedUserId,
30
+ logError: input.logError,
31
+ });
32
+ }
33
+ if (input.channelId === "local") {
34
+ const files = input.localFiles ?? {
35
+ inboxFile: "",
36
+ transcriptFile: "",
37
+ };
38
+ if (!files.inboxFile || !files.transcriptFile) {
39
+ throw new Error("The local channel requires inbox and transcript file paths.");
40
+ }
41
+ return new LocalChannelDriver({
42
+ operatorId: input.authorizedUserId,
43
+ inboxFile: files.inboxFile,
44
+ transcriptFile: files.transcriptFile,
45
+ log: input.log,
46
+ });
47
+ }
48
+ if (!input.wechatTransport) {
49
+ throw new Error("The wechat channel requires a WeChat transport.");
50
+ }
51
+ return new WechatChannelDriver({
52
+ transport: input.wechatTransport,
53
+ logError: input.logError,
54
+ buildInboundPrompt: input.buildWechatInboundPrompt,
55
+ });
56
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Channel boundary for local loopback output. Unlike WeChat there is no
3
+ * plain-text conversion: the harness consumes the raw final reply.
4
+ */
5
+ export class LocalChannelPort {
6
+ channelId = "local";
7
+ deps;
8
+ constructor(deps) {
9
+ this.deps = deps;
10
+ }
11
+ async send(output) {
12
+ if (output.target.channelId !== "local") {
13
+ throw new Error(`Local channel cannot send to ${output.target.channelId}.`);
14
+ }
15
+ const recipientId = output.target.recipientId;
16
+ const prefixText = (text) => this.deps.prefixText?.(output.adapter, text) ?? text;
17
+ const context = typeof output.metadata?.sendContext === "string"
18
+ ? output.metadata.sendContext
19
+ : output.kind;
20
+ if (output.kind === "final_reply") {
21
+ const rawText = output.text ?? "";
22
+ if (!rawText.trim()) {
23
+ this.deps.onEmptyVisibleReply?.(output.adapter, rawText);
24
+ return true;
25
+ }
26
+ const sent = await this.deps.sendText(recipientId, prefixText(rawText), "final_reply");
27
+ if (sent) {
28
+ this.deps.onTextSent?.(output.adapter, rawText);
29
+ }
30
+ return true;
31
+ }
32
+ if (output.attachment) {
33
+ await this.deps.sendAttachment?.(recipientId, output.attachment);
34
+ }
35
+ if (output.text?.trim()) {
36
+ return Boolean(await this.deps.sendText(recipientId, prefixText(output.text), context));
37
+ }
38
+ return true;
39
+ }
40
+ }