cli-wechat-bridge 1.2.1 → 1.2.2
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 +23 -9
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +207 -83
- 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-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 +57 -42
- package/dist/channels/channel-driver-factory.js +44 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- 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 +5 -1
- package/dist/daemon/wechat-daemon.js +202 -124
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +244 -1
- package/dist/i18n/messages-zh.js +244 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- package/package.json +2 -1
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
7
|
+
import { getLocale, parseLocale, setLocale, t } from "../i18n/index.js";
|
|
8
|
+
import { writeJsonFileAtomic } from "../utils/atomic-file.js";
|
|
9
|
+
let languageChangeChain = Promise.resolve();
|
|
10
|
+
/** Language changes never touch turn ownership or pending interactions. */
|
|
11
|
+
export function handleLanguageControl(params) {
|
|
12
|
+
const run = languageChangeChain.then(() => applyLanguageControl(params));
|
|
13
|
+
languageChangeChain = run.catch(() => undefined);
|
|
14
|
+
return run;
|
|
15
|
+
}
|
|
16
|
+
async function applyLanguageControl(params) {
|
|
17
|
+
if (!params.target)
|
|
18
|
+
return t("language.current", { locale: getLocale() });
|
|
19
|
+
const locale = parseLocale(params.target);
|
|
20
|
+
if (!locale)
|
|
21
|
+
return t("language.usage");
|
|
22
|
+
try {
|
|
23
|
+
writeJsonFileAtomic(params.settingsFile, { locale });
|
|
24
|
+
}
|
|
25
|
+
catch (error) {
|
|
26
|
+
return t("language.saveFailed", { error: error instanceof Error ? error.message : String(error) });
|
|
27
|
+
}
|
|
28
|
+
setLocale(locale);
|
|
29
|
+
process.env.CLI_BRIDGE_LANG = locale;
|
|
30
|
+
const results = await Promise.allSettled((params.adapters ?? []).map((adapter) => adapter.updateLocale?.(locale) ?? true));
|
|
31
|
+
const needsRestart = results.some((result) => result.status === "rejected" || result.value === false);
|
|
32
|
+
return t("language.changed", { locale }) + (needsRestart ? `\n${t("language.companionRestart")}` : "");
|
|
33
|
+
}
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
6
|
// LICENSE.txt. Network services built on it must offer source to users.
|
|
7
7
|
import xterm from "@xterm/headless";
|
|
8
|
+
import { t } from "../i18n/index.js";
|
|
8
9
|
const { Terminal } = xterm;
|
|
9
10
|
const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
10
11
|
/** A screen mirror, never a second input source: only explicit control operations write keys. */
|
|
@@ -117,20 +118,20 @@ export class NativeTerminalControl {
|
|
|
117
118
|
check() {
|
|
118
119
|
const operation = this.operation;
|
|
119
120
|
if (!operation)
|
|
120
|
-
throw new Error("
|
|
121
|
+
throw new Error(t("native.control.inactive"));
|
|
121
122
|
if (operation.error)
|
|
122
123
|
throw operation.error;
|
|
123
124
|
if (operation.identity !== this.options.identity()) {
|
|
124
|
-
operation.error = new Error("
|
|
125
|
+
operation.error = new Error(t("native.control.sessionChanged"));
|
|
125
126
|
throw operation.error;
|
|
126
127
|
}
|
|
127
128
|
this.options.assertReady();
|
|
128
129
|
if (Date.now() > operation.deadline)
|
|
129
|
-
throw new Error("
|
|
130
|
+
throw new Error(t("native.control.timeout"));
|
|
130
131
|
}
|
|
131
132
|
async run(action) {
|
|
132
133
|
if (this.operation)
|
|
133
|
-
throw new Error("
|
|
134
|
+
throw new Error(t("native.control.busy"));
|
|
134
135
|
this.options.assertReady();
|
|
135
136
|
this.operation = { identity: this.options.identity(), deadline: Date.now() + 25_000 };
|
|
136
137
|
try {
|
|
@@ -143,7 +144,7 @@ export class NativeTerminalControl {
|
|
|
143
144
|
else if (this.draft && this.options.isPromptEmpty?.())
|
|
144
145
|
this.draft = false;
|
|
145
146
|
if (this.draft)
|
|
146
|
-
throw new Error("
|
|
147
|
+
throw new Error(t("native.control.draft"));
|
|
147
148
|
const result = await action();
|
|
148
149
|
this.check();
|
|
149
150
|
return result;
|
|
@@ -183,7 +184,7 @@ export class NativeTerminalControl {
|
|
|
183
184
|
}
|
|
184
185
|
async command(command) {
|
|
185
186
|
if (!/^\/[a-z]+$/.test(command))
|
|
186
|
-
throw new Error("
|
|
187
|
+
throw new Error(t("native.control.invalid"));
|
|
187
188
|
await this.key(`\u001b[200~${command}\u001b[201~`);
|
|
188
189
|
await this.key("\r");
|
|
189
190
|
// Claude can consume the first Enter to accept a slash completion while its
|
|
@@ -211,7 +212,7 @@ export class NativeTerminalControl {
|
|
|
211
212
|
}
|
|
212
213
|
async openMenu(command, title) {
|
|
213
214
|
if (this.ownedMenu || /Select model|Select agent|Select variant/.test(this.text)) {
|
|
214
|
-
throw new Error("
|
|
215
|
+
throw new Error(t("native.control.menuOpen"));
|
|
215
216
|
}
|
|
216
217
|
await this.command(command);
|
|
217
218
|
await this.waitFor(() => this.hasMenu(title));
|
|
@@ -220,7 +221,7 @@ export class NativeTerminalControl {
|
|
|
220
221
|
async closeMenu() {
|
|
221
222
|
const title = this.ownedMenu;
|
|
222
223
|
if (!title || !this.hasMenu(title))
|
|
223
|
-
throw new Error("
|
|
224
|
+
throw new Error(t("native.control.selectorChanged"));
|
|
224
225
|
await this.key("\u001b");
|
|
225
226
|
await this.waitFor(() => !this.hasMenu(title));
|
|
226
227
|
this.ownedMenu = null;
|
|
@@ -228,7 +229,7 @@ export class NativeTerminalControl {
|
|
|
228
229
|
async submitMenu(nextTitle, key = "\r") {
|
|
229
230
|
const title = this.ownedMenu;
|
|
230
231
|
if (!title || !this.hasMenu(title))
|
|
231
|
-
throw new Error("
|
|
232
|
+
throw new Error(t("native.control.selectorChanged"));
|
|
232
233
|
await this.key(key);
|
|
233
234
|
await this.waitFor(() => !this.hasMenu(title));
|
|
234
235
|
this.ownedMenu = nextTitle && this.hasMenu(nextTitle) ? nextTitle : null;
|
|
@@ -260,7 +261,7 @@ export function parseClaudeModelSwitchConfirmation(text) {
|
|
|
260
261
|
export function readClaudePermissionMode(text) {
|
|
261
262
|
const lines = text.split("\n");
|
|
262
263
|
const prompt = lines.findLastIndex((line) => /^\s*❯/.test(line));
|
|
263
|
-
if (prompt < 0 ||
|
|
264
|
+
if (prompt < 0 || !isClaudePromptVisuallyEmpty(lines[prompt]))
|
|
264
265
|
return null;
|
|
265
266
|
const footer = lines.slice(prompt + 1).join("\n");
|
|
266
267
|
for (const [label, mode] of [["plan mode", "plan"], ["accept edits", "acceptEdits"], ["bypass permissions", "bypassPermissions"], ["auto", "auto"], ["don't ask", "dontAsk"]]) {
|
|
@@ -269,3 +270,18 @@ export function readClaudePermissionMode(text) {
|
|
|
269
270
|
}
|
|
270
271
|
return "default";
|
|
271
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Claude renders idle decorations inside the input box: an automatic context
|
|
275
|
+
* chip like `[⧉ In file.ts]` and a suggestion placeholder like
|
|
276
|
+
* Try "how does <filepath> work?". Both appear on a freshly started session
|
|
277
|
+
* before any user input; neither is a draft. Real typed input still fails
|
|
278
|
+
* this check (and the local-input mirror blocks controls independently).
|
|
279
|
+
*/
|
|
280
|
+
function isClaudePromptVisuallyEmpty(line) {
|
|
281
|
+
const body = line
|
|
282
|
+
.replace(/^\s*❯\s*/, "")
|
|
283
|
+
.replace(/^\[⧉ [^\]]*\]\s*/, "")
|
|
284
|
+
.replace(/^Try "[^"]*"\s*$/, "")
|
|
285
|
+
.replace(/\s+$/, "");
|
|
286
|
+
return body.length === 0;
|
|
287
|
+
}
|
|
@@ -110,9 +110,14 @@ function normalizeFormQuestions(form) {
|
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
112
|
function normalizeFormAnswer(field, values) {
|
|
113
|
+
const options = Array.isArray(field.options) ? field.options.filter(isRecord) : [];
|
|
114
|
+
const normalizedValues = values.map((value) => {
|
|
115
|
+
const option = options.find((option) => option.label === value);
|
|
116
|
+
return typeof option?.value === "string" ? option.value : value;
|
|
117
|
+
});
|
|
113
118
|
if (field.type === "multiselect")
|
|
114
|
-
return
|
|
115
|
-
const value =
|
|
119
|
+
return normalizedValues;
|
|
120
|
+
const value = normalizedValues[0] ?? "";
|
|
116
121
|
if (field.type === "boolean") {
|
|
117
122
|
return /^(1|true|yes|y)$/i.test(String(value));
|
|
118
123
|
}
|
|
@@ -407,7 +412,10 @@ export async function createOpenCodeV2CompatClient(options) {
|
|
|
407
412
|
sessionID: input.sessionID ?? permissionSessions.get(String(input.requestID)),
|
|
408
413
|
requestID: input.requestID,
|
|
409
414
|
decision: input.reply === "reject" ? "reject" : input.reply,
|
|
410
|
-
}, requestOptions).
|
|
415
|
+
}, requestOptions).then((response) => {
|
|
416
|
+
permissionSessions.delete(String(input.requestID));
|
|
417
|
+
return response;
|
|
418
|
+
})),
|
|
411
419
|
},
|
|
412
420
|
question: {
|
|
413
421
|
reply: (input, requestOptions) => result(async () => {
|
|
@@ -22,6 +22,8 @@ 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";
|
|
@@ -706,7 +708,7 @@ async function main() {
|
|
|
706
708
|
task: nextActiveTask,
|
|
707
709
|
conversation: channelMessage.conversation,
|
|
708
710
|
onBusy: async () => {
|
|
709
|
-
await queueInboundReply(message.senderId,
|
|
711
|
+
await queueInboundReply(message.senderId, t("bridge.busy.remote", { adapter: options.adapter }));
|
|
710
712
|
},
|
|
711
713
|
forward: async () => {
|
|
712
714
|
await dispatchInboundWechatText({
|
|
@@ -752,7 +754,7 @@ async function main() {
|
|
|
752
754
|
if (chatType === "direct") {
|
|
753
755
|
const result = await channelDriver.sendText({
|
|
754
756
|
target: channelDriver.directConversation(senderId),
|
|
755
|
-
text: "
|
|
757
|
+
text: t("channel.unauthorized"),
|
|
756
758
|
context: "notice",
|
|
757
759
|
log: (entry) => stateStore.appendLog(entry),
|
|
758
760
|
});
|
|
@@ -884,7 +886,7 @@ async function main() {
|
|
|
884
886
|
const result = await turns.dispatch({
|
|
885
887
|
task: nextActiveTask,
|
|
886
888
|
onBusy: async () => {
|
|
887
|
-
await queueWechatMessage(message.senderId,
|
|
889
|
+
await queueWechatMessage(message.senderId, t("bridge.busy.remote", { adapter: options.adapter }));
|
|
888
890
|
},
|
|
889
891
|
forward: async () => {
|
|
890
892
|
await dispatchInboundWechatText({
|
|
@@ -954,7 +956,7 @@ function syncSharedSessionState(stateStore, adapter) {
|
|
|
954
956
|
}
|
|
955
957
|
}
|
|
956
958
|
}
|
|
957
|
-
function wireAdapterEvents(params) {
|
|
959
|
+
export function wireAdapterEvents(params) {
|
|
958
960
|
const { adapter, channelDriver, resumeCoordinator, options, transport, stateStore, outputBatcher, queueWechatAttachmentAction, queueWechatMessage, trackWechatForwardTask, maybeDrainDeferredInboundMessages, getActiveTask, clearActiveTask, syncSharedSessionState, syncLocalClientEndpoint, requestShutdown, wecomTransport, getWecomTarget, } = params;
|
|
959
961
|
const channelPort = options.channelId === "wecom"
|
|
960
962
|
? new WecomChannelPort({
|
|
@@ -984,10 +986,12 @@ function wireAdapterEvents(params) {
|
|
|
984
986
|
syncLocalClientEndpoint();
|
|
985
987
|
const adapterState = adapter.getState();
|
|
986
988
|
const bridgeState = stateStore.getState();
|
|
987
|
-
if (bridgeState.pendingConfirmation && !adapterState.pendingApproval
|
|
989
|
+
if (bridgeState.pendingConfirmation && (!adapterState.pendingApproval
|
|
990
|
+
|| (adapterState.pendingApprovals !== undefined && bridgeState.pendingConfirmation.requestId !== adapterState.pendingApproval.requestId))) {
|
|
988
991
|
stateStore.clearPendingConfirmation();
|
|
989
992
|
}
|
|
990
|
-
if (bridgeState.pendingUserInput && !adapterState.pendingUserInput
|
|
993
|
+
if (bridgeState.pendingUserInput && (!adapterState.pendingUserInput
|
|
994
|
+
|| (bridgeState.pendingUserInput.requestId !== undefined && bridgeState.pendingUserInput.requestId !== adapterState.pendingUserInput.requestId))) {
|
|
991
995
|
stateStore.clearPendingUserInput();
|
|
992
996
|
}
|
|
993
997
|
const authorizedUserId = stateStore.getState().authorizedUserId;
|
|
@@ -1050,14 +1054,21 @@ function wireAdapterEvents(params) {
|
|
|
1050
1054
|
},
|
|
1051
1055
|
approvalRequired: (next) => {
|
|
1052
1056
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1057
|
+
const liveState = adapter.getState();
|
|
1058
|
+
if (liveState.pendingApprovals !== undefined && liveState.pendingApproval?.requestId !== next.request.requestId)
|
|
1059
|
+
return;
|
|
1053
1060
|
const pending = toPendingApproval(next.request);
|
|
1054
|
-
stateStore.setPendingConfirmation(pending);
|
|
1055
1061
|
stateStore.appendLog(`Approval requested (${pending.source}): ${pending.commandPreview}`);
|
|
1056
|
-
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending,
|
|
1062
|
+
await queueWechatMessage(authorizedUserId, formatApprovalMessage(pending, liveState), "approval_required", eventTarget);
|
|
1063
|
+
if (adapter.getState().pendingApproval?.requestId === pending.requestId) {
|
|
1064
|
+
stateStore.setPendingConfirmation(pending);
|
|
1065
|
+
}
|
|
1057
1066
|
}));
|
|
1058
1067
|
},
|
|
1059
1068
|
userInputRequired: (next) => {
|
|
1060
1069
|
trackWechatForwardTask(outputBatcher.flushNow().then(async () => {
|
|
1070
|
+
if (next.request.requestId !== undefined && adapter.getState().pendingUserInput?.requestId !== next.request.requestId)
|
|
1071
|
+
return;
|
|
1061
1072
|
const pending = toPendingUserInput(next.request);
|
|
1062
1073
|
stateStore.setPendingUserInput(pending);
|
|
1063
1074
|
stateStore.appendLog(`User input requested: questions=${pending.questions.length}`);
|
|
@@ -1155,12 +1166,12 @@ function formatInboundMessagePreview(message) {
|
|
|
1155
1166
|
}
|
|
1156
1167
|
return "(empty)";
|
|
1157
1168
|
}
|
|
1158
|
-
async function handleInboundMessage(params) {
|
|
1169
|
+
export async function handleInboundMessage(params) {
|
|
1159
1170
|
const { message, channelMessage, channelDriver, } = params;
|
|
1160
1171
|
const { options, stateStore, adapter, resumeCoordinator, queueWechatMessage, outputBatcher, clearActiveTask, dispatchInboundText, deferInboundMessage, } = params;
|
|
1161
1172
|
const state = stateStore.getState();
|
|
1162
1173
|
if (message.senderId !== state.authorizedUserId) {
|
|
1163
|
-
await queueWechatMessage(message.senderId,
|
|
1174
|
+
await queueWechatMessage(message.senderId, t("bridge.unauthorized", { mode: t("bridge.mode.bridge"), operator: channelDriver.operatorDescription }));
|
|
1164
1175
|
return null;
|
|
1165
1176
|
}
|
|
1166
1177
|
const systemCommand = parseWechatControlCommand(message.text, {
|
|
@@ -1169,19 +1180,24 @@ async function handleInboundMessage(params) {
|
|
|
1169
1180
|
hasPendingUserInput: Boolean(state.pendingUserInput),
|
|
1170
1181
|
});
|
|
1171
1182
|
switch (systemCommand?.type) {
|
|
1183
|
+
case "language":
|
|
1184
|
+
await queueWechatMessage(message.senderId, await handleLanguageControl({
|
|
1185
|
+
target: systemCommand.target, settingsFile: params.localeSettingsFile ?? LANGUAGE_FILE, adapters: [adapter],
|
|
1186
|
+
}));
|
|
1187
|
+
return null;
|
|
1172
1188
|
case "model":
|
|
1173
1189
|
case "plan":
|
|
1174
1190
|
await queueWechatMessage(message.senderId, await handleAdapterControl(adapter, message.senderId, systemCommand));
|
|
1175
1191
|
return null;
|
|
1176
1192
|
case "broadcast":
|
|
1177
|
-
await queueWechatMessage(message.senderId, "
|
|
1193
|
+
await queueWechatMessage(message.senderId, t("bridge.broadcast.daemonOnly"));
|
|
1178
1194
|
return null;
|
|
1179
1195
|
case "status":
|
|
1180
1196
|
await queueWechatMessage(message.senderId, formatStatusReport(stateStore.getState(), adapter.getState()));
|
|
1181
1197
|
return null;
|
|
1182
1198
|
case "resume": {
|
|
1183
1199
|
if (!isWechatResumeEnabled(options.adapter)) {
|
|
1184
|
-
await queueWechatMessage(message.senderId,
|
|
1200
|
+
await queueWechatMessage(message.senderId, t("bridge.resume.disabled", { channel: channelDriver.displayName, adapter: options.adapter, command: `${channelDriver.id}-${options.adapter}` }));
|
|
1185
1201
|
return null;
|
|
1186
1202
|
}
|
|
1187
1203
|
try {
|
|
@@ -1201,7 +1217,7 @@ async function handleInboundMessage(params) {
|
|
|
1201
1217
|
}
|
|
1202
1218
|
case "new_session": {
|
|
1203
1219
|
if (!adapter.createSession) {
|
|
1204
|
-
await queueWechatMessage(message.senderId,
|
|
1220
|
+
await queueWechatMessage(message.senderId, t("bridge.newSession.unavailable", { adapter: options.adapter }));
|
|
1205
1221
|
return null;
|
|
1206
1222
|
}
|
|
1207
1223
|
await outputBatcher.flushNow();
|
|
@@ -1217,8 +1233,8 @@ async function handleInboundMessage(params) {
|
|
|
1217
1233
|
case "stop": {
|
|
1218
1234
|
const interrupted = await adapter.interrupt();
|
|
1219
1235
|
await queueWechatMessage(message.senderId, interrupted
|
|
1220
|
-
? "
|
|
1221
|
-
: "
|
|
1236
|
+
? t("bridge.interrupt.workerSent")
|
|
1237
|
+
: t("bridge.interrupt.workerIdle"));
|
|
1222
1238
|
return null;
|
|
1223
1239
|
}
|
|
1224
1240
|
case "reset":
|
|
@@ -1230,47 +1246,48 @@ async function handleInboundMessage(params) {
|
|
|
1230
1246
|
resumeCoordinator.clear();
|
|
1231
1247
|
await adapter.reset();
|
|
1232
1248
|
stateStore.appendLog("Worker reset by owner.");
|
|
1233
|
-
await queueWechatMessage(message.senderId, "
|
|
1249
|
+
await queueWechatMessage(message.senderId, t("bridge.reset.done"));
|
|
1234
1250
|
return null;
|
|
1235
1251
|
case "confirm": {
|
|
1236
1252
|
const pending = state.pendingConfirmation;
|
|
1237
1253
|
if (!pending) {
|
|
1238
|
-
await queueWechatMessage(message.senderId, "
|
|
1254
|
+
await queueWechatMessage(message.senderId, t("approval.noPending"));
|
|
1239
1255
|
return null;
|
|
1240
1256
|
}
|
|
1241
|
-
const confirmed = await adapter.resolveApproval("confirm");
|
|
1257
|
+
const confirmed = await adapter.resolveApproval("confirm", pending.requestId);
|
|
1242
1258
|
if (!confirmed) {
|
|
1243
|
-
await queueWechatMessage(message.senderId, "
|
|
1259
|
+
await queueWechatMessage(message.senderId, t("bridge.approval.applyFailed"));
|
|
1244
1260
|
return null;
|
|
1245
1261
|
}
|
|
1246
|
-
stateStore.
|
|
1262
|
+
if (stateStore.getState().pendingConfirmation?.requestId === pending.requestId) {
|
|
1263
|
+
stateStore.clearPendingConfirmation();
|
|
1264
|
+
}
|
|
1247
1265
|
stateStore.appendLog(`Approval confirmed: ${pending.commandPreview}`);
|
|
1248
|
-
await queueWechatMessage(message.senderId, "
|
|
1249
|
-
return
|
|
1250
|
-
startedAt: Date.now(),
|
|
1251
|
-
inputPreview: pending.commandPreview,
|
|
1252
|
-
};
|
|
1266
|
+
await queueWechatMessage(message.senderId, t("approval.confirmed"));
|
|
1267
|
+
return null;
|
|
1253
1268
|
}
|
|
1254
1269
|
case "deny": {
|
|
1255
1270
|
const pending = state.pendingConfirmation;
|
|
1256
1271
|
if (!pending) {
|
|
1257
|
-
await queueWechatMessage(message.senderId, "
|
|
1272
|
+
await queueWechatMessage(message.senderId, t("approval.noPending"));
|
|
1258
1273
|
return null;
|
|
1259
1274
|
}
|
|
1260
|
-
const denied = await adapter.resolveApproval("deny");
|
|
1275
|
+
const denied = await adapter.resolveApproval("deny", pending.requestId);
|
|
1261
1276
|
if (!denied) {
|
|
1262
|
-
await queueWechatMessage(message.senderId, "
|
|
1277
|
+
await queueWechatMessage(message.senderId, t("bridge.approval.denyFailed"));
|
|
1263
1278
|
return null;
|
|
1264
1279
|
}
|
|
1265
|
-
stateStore.
|
|
1280
|
+
if (stateStore.getState().pendingConfirmation?.requestId === pending.requestId) {
|
|
1281
|
+
stateStore.clearPendingConfirmation();
|
|
1282
|
+
}
|
|
1266
1283
|
stateStore.appendLog(`Approval denied: ${pending.commandPreview}`);
|
|
1267
|
-
await queueWechatMessage(message.senderId, "
|
|
1284
|
+
await queueWechatMessage(message.senderId, t("approval.denied"));
|
|
1268
1285
|
return null;
|
|
1269
1286
|
}
|
|
1270
1287
|
case "answer": {
|
|
1271
1288
|
const pending = state.pendingUserInput;
|
|
1272
1289
|
if (!pending) {
|
|
1273
|
-
await queueWechatMessage(message.senderId, "
|
|
1290
|
+
await queueWechatMessage(message.senderId, t("bridge.answer.none"));
|
|
1274
1291
|
return null;
|
|
1275
1292
|
}
|
|
1276
1293
|
const parsed = parsePendingUserInputAnswerCommand(systemCommand.raw, pending);
|
|
@@ -1278,18 +1295,16 @@ async function handleInboundMessage(params) {
|
|
|
1278
1295
|
await queueWechatMessage(message.senderId, parsed.error);
|
|
1279
1296
|
return null;
|
|
1280
1297
|
}
|
|
1281
|
-
const submitted = await adapter.submitUserInput(parsed.answers);
|
|
1298
|
+
const submitted = await adapter.submitUserInput(parsed.answers, pending.requestId);
|
|
1282
1299
|
if (!submitted) {
|
|
1283
|
-
await queueWechatMessage(message.senderId, "
|
|
1300
|
+
await queueWechatMessage(message.senderId, t("bridge.answer.failed"));
|
|
1284
1301
|
return null;
|
|
1285
1302
|
}
|
|
1286
|
-
stateStore.
|
|
1303
|
+
if (stateStore.getState().pendingUserInput?.requestId === pending.requestId)
|
|
1304
|
+
stateStore.clearPendingUserInput();
|
|
1287
1305
|
stateStore.appendLog(`User input answered: ${parsed.preview}`);
|
|
1288
|
-
await queueWechatMessage(message.senderId, "
|
|
1289
|
-
return
|
|
1290
|
-
startedAt: Date.now(),
|
|
1291
|
-
inputPreview: parsed.preview,
|
|
1292
|
-
};
|
|
1306
|
+
await queueWechatMessage(message.senderId, t("bridge.answer.sent"));
|
|
1307
|
+
return null;
|
|
1293
1308
|
}
|
|
1294
1309
|
}
|
|
1295
1310
|
const adapterState = adapter.getState();
|
|
@@ -1316,16 +1331,16 @@ async function handleInboundMessage(params) {
|
|
|
1316
1331
|
const pendingUserInput = stateStore.getState().pendingUserInput;
|
|
1317
1332
|
await queueWechatMessage(message.senderId, pendingUserInput
|
|
1318
1333
|
? formatPendingUserInputReminder(pendingUserInput)
|
|
1319
|
-
:
|
|
1334
|
+
: t("bridge.input.waiting", { adapter: options.adapter }));
|
|
1320
1335
|
},
|
|
1321
1336
|
remindBusy: async () => {
|
|
1322
1337
|
const currentState = adapter.getState();
|
|
1323
1338
|
if ((options.adapter === "codex" || options.adapter === "opencode" || options.adapter === "pi") &&
|
|
1324
1339
|
currentState.activeTurnOrigin === "local") {
|
|
1325
|
-
await queueWechatMessage(message.senderId,
|
|
1340
|
+
await queueWechatMessage(message.senderId, t("bridge.busy.local", { adapter: options.adapter === "opencode" ? "OpenCode" : options.adapter === "pi" ? "Pi" : "codex" }));
|
|
1326
1341
|
return;
|
|
1327
1342
|
}
|
|
1328
|
-
await queueWechatMessage(message.senderId,
|
|
1343
|
+
await queueWechatMessage(message.senderId, t("bridge.busy.remote", { adapter: options.adapter }));
|
|
1329
1344
|
},
|
|
1330
1345
|
defer: async () => {
|
|
1331
1346
|
await deferInboundMessage(message);
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { WecomChannelDriver } from "./wecom/wecom-driver.js";
|
|
2
|
+
import { WechatChannelDriver } from "./wechat/wechat-driver.js";
|
|
3
|
+
import { LocalChannelDriver } from "./local/local-driver.js";
|
|
4
|
+
/**
|
|
5
|
+
* Single construction point for channel drivers. Adding a channel means
|
|
6
|
+
* adding a driver implementation and one branch here; orchestration layers
|
|
7
|
+
* stay channel-neutral.
|
|
8
|
+
*/
|
|
9
|
+
export function createChannelDriver(input) {
|
|
10
|
+
if (input.channelId === "wecom") {
|
|
11
|
+
if (!input.wecomTransport) {
|
|
12
|
+
throw new Error("The wecom channel requires a WeCom transport.");
|
|
13
|
+
}
|
|
14
|
+
return new WecomChannelDriver({
|
|
15
|
+
transport: input.wecomTransport,
|
|
16
|
+
accountId: input.accountId,
|
|
17
|
+
operatorId: input.authorizedUserId,
|
|
18
|
+
logError: input.logError,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
if (input.channelId === "local") {
|
|
22
|
+
const files = input.localFiles ?? {
|
|
23
|
+
inboxFile: "",
|
|
24
|
+
transcriptFile: "",
|
|
25
|
+
};
|
|
26
|
+
if (!files.inboxFile || !files.transcriptFile) {
|
|
27
|
+
throw new Error("The local channel requires inbox and transcript file paths.");
|
|
28
|
+
}
|
|
29
|
+
return new LocalChannelDriver({
|
|
30
|
+
operatorId: input.authorizedUserId,
|
|
31
|
+
inboxFile: files.inboxFile,
|
|
32
|
+
transcriptFile: files.transcriptFile,
|
|
33
|
+
log: input.log,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
if (!input.wechatTransport) {
|
|
37
|
+
throw new Error("The wechat channel requires a WeChat transport.");
|
|
38
|
+
}
|
|
39
|
+
return new WechatChannelDriver({
|
|
40
|
+
transport: input.wechatTransport,
|
|
41
|
+
logError: input.logError,
|
|
42
|
+
buildInboundPrompt: input.buildWechatInboundPrompt,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
@@ -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
|
+
}
|