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.
- package/README.md +146 -73
- package/README_EN.md +544 -0
- package/bin/telegram-check-update.mjs +10 -0
- package/bin/telegram-claude.mjs +10 -0
- package/bin/telegram-codex.mjs +10 -0
- package/bin/telegram-daemon.mjs +10 -0
- package/bin/telegram-opencode.mjs +10 -0
- package/bin/telegram-pi.mjs +10 -0
- package/bin/telegram-setup.mjs +10 -0
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +532 -99
- package/dist/bridge/bridge-adapters.core.js +84 -7
- package/dist/bridge/bridge-adapters.opencode.js +202 -55
- package/dist/bridge/bridge-adapters.pi.js +23 -18
- package/dist/bridge/bridge-adapters.shared.js +9 -9
- package/dist/bridge/bridge-session-resume.js +13 -6
- package/dist/bridge/bridge-state.js +3 -1
- package/dist/bridge/bridge-utils.js +94 -144
- package/dist/bridge/claude-hooks.js +60 -3
- package/dist/bridge/claude-native-controls.js +19 -12
- package/dist/bridge/language-control.js +33 -0
- package/dist/bridge/native-terminal-control.js +26 -10
- package/dist/bridge/opencode-v2-compat.js +11 -3
- package/dist/bridge/wechat-bridge.js +154 -72
- package/dist/channels/channel-driver-factory.js +56 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/telegram/setup-cli.js +12 -0
- package/dist/channels/telegram/setup.js +163 -0
- package/dist/channels/telegram/telegram-channel-port.js +47 -0
- package/dist/channels/telegram/telegram-config.js +66 -0
- package/dist/channels/telegram/telegram-driver.js +106 -0
- package/dist/channels/telegram/telegram-message.js +41 -0
- package/dist/channels/telegram/telegram-transport.js +378 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- package/dist/companion/local-companion-start.js +180 -61
- package/dist/companion/local-companion.js +27 -7
- package/dist/companion/pi-tui-bridge-extension.js +10 -4
- package/dist/core/bridge-control.js +3 -0
- package/dist/core/bridge-defer.js +3 -2
- package/dist/core/bridge-event-forwarder.js +3 -0
- package/dist/core/channel-types.js +13 -1
- package/dist/daemon/wechat-daemon.js +239 -126
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +245 -1
- package/dist/i18n/messages-zh.js +245 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- package/package.json +17 -5
- package/skills/codex-bridge/SKILL.md +35 -0
- package/skills/codex-bridge/agents/openai.yaml +4 -0
- 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 ||
|
|
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
|
-
:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
:
|
|
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
|
-
:
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
-
|
|
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(
|
|
418
|
-
stateStore.appendLog(
|
|
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,
|
|
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: "
|
|
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,
|
|
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
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
:
|
|
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:
|
|
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,
|
|
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,
|
|
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, "
|
|
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,
|
|
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,
|
|
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
|
-
? "
|
|
1221
|
-
: "
|
|
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, "
|
|
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, "
|
|
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, "
|
|
1326
|
+
await queueWechatMessage(message.senderId, t("bridge.approval.applyFailed"));
|
|
1244
1327
|
return null;
|
|
1245
1328
|
}
|
|
1246
|
-
stateStore.
|
|
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, "
|
|
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, "
|
|
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, "
|
|
1344
|
+
await queueWechatMessage(message.senderId, t("bridge.approval.denyFailed"));
|
|
1263
1345
|
return null;
|
|
1264
1346
|
}
|
|
1265
|
-
stateStore.
|
|
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, "
|
|
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, "
|
|
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, "
|
|
1367
|
+
await queueWechatMessage(message.senderId, t("bridge.answer.failed"));
|
|
1284
1368
|
return null;
|
|
1285
1369
|
}
|
|
1286
|
-
stateStore.
|
|
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, "
|
|
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
|
-
:
|
|
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,
|
|
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,
|
|
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
|
+
}
|