@unlinearity/cli-wechat-bridge 1.1.3 → 1.1.5
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 +110 -140
- package/bin/wechat-claude.mjs +1 -1
- package/bin/wechat-codex.mjs +1 -1
- package/bin/wechat-opencode.mjs +1 -1
- package/bin/wechat-pi.mjs +1 -1
- package/dist/bridge/bridge-adapters.claude.js +448 -11
- package/dist/bridge/bridge-adapters.codex.js +262 -11
- package/dist/bridge/bridge-adapters.core.js +76 -4
- package/dist/bridge/bridge-adapters.js +0 -3
- package/dist/bridge/bridge-adapters.opencode.js +497 -49
- package/dist/bridge/bridge-adapters.pi.js +44 -8
- package/dist/bridge/bridge-adapters.shared.js +13 -100
- package/dist/bridge/bridge-controller.js +3 -0
- package/dist/bridge/bridge-final-reply.js +17 -2
- package/dist/bridge/bridge-process-reaper.js +14 -4
- package/dist/bridge/bridge-session-resume.js +151 -0
- package/dist/bridge/bridge-state.js +41 -6
- package/dist/bridge/bridge-utils.js +63 -229
- package/dist/bridge/claude-hooks.js +1 -0
- package/dist/bridge/wechat-bridge.js +93 -140
- package/dist/bridge/wechat-forwarding.js +58 -0
- package/dist/companion/codex-remote-client.js +309 -36
- package/dist/companion/codex-visible-client-link.js +122 -0
- package/dist/companion/local-companion-link.js +33 -2
- package/dist/companion/local-companion-start.js +44 -13
- package/dist/companion/local-companion.js +6 -3
- package/dist/companion/opencode-tui-bridge-plugin.js +73 -0
- package/dist/companion/pi-tui-bridge-extension.js +42 -3
- package/dist/daemon/daemon-link.js +25 -3
- package/dist/daemon/wechat-daemon.js +168 -56
- package/dist/i18n/index.js +0 -3
- package/dist/i18n/messages-en.js +2 -3
- package/dist/i18n/messages-zh.js +2 -3
- package/dist/utils/atomic-file.js +15 -0
- package/dist/utils/doctor.js +23 -11
- package/dist/wechat/channel-config.js +0 -2
- package/dist/wechat/setup.js +5 -11
- package/dist/wechat/wechat-transport.js +14 -8
- package/package.json +5 -12
- package/scripts/ensure-node-pty-permissions.mjs +73 -0
- package/bin/wechat-bridge-claude.mjs +0 -5
- package/bin/wechat-bridge-codex.mjs +0 -5
- package/bin/wechat-bridge-opencode.mjs +0 -5
- package/bin/wechat-bridge-pi.mjs +0 -5
- package/bin/wechat-bridge-shell.mjs +0 -5
- package/bin/wechat-bridge.mjs +0 -5
- package/bin/wechat-claude-start.mjs +0 -5
- package/bin/wechat-codex-start.mjs +0 -5
- package/bin/wechat-opencode-start.mjs +0 -5
- package/bin/wechat-pi-start.mjs +0 -5
- package/dist/bridge/bridge-adapters.shell.js +0 -363
|
@@ -12,7 +12,8 @@ import { forwardWechatFinalReply } from "../bridge/bridge-final-reply.js";
|
|
|
12
12
|
import { readBridgeLockFile, } from "../bridge/bridge-state.js";
|
|
13
13
|
import { getProcessRecordByPid, isWechatDaemonCommandLine, killProcessTreeSync, listWechatDaemonProcesses, reapOrphanedOpencodeProcesses, reapPeerBridgeProcesses, } from "../bridge/bridge-process-reaper.js";
|
|
14
14
|
import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, formatDuration, formatMirroredUserInputMessage, formatPendingApprovalReminder, formatPendingUserInputReminder, formatSessionSwitchMessage, formatTaskFailedMessage, formatUserInputRequestMessage, MESSAGE_START_GRACE_MS, nowIso, OutputBatcher, parsePendingUserInputAnswerCommand, parseWechatControlCommand, truncatePreview, } from "../bridge/bridge-utils.js";
|
|
15
|
-
import {
|
|
15
|
+
import { ResumeSessionCoordinator, isWechatResumeEnabled, shouldForwardSessionSwitchEvent, } from "../bridge/bridge-session-resume.js";
|
|
16
|
+
import { WECHAT_SEND_MAX_ATTEMPTS, computeWechatSendRetryDelayMs, formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../bridge/wechat-forwarding.js";
|
|
16
17
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
17
18
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
18
19
|
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
@@ -26,8 +27,6 @@ const RUNTIME_ENTRY_EXTENSION = path.extname(MODULE_FILE) === ".ts" ? ".ts" : ".
|
|
|
26
27
|
const DAEMON_HOST = "127.0.0.1";
|
|
27
28
|
const POLL_RETRY_BASE_MS = 1_000;
|
|
28
29
|
const POLL_RETRY_MAX_MS = 30_000;
|
|
29
|
-
const WECHAT_SEND_MAX_ATTEMPTS = 3;
|
|
30
|
-
const WECHAT_SEND_RETRY_BASE_MS = 750;
|
|
31
30
|
const SINGLE_BRIDGE_STOP_TIMEOUT_MS = 10_000;
|
|
32
31
|
const SINGLE_BRIDGE_FORCE_STOP_TIMEOUT_MS = 3_000;
|
|
33
32
|
const SINGLE_BRIDGE_STOP_POLL_MS = 250;
|
|
@@ -52,9 +51,6 @@ function computePollRetryDelayMs(consecutiveFailures) {
|
|
|
52
51
|
const exponent = Math.min(normalizedFailures - 1, 5);
|
|
53
52
|
return Math.min(POLL_RETRY_MAX_MS, POLL_RETRY_BASE_MS * 2 ** exponent);
|
|
54
53
|
}
|
|
55
|
-
function computeWechatSendRetryDelayMs(attempt) {
|
|
56
|
-
return WECHAT_SEND_RETRY_BASE_MS * attempt;
|
|
57
|
-
}
|
|
58
54
|
function isDaemonAdapterKind(value) {
|
|
59
55
|
return value === "codex" || value === "claude" || value === "opencode" || value === "pi";
|
|
60
56
|
}
|
|
@@ -155,12 +151,16 @@ export function defaultDaemonSessionStartMode(adapter) {
|
|
|
155
151
|
return adapter === "codex" ? "restore" : "new";
|
|
156
152
|
}
|
|
157
153
|
export function resolveDaemonSessionStartMode(params) {
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
154
|
+
// An explicit --session-start-mode wins over visible-client reuse: the user
|
|
155
|
+
// asked for a fresh (or restored) session on purpose, so silently degrading
|
|
156
|
+
// "new" back to "restore" because a window happens to be connected would
|
|
157
|
+
// ignore the request.
|
|
161
158
|
if (params.explicitSessionStartMode) {
|
|
162
159
|
return params.explicitSessionStartMode;
|
|
163
160
|
}
|
|
161
|
+
if (params.reuseExistingVisible && params.visibleConnected) {
|
|
162
|
+
return "restore";
|
|
163
|
+
}
|
|
164
164
|
if (params.adapter === "codex") {
|
|
165
165
|
return "restore";
|
|
166
166
|
}
|
|
@@ -483,6 +483,8 @@ class WechatDaemon {
|
|
|
483
483
|
authorizedUserId;
|
|
484
484
|
transport;
|
|
485
485
|
slots = new Map();
|
|
486
|
+
// Per-adapter serialization chains for ensureSlot (see ensureSlot comment).
|
|
487
|
+
slotEnsureChains = new Map();
|
|
486
488
|
startedAt = new Date().toISOString();
|
|
487
489
|
bridgeStartedAtMs = Date.now();
|
|
488
490
|
backlogNoticeSent = false;
|
|
@@ -638,14 +640,12 @@ class WechatDaemon {
|
|
|
638
640
|
}
|
|
639
641
|
catch (error) {
|
|
640
642
|
const errorText = error instanceof Error ? error.message : String(error);
|
|
641
|
-
const isUserFacingShellRejection = error instanceof Error && error.name === "ShellCommandRejectedError";
|
|
642
643
|
logError(errorText);
|
|
643
|
-
appendDaemonLog(
|
|
644
|
+
appendDaemonLog(`inbound_error: ${errorText}`);
|
|
644
645
|
await this.queueWechatMessage(message.senderId, formatUserFacingInboundError({
|
|
645
646
|
adapter: this.activeAdapter ?? "codex",
|
|
646
647
|
cwd: this.cwd,
|
|
647
648
|
errorText,
|
|
648
|
-
isUserFacingShellRejection,
|
|
649
649
|
}), "inbound_error");
|
|
650
650
|
}
|
|
651
651
|
}
|
|
@@ -657,6 +657,32 @@ class WechatDaemon {
|
|
|
657
657
|
}
|
|
658
658
|
await this.shutdownPromise;
|
|
659
659
|
}
|
|
660
|
+
/**
|
|
661
|
+
* Remove a slot whose runtime reported a fatal (terminal) error. The dead
|
|
662
|
+
* runtime would otherwise stay in the slot map forever: every later message
|
|
663
|
+
* would fail against it ("adapter is not running") with no path back to a
|
|
664
|
+
* working adapter short of a manual /reset or daemon restart. After removal,
|
|
665
|
+
* the next switch directive (/codex etc.) rebuilds the adapter from scratch.
|
|
666
|
+
*/
|
|
667
|
+
disposeDeadSlot(slot) {
|
|
668
|
+
if (this.slots.get(slot.adapter) !== slot) {
|
|
669
|
+
return; // Already replaced by a newer ensureSlot run.
|
|
670
|
+
}
|
|
671
|
+
this.slots.delete(slot.adapter);
|
|
672
|
+
if (this.activeAdapter === slot.adapter) {
|
|
673
|
+
this.activeAdapter = null;
|
|
674
|
+
}
|
|
675
|
+
appendDaemonLog(`dead_slot_disposed: adapter=${slot.adapter}`);
|
|
676
|
+
void (async () => {
|
|
677
|
+
try {
|
|
678
|
+
await slot.runtime.dispose();
|
|
679
|
+
}
|
|
680
|
+
catch {
|
|
681
|
+
// Best effort: the runtime already reported a fatal error.
|
|
682
|
+
}
|
|
683
|
+
slot.controller.clearLocalClientEndpoint();
|
|
684
|
+
})();
|
|
685
|
+
}
|
|
660
686
|
async cleanup() {
|
|
661
687
|
appendDaemonLog("shutdown_started");
|
|
662
688
|
for (const slot of this.slots.values()) {
|
|
@@ -721,6 +747,19 @@ class WechatDaemon {
|
|
|
721
747
|
}
|
|
722
748
|
}
|
|
723
749
|
async ensureSlot(adapter, options = {}) {
|
|
750
|
+
// Serialize per-adapter ensureSlot runs. The IPC handler, the WeChat poll
|
|
751
|
+
// loop, and startup can all call this concurrently; without serialization
|
|
752
|
+
// two callers would both observe an empty slot map, create two runtimes,
|
|
753
|
+
// and the second slots.set would orphan the first (duplicate PTY child,
|
|
754
|
+
// double visible client, endpoint files overwriting each other).
|
|
755
|
+
const tail = this.slotEnsureChains.get(adapter) ?? Promise.resolve();
|
|
756
|
+
const run = tail.then(() => this.runEnsureSlot(adapter, options),
|
|
757
|
+
// A rejected predecessor must not block this run — start fresh either way.
|
|
758
|
+
() => this.runEnsureSlot(adapter, options));
|
|
759
|
+
this.slotEnsureChains.set(adapter, run.then(() => undefined, () => undefined));
|
|
760
|
+
return run;
|
|
761
|
+
}
|
|
762
|
+
async runEnsureSlot(adapter, options = {}) {
|
|
724
763
|
const previousActiveAdapter = this.activeAdapter ?? undefined;
|
|
725
764
|
let slot = this.slots.get(adapter);
|
|
726
765
|
let created = false;
|
|
@@ -765,7 +804,8 @@ class WechatDaemon {
|
|
|
765
804
|
reuseExistingVisible: options.reuseExistingVisible !== false,
|
|
766
805
|
});
|
|
767
806
|
if (!created &&
|
|
768
|
-
options.reuseExistingVisible === false
|
|
807
|
+
(options.reuseExistingVisible === false ||
|
|
808
|
+
options.sessionStartMode === "new") &&
|
|
769
809
|
sessionStartMode === "new" &&
|
|
770
810
|
(adapter === "claude" || adapter === "opencode" || adapter === "pi") &&
|
|
771
811
|
visibleConnected) {
|
|
@@ -853,6 +893,10 @@ class WechatDaemon {
|
|
|
853
893
|
}),
|
|
854
894
|
pendingConfirmations: [],
|
|
855
895
|
pendingUserInput: null,
|
|
896
|
+
resumeCoordinator: new ResumeSessionCoordinator({
|
|
897
|
+
adapter,
|
|
898
|
+
runtime,
|
|
899
|
+
}),
|
|
856
900
|
activeTask: null,
|
|
857
901
|
lastOutputAt: 0,
|
|
858
902
|
};
|
|
@@ -952,14 +996,19 @@ class WechatDaemon {
|
|
|
952
996
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
|
|
953
997
|
}));
|
|
954
998
|
break;
|
|
955
|
-
case "user_input_required":
|
|
999
|
+
case "user_input_required": {
|
|
1000
|
+
// Mirror the pending request synchronously, before the async WeChat
|
|
1001
|
+
// send (which retries with backoff). Otherwise a fast user reply can
|
|
1002
|
+
// race past the still-unwritten mirror and be dispatched as plain
|
|
1003
|
+
// input into a CLI that is waiting for a structured form answer.
|
|
1004
|
+
const pending = toPendingUserInput(event.request);
|
|
1005
|
+
slot.pendingUserInput = pending;
|
|
1006
|
+
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
956
1007
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
957
|
-
const pending = toPendingUserInput(event.request);
|
|
958
|
-
slot.pendingUserInput = pending;
|
|
959
|
-
appendDaemonLog(`user_input_required: adapter=${slot.adapter} questions=${pending.questions.length}`);
|
|
960
1008
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserInputRequestMessage(pending, adapterState)), "user_input_required");
|
|
961
1009
|
}));
|
|
962
1010
|
break;
|
|
1011
|
+
}
|
|
963
1012
|
case "mirrored_user_input":
|
|
964
1013
|
appendDaemonLog(`mirrored_local_input: adapter=${slot.adapter} text=${truncatePreview(event.text)}`);
|
|
965
1014
|
if (shouldForwardBridgeEventToWechat(slot.adapter, event.type, { text: event.text })) {
|
|
@@ -969,8 +1018,12 @@ class WechatDaemon {
|
|
|
969
1018
|
}
|
|
970
1019
|
break;
|
|
971
1020
|
case "session_switched":
|
|
1021
|
+
if (event.source === "local") {
|
|
1022
|
+
slot.resumeCoordinator.clear();
|
|
1023
|
+
}
|
|
972
1024
|
appendDaemonLog(`session_switched: adapter=${slot.adapter} session=${event.sessionId} source=${event.source} reason=${event.reason}`);
|
|
973
|
-
if (
|
|
1025
|
+
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
1026
|
+
shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
|
|
974
1027
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
975
1028
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
976
1029
|
adapter: slot.adapter,
|
|
@@ -982,8 +1035,12 @@ class WechatDaemon {
|
|
|
982
1035
|
}
|
|
983
1036
|
break;
|
|
984
1037
|
case "thread_switched":
|
|
1038
|
+
if (event.source === "local") {
|
|
1039
|
+
slot.resumeCoordinator.clear();
|
|
1040
|
+
}
|
|
985
1041
|
appendDaemonLog(`thread_switched: adapter=${slot.adapter} thread=${event.threadId} source=${event.source} reason=${event.reason}`);
|
|
986
|
-
if (
|
|
1042
|
+
if (shouldForwardSessionSwitchEvent(event.reason) &&
|
|
1043
|
+
shouldForwardBridgeEventToWechat(slot.adapter, event.type)) {
|
|
987
1044
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
988
1045
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatSessionSwitchMessage({
|
|
989
1046
|
adapter: slot.adapter,
|
|
@@ -1015,6 +1072,7 @@ class WechatDaemon {
|
|
|
1015
1072
|
slot.pendingConfirmations = [];
|
|
1016
1073
|
slot.pendingUserInput = null;
|
|
1017
1074
|
slot.activeTask = null;
|
|
1075
|
+
this.disposeDeadSlot(slot);
|
|
1018
1076
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
1019
1077
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatUserFacingBridgeFatalError(event.message)), "fatal_error");
|
|
1020
1078
|
}));
|
|
@@ -1120,6 +1178,15 @@ class WechatDaemon {
|
|
|
1120
1178
|
}
|
|
1121
1179
|
slot = this.getActiveSlot() ?? slot;
|
|
1122
1180
|
const adapterState = slot.runtime.getState();
|
|
1181
|
+
if (adapterState.status === "awaiting_input") {
|
|
1182
|
+
// Belt-and-braces with the pendingUserInput mirror above: the adapter
|
|
1183
|
+
// state is authoritative even when the mirror was cleared or the WeChat
|
|
1184
|
+
// notification is still queued.
|
|
1185
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, slot.pendingUserInput
|
|
1186
|
+
? formatPendingUserInputReminder(slot.pendingUserInput)
|
|
1187
|
+
: `${slot.adapter} is waiting for structured input. Reply with /answer <key>=<value> ...`));
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1123
1190
|
if (adapterState.status === "busy" || adapterState.status === "awaiting_approval") {
|
|
1124
1191
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
|
|
1125
1192
|
return;
|
|
@@ -1151,9 +1218,37 @@ class WechatDaemon {
|
|
|
1151
1218
|
case "status":
|
|
1152
1219
|
await this.queueWechatMessage(message.senderId, formatDaemonStatus(this.getStatus()));
|
|
1153
1220
|
return;
|
|
1154
|
-
case "resume":
|
|
1155
|
-
|
|
1221
|
+
case "resume": {
|
|
1222
|
+
if (!isWechatResumeEnabled(activeSlot.adapter)) {
|
|
1223
|
+
await this.queueWechatMessage(message.senderId, `WeChat /resume is disabled for ${activeSlot.adapter} in daemon mode. Use /resume directly inside the visible terminal; WeChat will follow that local session.`);
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1226
|
+
try {
|
|
1227
|
+
let resumeSlot = activeSlot;
|
|
1228
|
+
if (activeSlot.adapter === "codex" && command.target) {
|
|
1229
|
+
const visible = await this.ensureSlot("codex", {
|
|
1230
|
+
openVisible: true,
|
|
1231
|
+
reuseExistingVisible: true,
|
|
1232
|
+
});
|
|
1233
|
+
if (!visible.activated) {
|
|
1234
|
+
throw new Error(formatDaemonSwitchResultDetail(visible));
|
|
1235
|
+
}
|
|
1236
|
+
resumeSlot = this.slots.get("codex") ?? activeSlot;
|
|
1237
|
+
}
|
|
1238
|
+
if (command.target) {
|
|
1239
|
+
await resumeSlot.outputBatcher.flushNow();
|
|
1240
|
+
}
|
|
1241
|
+
const result = await resumeSlot.resumeCoordinator.execute(command.target);
|
|
1242
|
+
if (result.kind === "resumed") {
|
|
1243
|
+
resumeSlot.activeTask = null;
|
|
1244
|
+
}
|
|
1245
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(resumeSlot.adapter, result.message));
|
|
1246
|
+
}
|
|
1247
|
+
catch (error) {
|
|
1248
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, error instanceof Error ? error.message : String(error)));
|
|
1249
|
+
}
|
|
1156
1250
|
return;
|
|
1251
|
+
}
|
|
1157
1252
|
case "new_session":
|
|
1158
1253
|
if (!activeSlot.runtime.createSession) {
|
|
1159
1254
|
await this.queueWechatMessage(message.senderId, `/new is not available in ${activeSlot.adapter} mode.`);
|
|
@@ -1163,6 +1258,7 @@ class WechatDaemon {
|
|
|
1163
1258
|
activeSlot.outputBatcher.clear();
|
|
1164
1259
|
activeSlot.pendingConfirmations = [];
|
|
1165
1260
|
activeSlot.pendingUserInput = null;
|
|
1261
|
+
activeSlot.resumeCoordinator.clear();
|
|
1166
1262
|
await activeSlot.runtime.createSession();
|
|
1167
1263
|
appendDaemonLog(`new_session: adapter=${activeSlot.adapter}`);
|
|
1168
1264
|
return;
|
|
@@ -1178,6 +1274,7 @@ class WechatDaemon {
|
|
|
1178
1274
|
activeSlot.outputBatcher.clear();
|
|
1179
1275
|
activeSlot.pendingConfirmations = [];
|
|
1180
1276
|
activeSlot.pendingUserInput = null;
|
|
1277
|
+
activeSlot.resumeCoordinator.clear();
|
|
1181
1278
|
await activeSlot.runtime.reset();
|
|
1182
1279
|
appendDaemonLog(`reset: adapter=${activeSlot.adapter}`);
|
|
1183
1280
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "Worker session has been reset."));
|
|
@@ -1231,28 +1328,42 @@ class WechatDaemon {
|
|
|
1231
1328
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, count > 1 ? `${count} approvals denied.` : "Approval denied."));
|
|
1232
1329
|
}
|
|
1233
1330
|
async answerPendingUserInput(message, activeSlot, raw) {
|
|
1234
|
-
|
|
1331
|
+
// Mirror the /confirm and /deny behavior: search every slot for a pending
|
|
1332
|
+
// user-input request so the answer reaches the adapter that asked, even
|
|
1333
|
+
// when the user has since switched the active adapter.
|
|
1334
|
+
const slot = this.resolvePendingUserInputSlot(activeSlot);
|
|
1335
|
+
if (!slot) {
|
|
1336
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "No pending user input request."));
|
|
1337
|
+
return;
|
|
1338
|
+
}
|
|
1339
|
+
const pending = slot.pendingUserInput;
|
|
1235
1340
|
if (!pending) {
|
|
1236
1341
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "No pending user input request."));
|
|
1237
1342
|
return;
|
|
1238
1343
|
}
|
|
1239
1344
|
const parsed = parsePendingUserInputAnswerCommand(raw, pending);
|
|
1240
1345
|
if ("error" in parsed) {
|
|
1241
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(
|
|
1346
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, parsed.error));
|
|
1242
1347
|
return;
|
|
1243
1348
|
}
|
|
1244
|
-
const submitted = await
|
|
1349
|
+
const submitted = await slot.runtime.submitUserInput(parsed.answers);
|
|
1245
1350
|
if (!submitted) {
|
|
1246
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(
|
|
1351
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "The worker could not apply this answer."));
|
|
1247
1352
|
return;
|
|
1248
1353
|
}
|
|
1249
|
-
|
|
1250
|
-
|
|
1354
|
+
slot.pendingUserInput = null;
|
|
1355
|
+
slot.activeTask = {
|
|
1251
1356
|
startedAt: Date.now(),
|
|
1252
1357
|
inputPreview: parsed.preview,
|
|
1253
1358
|
};
|
|
1254
|
-
appendDaemonLog(`user_input_answered: adapter=${
|
|
1255
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(
|
|
1359
|
+
appendDaemonLog(`user_input_answered: adapter=${slot.adapter} preview=${parsed.preview}`);
|
|
1360
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "Answer submitted. Continuing..."));
|
|
1361
|
+
}
|
|
1362
|
+
resolvePendingUserInputSlot(activeSlot) {
|
|
1363
|
+
if (activeSlot.pendingUserInput) {
|
|
1364
|
+
return activeSlot;
|
|
1365
|
+
}
|
|
1366
|
+
return (Array.from(this.slots.values()).find((slot) => slot.pendingUserInput !== null) ?? null);
|
|
1256
1367
|
}
|
|
1257
1368
|
resolvePendingApprovalSlot(activeSlot) {
|
|
1258
1369
|
if (activeSlot.pendingConfirmations.length > 0) {
|
|
@@ -1260,13 +1371,6 @@ class WechatDaemon {
|
|
|
1260
1371
|
}
|
|
1261
1372
|
return (Array.from(this.slots.values()).find((slot) => slot.pendingConfirmations.length > 0) ?? null);
|
|
1262
1373
|
}
|
|
1263
|
-
countPendingApprovals() {
|
|
1264
|
-
let count = 0;
|
|
1265
|
-
for (const slot of this.slots.values()) {
|
|
1266
|
-
count += slot.pendingConfirmations.length;
|
|
1267
|
-
}
|
|
1268
|
-
return count;
|
|
1269
|
-
}
|
|
1270
1374
|
getActiveSlot() {
|
|
1271
1375
|
if (!this.activeAdapter) {
|
|
1272
1376
|
return null;
|
|
@@ -1658,27 +1762,35 @@ export async function runDaemon(options) {
|
|
|
1658
1762
|
daemon.takenOverAdapter = cleanupResult.lock.adapter;
|
|
1659
1763
|
}
|
|
1660
1764
|
await daemon.startIpcServer();
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
process.
|
|
1765
|
+
try {
|
|
1766
|
+
await daemon.runInitialAdapter(options);
|
|
1767
|
+
let shutdownInProgress = false;
|
|
1768
|
+
const handleSignal = (signal) => {
|
|
1769
|
+
if (shutdownInProgress) {
|
|
1770
|
+
log(`Received ${signal} during shutdown, forcing exit.`);
|
|
1771
|
+
process.exit(1);
|
|
1772
|
+
}
|
|
1773
|
+
shutdownInProgress = true;
|
|
1774
|
+
log(`Received ${signal}. Stopping daemon.`);
|
|
1775
|
+
void daemon.shutdown().finally(() => process.exit(0));
|
|
1776
|
+
};
|
|
1777
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
1778
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
1779
|
+
process.on("SIGHUP", () => handleSignal("SIGHUP"));
|
|
1780
|
+
if (process.platform === "win32") {
|
|
1781
|
+
process.on("SIGBREAK", () => handleSignal("SIGBREAK"));
|
|
1782
|
+
}
|
|
1783
|
+
process.on("exit", () => {
|
|
1784
|
+
clearDaemonEndpoint();
|
|
1785
|
+
});
|
|
1786
|
+
await daemon.runPollLoop();
|
|
1787
|
+
}
|
|
1788
|
+
catch (error) {
|
|
1789
|
+
// Abnormal exit (poll loop failure, initial adapter failure): dispose the
|
|
1790
|
+
// slot runtimes so adapter child processes are not stranded as orphans.
|
|
1791
|
+
await daemon.shutdown().catch(() => undefined);
|
|
1792
|
+
throw error;
|
|
1677
1793
|
}
|
|
1678
|
-
process.on("exit", () => {
|
|
1679
|
-
clearDaemonEndpoint();
|
|
1680
|
-
});
|
|
1681
|
-
await daemon.runPollLoop();
|
|
1682
1794
|
}
|
|
1683
1795
|
export async function main(argv = process.argv.slice(2)) {
|
|
1684
1796
|
if (argv.includes("--doctor")) {
|
package/dist/i18n/index.js
CHANGED
package/dist/i18n/messages-en.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const messages = {
|
|
2
2
|
// === Bridge welcome & control ===
|
|
3
|
-
"bridge.welcome": "WeChat Bridge ready ({adapter}).\nCWD: {cwd}
|
|
3
|
+
"bridge.welcome": "WeChat Bridge ready ({adapter}).\nCWD: {cwd}",
|
|
4
4
|
"bridge.stopped": "Bridge stopped.",
|
|
5
5
|
"bridge.interrupt.sent": "Interrupt signal sent.",
|
|
6
6
|
"bridge.interrupt.notBusy": "No active task to interrupt.",
|
|
@@ -66,7 +66,6 @@ export const messages = {
|
|
|
66
66
|
"doctor.clock.ok": "~{skewSeconds}s skew vs server time",
|
|
67
67
|
"doctor.clock.skewWarning": "~{skewSeconds}s skew vs server time (large skew makes the bridge treat fresh messages as pre-start backlog — sync the system clock)",
|
|
68
68
|
"doctor.label.nodePty": "node-pty",
|
|
69
|
-
"doctor.label.shell": "shell",
|
|
70
69
|
"doctor.label.dataDir": "Data dir",
|
|
71
70
|
"doctor.label.credentials": "Credentials",
|
|
72
71
|
"doctor.label.mode": "Mode",
|
|
@@ -79,7 +78,6 @@ export const messages = {
|
|
|
79
78
|
"doctor.winBuildConpty": "(ConPTY requires build 18309+)",
|
|
80
79
|
"doctor.nodePty.loaded": "loaded",
|
|
81
80
|
"doctor.nodePty.fix": "Fix: {fix}",
|
|
82
|
-
"doctor.adapterCli.shellNative": "uses the local shell; no other adapter CLI is checked.",
|
|
83
81
|
"doctor.adapterCli.notFound": "not found{optional}",
|
|
84
82
|
"doctor.dataDirMissing": "(will be created on first run)",
|
|
85
83
|
"doctor.credentialsFound": "found",
|
|
@@ -88,6 +86,7 @@ export const messages = {
|
|
|
88
86
|
"doctor.mode.generic": "generic",
|
|
89
87
|
"doctor.notSpecified": "(not specified)",
|
|
90
88
|
"doctor.none": "none",
|
|
89
|
+
"doctor.endpoint.corrupted": "endpoint file exists but cannot be parsed; it will be replaced on the next successful write",
|
|
91
90
|
"doctor.unknown": "unknown",
|
|
92
91
|
"doctor.yes": "yes",
|
|
93
92
|
"doctor.no": "no",
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export const messages = {
|
|
2
2
|
// === 桥接欢迎 & 控制 ===
|
|
3
|
-
"bridge.welcome": "微信桥接就绪 ({adapter})。\n工作目录: {cwd}
|
|
3
|
+
"bridge.welcome": "微信桥接就绪 ({adapter})。\n工作目录: {cwd}",
|
|
4
4
|
"bridge.stopped": "桥接已停止。",
|
|
5
5
|
"bridge.interrupt.sent": "已发送中断信号。",
|
|
6
6
|
"bridge.interrupt.notBusy": "没有正在执行的任务可中断。",
|
|
@@ -66,7 +66,6 @@ export const messages = {
|
|
|
66
66
|
"doctor.clock.ok": "与服务器时间偏差约 {skewSeconds}s",
|
|
67
67
|
"doctor.clock.skewWarning": "与服务器时间偏差约 {skewSeconds}s(偏差过大会导致桥接启动后的消息被当作积压忽略,请校准系统时间)",
|
|
68
68
|
"doctor.label.nodePty": "node-pty",
|
|
69
|
-
"doctor.label.shell": "shell",
|
|
70
69
|
"doctor.label.dataDir": "数据目录",
|
|
71
70
|
"doctor.label.credentials": "凭据",
|
|
72
71
|
"doctor.label.mode": "模式",
|
|
@@ -79,7 +78,6 @@ export const messages = {
|
|
|
79
78
|
"doctor.winBuildConpty": "(ConPTY 需要 build 18309+)",
|
|
80
79
|
"doctor.nodePty.loaded": "已加载",
|
|
81
80
|
"doctor.nodePty.fix": "修复: {fix}",
|
|
82
|
-
"doctor.adapterCli.shellNative": "使用本机 shell,无需检查其它适配器 CLI。",
|
|
83
81
|
"doctor.adapterCli.notFound": "未找到{optional}",
|
|
84
82
|
"doctor.dataDirMissing": "(首次运行会创建)",
|
|
85
83
|
"doctor.credentialsFound": "已找到",
|
|
@@ -88,6 +86,7 @@ export const messages = {
|
|
|
88
86
|
"doctor.mode.generic": "通用",
|
|
89
87
|
"doctor.notSpecified": "(未指定)",
|
|
90
88
|
"doctor.none": "无",
|
|
89
|
+
"doctor.endpoint.corrupted": "endpoint 文件存在但无法解析;下次成功写入时将被替换",
|
|
91
90
|
"doctor.unknown": "未知",
|
|
92
91
|
"doctor.yes": "是",
|
|
93
92
|
"doctor.no": "否",
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import { ensureChannelDataDir } from "../wechat/channel-config.js";
|
|
3
|
+
/**
|
|
4
|
+
* Write JSON atomically via temp file + rename, so a crash mid-write cannot
|
|
5
|
+
* leave a truncated or half-written file behind. Cross-process readers (the
|
|
6
|
+
* daemon, launchers, companions) either see the previous complete file or the
|
|
7
|
+
* new complete file — never a torn write.
|
|
8
|
+
*/
|
|
9
|
+
export function writeJsonFileAtomic(filePath, value) {
|
|
10
|
+
ensureChannelDataDir();
|
|
11
|
+
const data = JSON.stringify(value, null, 2);
|
|
12
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
13
|
+
fs.writeFileSync(tempPath, data, "utf-8");
|
|
14
|
+
fs.renameSync(tempPath, filePath);
|
|
15
|
+
}
|
package/dist/utils/doctor.js
CHANGED
|
@@ -7,7 +7,7 @@ import { normalizeBridgeLockPayload, shouldAutoReclaimBridgeLock, } from "../bri
|
|
|
7
7
|
import { readLocalCompanionEndpoint as readWorkspaceEndpoint, } from "../companion/local-companion-link.js";
|
|
8
8
|
import { isDaemonEndpointAlive, } from "../daemon/daemon-link.js";
|
|
9
9
|
import { t } from "../i18n/index.js";
|
|
10
|
-
import { DEFAULT_BASE_URL, resolveChannelDataDir } from "../wechat/channel-config.js";
|
|
10
|
+
import { DEFAULT_BASE_URL, getWorkspaceAdapterEndpointFile, getWorkspaceChannelPaths, resolveChannelDataDir, } from "../wechat/channel-config.js";
|
|
11
11
|
const STATE_OK = "[ok]";
|
|
12
12
|
const STATE_WARN = "[warn]";
|
|
13
13
|
const STATE_FAIL = "[fail]";
|
|
@@ -174,8 +174,7 @@ function isBridgeAdapterKind(value) {
|
|
|
174
174
|
return (value === "codex" ||
|
|
175
175
|
value === "claude" ||
|
|
176
176
|
value === "opencode" ||
|
|
177
|
-
value === "pi"
|
|
178
|
-
value === "shell");
|
|
177
|
+
value === "pi");
|
|
179
178
|
}
|
|
180
179
|
export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
|
|
181
180
|
let cwd = fallbackCwd;
|
|
@@ -417,8 +416,6 @@ function chooseCliChecks(options) {
|
|
|
417
416
|
return [{ name: "opencode", label: "OpenCode", optional: false }];
|
|
418
417
|
case "pi":
|
|
419
418
|
return [{ name: "pi", label: "Pi", optional: false }];
|
|
420
|
-
case "shell":
|
|
421
|
-
return [];
|
|
422
419
|
default:
|
|
423
420
|
return [];
|
|
424
421
|
}
|
|
@@ -432,11 +429,6 @@ function chooseCliChecks(options) {
|
|
|
432
429
|
}
|
|
433
430
|
function appendCliLines(lines, options, deps) {
|
|
434
431
|
const clis = chooseCliChecks(options);
|
|
435
|
-
if (options.mode === "bridge" && options.adapter === "shell") {
|
|
436
|
-
section(lines, msg("section.adapterCli"));
|
|
437
|
-
lines.push(row(STATE_OK, msg("label.shell"), msg("adapterCli.shellNative")));
|
|
438
|
-
return;
|
|
439
|
-
}
|
|
440
432
|
if (clis.length === 0) {
|
|
441
433
|
return;
|
|
442
434
|
}
|
|
@@ -519,6 +511,18 @@ async function appendEndpointSummary(lines, label, endpoint, options, lock, deps
|
|
|
519
511
|
lines.push(detail(msg("issue", { issue })));
|
|
520
512
|
}
|
|
521
513
|
}
|
|
514
|
+
function endpointFileState(filePath) {
|
|
515
|
+
if (!fs.existsSync(filePath)) {
|
|
516
|
+
return "missing";
|
|
517
|
+
}
|
|
518
|
+
try {
|
|
519
|
+
JSON.parse(fs.readFileSync(filePath, "utf8"));
|
|
520
|
+
return "readable";
|
|
521
|
+
}
|
|
522
|
+
catch {
|
|
523
|
+
return "corrupted";
|
|
524
|
+
}
|
|
525
|
+
}
|
|
522
526
|
async function appendWorkspaceEndpointLines(lines, options, lock, deps) {
|
|
523
527
|
const adapters = chooseEndpointAdapters(options, lock);
|
|
524
528
|
const seenEndpoints = new Set();
|
|
@@ -527,12 +531,20 @@ async function appendWorkspaceEndpointLines(lines, options, lock, deps) {
|
|
|
527
531
|
if (endpoint) {
|
|
528
532
|
seenEndpoints.add(`${endpoint.kind}:${endpoint.instanceId}:${endpoint.port}`);
|
|
529
533
|
}
|
|
534
|
+
else if (endpointFileState(getWorkspaceAdapterEndpointFile(options.cwd, adapter)) ===
|
|
535
|
+
"corrupted") {
|
|
536
|
+
// A torn or otherwise unparsable endpoint file must not read as a
|
|
537
|
+
// healthy "none" — surface it so the operator knows to clear it.
|
|
538
|
+
lines.push(row(STATE_WARN, msg("label.workspaceEndpoint", { adapter }), msg("endpoint.corrupted")));
|
|
539
|
+
continue;
|
|
540
|
+
}
|
|
530
541
|
await appendEndpointSummary(lines, msg("label.workspaceEndpoint", { adapter }), endpoint, { ...options, adapter }, lock, deps);
|
|
531
542
|
}
|
|
532
543
|
const legacyEndpoint = deps.readLocalCompanionEndpoint(options.cwd);
|
|
533
544
|
const legacyLabel = msg("label.legacyEndpoint");
|
|
534
545
|
if (!legacyEndpoint) {
|
|
535
|
-
|
|
546
|
+
const legacyFileState = endpointFileState(getWorkspaceChannelPaths(options.cwd).endpointFile);
|
|
547
|
+
lines.push(row(legacyFileState === "corrupted" ? STATE_WARN : STATE_OK, legacyLabel, legacyFileState === "corrupted" ? msg("endpoint.corrupted") : msg("none")));
|
|
536
548
|
return;
|
|
537
549
|
}
|
|
538
550
|
const legacyKey = `${legacyEndpoint.kind}:${legacyEndpoint.instanceId}:${legacyEndpoint.port}`;
|
|
@@ -16,7 +16,6 @@ export const CHANNEL_DATA_DIR = resolveChannelDataDir();
|
|
|
16
16
|
export const CREDENTIALS_FILE = path.join(CHANNEL_DATA_DIR, "account.json");
|
|
17
17
|
export const SYNC_BUF_FILE = path.join(CHANNEL_DATA_DIR, "sync_buf.txt");
|
|
18
18
|
export const CONTEXT_CACHE_FILE = path.join(CHANNEL_DATA_DIR, "context_tokens.json");
|
|
19
|
-
export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
|
|
20
19
|
export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
|
|
21
20
|
// Hard cap for bridge.log. Long-running daemons otherwise grow it without bound.
|
|
22
21
|
export const BRIDGE_LOG_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
@@ -48,7 +47,6 @@ export function appendBoundedLog(filePath, line) {
|
|
|
48
47
|
}
|
|
49
48
|
export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
|
|
50
49
|
export const DAEMON_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "daemon-endpoint.json");
|
|
51
|
-
export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
|
|
52
50
|
export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
|
|
53
51
|
export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
|
|
54
52
|
export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
|
package/dist/wechat/setup.js
CHANGED
|
@@ -166,17 +166,11 @@ function printPostLoginHelp(log) {
|
|
|
166
166
|
log("This WeChat account is now authorized for the bridge.");
|
|
167
167
|
log("");
|
|
168
168
|
log("Start from any project directory with one of:");
|
|
169
|
-
log(" wechat-codex
|
|
170
|
-
log(" wechat-claude
|
|
171
|
-
log(" wechat-opencode
|
|
172
|
-
log(" wechat-pi
|
|
173
|
-
log(" wechat-
|
|
174
|
-
log("");
|
|
175
|
-
log("Manual two-terminal mode is also available:");
|
|
176
|
-
log(" wechat-bridge-codex + wechat-codex");
|
|
177
|
-
log(" wechat-bridge-claude + wechat-claude");
|
|
178
|
-
log(" wechat-bridge-opencode + wechat-opencode");
|
|
179
|
-
log(" wechat-bridge-pi + wechat-pi");
|
|
169
|
+
log(" wechat-codex");
|
|
170
|
+
log(" wechat-claude");
|
|
171
|
+
log(" wechat-opencode");
|
|
172
|
+
log(" wechat-pi");
|
|
173
|
+
log(" wechat-daemon");
|
|
180
174
|
log("");
|
|
181
175
|
log("Run wechat-setup again any time you need to refresh the login.");
|
|
182
176
|
}
|