cli-wechat-bridge 1.0.7 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.txt +661 -21
- package/README.md +49 -2
- package/dist/bridge/bridge-adapters.claude.js +194 -5
- package/dist/bridge/bridge-adapters.codex.js +84 -37
- package/dist/bridge/bridge-adapters.core.js +58 -10
- package/dist/bridge/bridge-adapters.opencode.js +20 -12
- package/dist/bridge/bridge-adapters.shared.js +101 -1
- package/dist/bridge/bridge-adapters.shell.js +1 -1
- package/dist/bridge/bridge-process-reaper.js +7 -2
- package/dist/bridge/bridge-utils.js +40 -14
- package/dist/bridge/claude-hooks.js +14 -16
- package/dist/bridge/wechat-bridge.js +59 -10
- package/dist/companion/local-companion-link.js +6 -0
- package/dist/companion/local-companion.js +1 -0
- package/dist/daemon/daemon-link.js +6 -0
- package/dist/daemon/emoji-bindings.js +111 -0
- package/dist/daemon/wechat-daemon.js +197 -61
- package/dist/i18n/index.js +32 -0
- package/dist/i18n/messages-en.js +48 -0
- package/dist/i18n/messages-zh.js +48 -0
- package/dist/runtime/legacy-adapter-runtime.js +3 -0
- package/dist/wechat/channel-config.js +3 -0
- package/package.json +2 -2
|
@@ -2,9 +2,10 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import net from "node:net";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { spawn } from "node:child_process";
|
|
5
|
+
import { execFileSync, spawn } from "node:child_process";
|
|
6
6
|
import { fileURLToPath } from "node:url";
|
|
7
7
|
import { resolveDefaultAdapterCommand, } from "../bridge/bridge-adapters.js";
|
|
8
|
+
import { t } from "../i18n/index.js";
|
|
8
9
|
import { delay, getSharedSessionIdFromAdapterState, quoteWindowsCommandArg, } from "../bridge/bridge-adapters.shared.js";
|
|
9
10
|
import { BridgeController } from "../bridge/bridge-controller.js";
|
|
10
11
|
import { forwardWechatFinalReply } from "../bridge/bridge-final-reply.js";
|
|
@@ -14,6 +15,7 @@ import { buildOneTimeCode, buildWechatInboundPrompt, formatApprovalMessage, form
|
|
|
14
15
|
import { formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../bridge/wechat-bridge.js";
|
|
15
16
|
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
16
17
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
18
|
+
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
17
19
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
|
|
18
20
|
import { createRuntimeHost, } from "../runtime/create-runtime-host.js";
|
|
19
21
|
import { clearLocalCompanionEndpoint, clearLocalCompanionOccupancy, readLocalCompanionEndpoint, } from "../companion/local-companion-link.js";
|
|
@@ -208,6 +210,33 @@ export function buildWindowsVisibleClientLaunchCommand(params) {
|
|
|
208
210
|
...params.args.map((arg) => quoteWindowsCommandArg(arg)),
|
|
209
211
|
].join(" ");
|
|
210
212
|
}
|
|
213
|
+
const LINUX_TERMINALS = [
|
|
214
|
+
{ cmd: "gnome-terminal", buildArgs: (title) => ["--title", title, "--"] },
|
|
215
|
+
{ cmd: "konsole", buildArgs: (title) => ["-p", `tabtitle=${title}`, "-e"] },
|
|
216
|
+
{ cmd: "xfce4-terminal", buildArgs: (title) => ["--title", title, "-e"] },
|
|
217
|
+
{ cmd: "xterm", buildArgs: (title) => ["-title", title, "-e"] },
|
|
218
|
+
];
|
|
219
|
+
let cachedLinuxTerminal;
|
|
220
|
+
function detectLinuxTerminal() {
|
|
221
|
+
if (cachedLinuxTerminal !== undefined) {
|
|
222
|
+
return cachedLinuxTerminal;
|
|
223
|
+
}
|
|
224
|
+
for (const entry of LINUX_TERMINALS) {
|
|
225
|
+
try {
|
|
226
|
+
execFileSync("which", [entry.cmd], { stdio: "ignore" });
|
|
227
|
+
cachedLinuxTerminal = entry;
|
|
228
|
+
return entry;
|
|
229
|
+
}
|
|
230
|
+
catch {
|
|
231
|
+
// not found, try next
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
cachedLinuxTerminal = null;
|
|
235
|
+
return null;
|
|
236
|
+
}
|
|
237
|
+
function shellQuotePosix(s) {
|
|
238
|
+
return "'" + s.replace(/'/g, "'\\''") + "'";
|
|
239
|
+
}
|
|
211
240
|
function formatLaunchPreview(launch) {
|
|
212
241
|
return [launch.command, ...launch.args].join(" ");
|
|
213
242
|
}
|
|
@@ -242,12 +271,53 @@ function openVisibleClient(params) {
|
|
|
242
271
|
pid: child.pid,
|
|
243
272
|
};
|
|
244
273
|
}
|
|
274
|
+
const title = `wechat-${params.adapter}`;
|
|
275
|
+
const fullArgs = [process.execPath, ...args];
|
|
276
|
+
if (process.platform === "darwin") {
|
|
277
|
+
const cmdLine = fullArgs.map(shellQuotePosix).join(" ");
|
|
278
|
+
const script = `tell application "Terminal"
|
|
279
|
+
activate
|
|
280
|
+
do script "cd ${shellQuotePosix(params.cwd)} && exec ${cmdLine}"
|
|
281
|
+
end tell`;
|
|
282
|
+
const child = spawn("osascript", ["-e", script], {
|
|
283
|
+
cwd: params.cwd,
|
|
284
|
+
detached: true,
|
|
285
|
+
stdio: "ignore",
|
|
286
|
+
});
|
|
287
|
+
child.once("error", (error) => {
|
|
288
|
+
params.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
289
|
+
});
|
|
290
|
+
child.unref();
|
|
291
|
+
return {
|
|
292
|
+
command: "osascript",
|
|
293
|
+
args: ["-e", script],
|
|
294
|
+
pid: child.pid,
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
const terminal = detectLinuxTerminal();
|
|
298
|
+
if (terminal) {
|
|
299
|
+
const termArgs = [...terminal.buildArgs(title), ...fullArgs];
|
|
300
|
+
const child = spawn(terminal.cmd, termArgs, {
|
|
301
|
+
cwd: params.cwd,
|
|
302
|
+
env: process.env,
|
|
303
|
+
detached: true,
|
|
304
|
+
stdio: "ignore",
|
|
305
|
+
});
|
|
306
|
+
child.once("error", (error) => {
|
|
307
|
+
params.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
308
|
+
});
|
|
309
|
+
child.unref();
|
|
310
|
+
return {
|
|
311
|
+
command: terminal.cmd,
|
|
312
|
+
args: termArgs,
|
|
313
|
+
pid: child.pid,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
245
316
|
const child = spawn(process.execPath, args, {
|
|
246
317
|
cwd: params.cwd,
|
|
247
318
|
env: process.env,
|
|
248
319
|
detached: true,
|
|
249
320
|
stdio: "ignore",
|
|
250
|
-
windowsHide: false,
|
|
251
321
|
});
|
|
252
322
|
child.once("error", (error) => {
|
|
253
323
|
params.onError?.(error instanceof Error ? error : new Error(String(error)));
|
|
@@ -375,6 +445,7 @@ class WechatDaemon {
|
|
|
375
445
|
startedAt = new Date().toISOString();
|
|
376
446
|
bridgeStartedAtMs = Date.now();
|
|
377
447
|
activeAdapter = null;
|
|
448
|
+
takenOverAdapter;
|
|
378
449
|
textSendChain = Promise.resolve();
|
|
379
450
|
attachmentSendChain = Promise.resolve();
|
|
380
451
|
pendingWechatForwardTasks = new Set();
|
|
@@ -452,7 +523,7 @@ class WechatDaemon {
|
|
|
452
523
|
status: slot.runtime.getState().status,
|
|
453
524
|
cwd: this.cwd,
|
|
454
525
|
companionPid: endpoint?.companionPid,
|
|
455
|
-
pendingApproval:
|
|
526
|
+
pendingApproval: slot.pendingConfirmations.length > 0,
|
|
456
527
|
pendingUserInput: Boolean(slot.pendingUserInput),
|
|
457
528
|
};
|
|
458
529
|
}),
|
|
@@ -473,6 +544,13 @@ class WechatDaemon {
|
|
|
473
544
|
log(`Working directory: ${this.cwd}`);
|
|
474
545
|
log("Switch from WeChat with /codex, /claude, or /opencode.");
|
|
475
546
|
appendDaemonLog(`started: cwd=${this.cwd}`);
|
|
547
|
+
const activeSlot = this.getActiveSlot();
|
|
548
|
+
const welcomeText = t("daemon.welcome", {
|
|
549
|
+
cwd: this.cwd,
|
|
550
|
+
adapter: activeSlot?.adapter ?? "none",
|
|
551
|
+
bindings: formatBindingsListMessage(listBindings()),
|
|
552
|
+
});
|
|
553
|
+
await this.queueWechatMessage(this.authorizedUserId, welcomeText);
|
|
476
554
|
while (!this.shutdownPromise) {
|
|
477
555
|
let pollResult;
|
|
478
556
|
try {
|
|
@@ -598,7 +676,11 @@ class WechatDaemon {
|
|
|
598
676
|
let slot = this.slots.get(adapter);
|
|
599
677
|
let created = false;
|
|
600
678
|
if (!slot) {
|
|
601
|
-
const createSessionStartMode = options.sessionStartMode ??
|
|
679
|
+
const createSessionStartMode = options.sessionStartMode ??
|
|
680
|
+
(adapter === this.takenOverAdapter ? "restore" : defaultDaemonSessionStartMode(adapter));
|
|
681
|
+
if (adapter === this.takenOverAdapter) {
|
|
682
|
+
this.takenOverAdapter = undefined;
|
|
683
|
+
}
|
|
602
684
|
slot = await this.createSlot(adapter, {
|
|
603
685
|
profile: options.profile ?? this.profile,
|
|
604
686
|
sessionStartMode: createSessionStartMode,
|
|
@@ -683,7 +765,7 @@ class WechatDaemon {
|
|
|
683
765
|
outputBatcher: new OutputBatcher(async (text) => {
|
|
684
766
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(adapter, text));
|
|
685
767
|
}),
|
|
686
|
-
|
|
768
|
+
pendingConfirmations: [],
|
|
687
769
|
pendingUserInput: null,
|
|
688
770
|
activeTask: null,
|
|
689
771
|
lastOutputAt: 0,
|
|
@@ -699,7 +781,7 @@ class WechatDaemon {
|
|
|
699
781
|
async startFreshSlotSession(slot) {
|
|
700
782
|
await slot.outputBatcher.flushNow();
|
|
701
783
|
slot.outputBatcher.clear();
|
|
702
|
-
slot.
|
|
784
|
+
slot.pendingConfirmations = [];
|
|
703
785
|
slot.pendingUserInput = null;
|
|
704
786
|
slot.activeTask = null;
|
|
705
787
|
if (slot.adapter === "claude") {
|
|
@@ -716,8 +798,8 @@ class WechatDaemon {
|
|
|
716
798
|
handleSlotEvent(slot, event) {
|
|
717
799
|
slot.controller.syncLocalClientEndpoint();
|
|
718
800
|
const adapterState = slot.runtime.getState();
|
|
719
|
-
if (slot.
|
|
720
|
-
slot.
|
|
801
|
+
if (slot.pendingConfirmations.length > 0 && !adapterState.pendingApproval) {
|
|
802
|
+
slot.pendingConfirmations = [];
|
|
721
803
|
}
|
|
722
804
|
if (slot.pendingUserInput && !adapterState.pendingUserInput) {
|
|
723
805
|
slot.pendingUserInput = null;
|
|
@@ -779,7 +861,7 @@ class WechatDaemon {
|
|
|
779
861
|
case "approval_required":
|
|
780
862
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
781
863
|
const pending = toPendingApproval(event);
|
|
782
|
-
slot.
|
|
864
|
+
slot.pendingConfirmations.push(pending);
|
|
783
865
|
appendDaemonLog(`approval_required: adapter=${slot.adapter} source=${pending.source} command=${truncatePreview(pending.commandPreview)}`);
|
|
784
866
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatApprovalMessage(pending, adapterState)), "approval_required");
|
|
785
867
|
}));
|
|
@@ -828,14 +910,14 @@ class WechatDaemon {
|
|
|
828
910
|
break;
|
|
829
911
|
case "task_complete":
|
|
830
912
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(() => {
|
|
831
|
-
slot.
|
|
913
|
+
slot.pendingConfirmations = [];
|
|
832
914
|
slot.pendingUserInput = null;
|
|
833
915
|
slot.activeTask = null;
|
|
834
916
|
}));
|
|
835
917
|
break;
|
|
836
918
|
case "task_failed":
|
|
837
919
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
838
|
-
slot.
|
|
920
|
+
slot.pendingConfirmations = [];
|
|
839
921
|
slot.pendingUserInput = null;
|
|
840
922
|
slot.activeTask = null;
|
|
841
923
|
await this.queueWechatMessage(this.authorizedUserId, prefixDaemonAdapterMessage(slot.adapter, formatTaskFailedMessage(slot.adapter, event.message)), "task_failed");
|
|
@@ -844,7 +926,7 @@ class WechatDaemon {
|
|
|
844
926
|
case "fatal_error":
|
|
845
927
|
logError(`${slot.adapter}: ${event.message}`);
|
|
846
928
|
appendDaemonLog(`fatal_error: adapter=${slot.adapter} message=${event.message}`);
|
|
847
|
-
slot.
|
|
929
|
+
slot.pendingConfirmations = [];
|
|
848
930
|
slot.pendingUserInput = null;
|
|
849
931
|
slot.activeTask = null;
|
|
850
932
|
this.trackWechatForwardTask(slot.outputBatcher.flushNow().then(async () => {
|
|
@@ -861,6 +943,30 @@ class WechatDaemon {
|
|
|
861
943
|
await this.queueWechatMessage(message.senderId, "Unauthorized. This daemon only accepts messages from the configured WeChat owner.");
|
|
862
944
|
return;
|
|
863
945
|
}
|
|
946
|
+
const emojiMatch = resolveEmojiCommand(message.text);
|
|
947
|
+
if (emojiMatch) {
|
|
948
|
+
const switchTarget = parseDaemonSwitchCommand(emojiMatch.command);
|
|
949
|
+
if (switchTarget && emojiMatch.remainder) {
|
|
950
|
+
const result = await this.ensureSlot(switchTarget, {
|
|
951
|
+
openVisible: true,
|
|
952
|
+
reuseExistingVisible: true,
|
|
953
|
+
});
|
|
954
|
+
if (result.activated) {
|
|
955
|
+
message = { ...message, text: emojiMatch.remainder };
|
|
956
|
+
}
|
|
957
|
+
else {
|
|
958
|
+
const detail = formatDaemonSwitchResultDetail(result);
|
|
959
|
+
await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchTarget}.\n${detail}`);
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
const rewritten = emojiMatch.remainder
|
|
965
|
+
? `${emojiMatch.command} ${emojiMatch.remainder}`
|
|
966
|
+
: emojiMatch.command;
|
|
967
|
+
message = { ...message, text: rewritten };
|
|
968
|
+
}
|
|
969
|
+
}
|
|
864
970
|
const switchAdapter = parseDaemonSwitchCommand(message.text);
|
|
865
971
|
if (switchAdapter) {
|
|
866
972
|
const result = await this.ensureSlot(switchAdapter, {
|
|
@@ -881,6 +987,15 @@ class WechatDaemon {
|
|
|
881
987
|
}, 0);
|
|
882
988
|
return;
|
|
883
989
|
}
|
|
990
|
+
const bindingsCmd = parseEmojiBindingsCommand(message.text);
|
|
991
|
+
if (bindingsCmd) {
|
|
992
|
+
await this.handleEmojiBindingsCommand(message.senderId, bindingsCmd);
|
|
993
|
+
return;
|
|
994
|
+
}
|
|
995
|
+
if (isBindCommandPrefix(message.text)) {
|
|
996
|
+
await this.queueWechatMessage(message.senderId, formatBindCommandUsage());
|
|
997
|
+
return;
|
|
998
|
+
}
|
|
884
999
|
const slot = this.getActiveSlot();
|
|
885
1000
|
if (!slot) {
|
|
886
1001
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
@@ -888,15 +1003,15 @@ class WechatDaemon {
|
|
|
888
1003
|
}
|
|
889
1004
|
const command = parseWechatControlCommand(message.text, {
|
|
890
1005
|
adapter: slot.adapter,
|
|
891
|
-
hasPendingConfirmation:
|
|
1006
|
+
hasPendingConfirmation: slot.pendingConfirmations.length > 0,
|
|
892
1007
|
hasPendingUserInput: Boolean(slot.pendingUserInput),
|
|
893
1008
|
});
|
|
894
1009
|
if (command) {
|
|
895
1010
|
await this.handleSystemCommand(message, slot, command);
|
|
896
1011
|
return;
|
|
897
1012
|
}
|
|
898
|
-
if (slot.
|
|
899
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingApprovalReminder(slot.
|
|
1013
|
+
if (slot.pendingConfirmations.length > 0) {
|
|
1014
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingApprovalReminder(slot.pendingConfirmations[0], slot.runtime.getState())));
|
|
900
1015
|
return;
|
|
901
1016
|
}
|
|
902
1017
|
if (slot.pendingUserInput) {
|
|
@@ -910,6 +1025,26 @@ class WechatDaemon {
|
|
|
910
1025
|
}
|
|
911
1026
|
await this.dispatchInboundWechatText(message, slot);
|
|
912
1027
|
}
|
|
1028
|
+
async handleEmojiBindingsCommand(senderId, cmd) {
|
|
1029
|
+
switch (cmd.type) {
|
|
1030
|
+
case "list": {
|
|
1031
|
+
await this.queueWechatMessage(senderId, formatBindingsListMessage(listBindings()));
|
|
1032
|
+
return;
|
|
1033
|
+
}
|
|
1034
|
+
case "bind": {
|
|
1035
|
+
setBinding(cmd.emoji, cmd.command);
|
|
1036
|
+
await this.queueWechatMessage(senderId, `Bound ${cmd.emoji} → ${cmd.command}`);
|
|
1037
|
+
return;
|
|
1038
|
+
}
|
|
1039
|
+
case "unbind": {
|
|
1040
|
+
const removed = removeBinding(cmd.emoji);
|
|
1041
|
+
await this.queueWechatMessage(senderId, removed
|
|
1042
|
+
? `Unbound ${cmd.emoji}`
|
|
1043
|
+
: `No binding found for ${cmd.emoji}`);
|
|
1044
|
+
return;
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
913
1048
|
async handleSystemCommand(message, activeSlot, command) {
|
|
914
1049
|
switch (command.type) {
|
|
915
1050
|
case "status":
|
|
@@ -925,7 +1060,7 @@ class WechatDaemon {
|
|
|
925
1060
|
}
|
|
926
1061
|
await activeSlot.outputBatcher.flushNow();
|
|
927
1062
|
activeSlot.outputBatcher.clear();
|
|
928
|
-
activeSlot.
|
|
1063
|
+
activeSlot.pendingConfirmations = [];
|
|
929
1064
|
activeSlot.pendingUserInput = null;
|
|
930
1065
|
await activeSlot.runtime.createSession();
|
|
931
1066
|
appendDaemonLog(`new_session: adapter=${activeSlot.adapter}`);
|
|
@@ -940,14 +1075,14 @@ class WechatDaemon {
|
|
|
940
1075
|
case "reset":
|
|
941
1076
|
await activeSlot.outputBatcher.flushNow();
|
|
942
1077
|
activeSlot.outputBatcher.clear();
|
|
943
|
-
activeSlot.
|
|
1078
|
+
activeSlot.pendingConfirmations = [];
|
|
944
1079
|
activeSlot.pendingUserInput = null;
|
|
945
1080
|
await activeSlot.runtime.reset();
|
|
946
1081
|
appendDaemonLog(`reset: adapter=${activeSlot.adapter}`);
|
|
947
1082
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "Worker session has been reset."));
|
|
948
1083
|
return;
|
|
949
1084
|
case "confirm":
|
|
950
|
-
await this.confirmPendingApproval(message, activeSlot
|
|
1085
|
+
await this.confirmPendingApproval(message, activeSlot);
|
|
951
1086
|
return;
|
|
952
1087
|
case "deny":
|
|
953
1088
|
await this.denyPendingApproval(message, activeSlot);
|
|
@@ -957,51 +1092,42 @@ class WechatDaemon {
|
|
|
957
1092
|
return;
|
|
958
1093
|
}
|
|
959
1094
|
}
|
|
960
|
-
async confirmPendingApproval(message, activeSlot
|
|
961
|
-
const slot = this.resolvePendingApprovalSlot(activeSlot
|
|
962
|
-
if (!slot) {
|
|
963
|
-
await this.queueWechatMessage(message.senderId, "No matching pending approval request.");
|
|
964
|
-
return;
|
|
965
|
-
}
|
|
966
|
-
const pending = slot.pendingConfirmation;
|
|
967
|
-
if (!pending) {
|
|
1095
|
+
async confirmPendingApproval(message, activeSlot) {
|
|
1096
|
+
const slot = this.resolvePendingApprovalSlot(activeSlot);
|
|
1097
|
+
if (!slot || slot.pendingConfirmations.length === 0) {
|
|
968
1098
|
await this.queueWechatMessage(message.senderId, "No pending approval request.");
|
|
969
1099
|
return;
|
|
970
1100
|
}
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
return;
|
|
974
|
-
}
|
|
975
|
-
const confirmed = await slot.runtime.resolveApproval("confirm");
|
|
976
|
-
if (!confirmed) {
|
|
1101
|
+
const count = await slot.runtime.resolveAllApprovals("confirm");
|
|
1102
|
+
if (!count) {
|
|
977
1103
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "The worker could not apply this approval request."));
|
|
978
1104
|
return;
|
|
979
1105
|
}
|
|
980
|
-
slot.
|
|
1106
|
+
const preview = slot.pendingConfirmations[0]?.commandPreview ?? "";
|
|
1107
|
+
slot.pendingConfirmations = [];
|
|
981
1108
|
slot.activeTask = {
|
|
982
1109
|
startedAt: Date.now(),
|
|
983
|
-
inputPreview:
|
|
1110
|
+
inputPreview: preview,
|
|
984
1111
|
};
|
|
985
|
-
appendDaemonLog(`approval_confirmed: adapter=${slot.adapter} command=${truncatePreview(
|
|
986
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter,
|
|
1112
|
+
appendDaemonLog(`approval_confirmed: adapter=${slot.adapter} count=${count} command=${truncatePreview(preview)}`);
|
|
1113
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, count > 1
|
|
1114
|
+
? `${count} approvals confirmed. Continuing...`
|
|
1115
|
+
: "Approval confirmed. Continuing..."));
|
|
987
1116
|
}
|
|
988
1117
|
async denyPendingApproval(message, activeSlot) {
|
|
989
|
-
const slot =
|
|
990
|
-
|
|
991
|
-
: Array.from(this.slots.values()).find((entry) => entry.pendingConfirmation) ?? activeSlot;
|
|
992
|
-
const pending = slot.pendingConfirmation;
|
|
993
|
-
if (!pending) {
|
|
1118
|
+
const slot = this.resolvePendingApprovalSlot(activeSlot);
|
|
1119
|
+
if (!slot || slot.pendingConfirmations.length === 0) {
|
|
994
1120
|
await this.queueWechatMessage(message.senderId, "No pending approval request.");
|
|
995
1121
|
return;
|
|
996
1122
|
}
|
|
997
|
-
const
|
|
998
|
-
if (!
|
|
1123
|
+
const count = await slot.runtime.resolveAllApprovals("deny");
|
|
1124
|
+
if (!count) {
|
|
999
1125
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "The worker could not deny this approval request cleanly."));
|
|
1000
1126
|
return;
|
|
1001
1127
|
}
|
|
1002
|
-
slot.
|
|
1003
|
-
appendDaemonLog(`approval_denied: adapter=${slot.adapter}
|
|
1004
|
-
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, "Approval denied."));
|
|
1128
|
+
slot.pendingConfirmations = [];
|
|
1129
|
+
appendDaemonLog(`approval_denied: adapter=${slot.adapter} count=${count}`);
|
|
1130
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, count > 1 ? `${count} approvals denied.` : "Approval denied."));
|
|
1005
1131
|
}
|
|
1006
1132
|
async answerPendingUserInput(message, activeSlot, raw) {
|
|
1007
1133
|
const pending = activeSlot.pendingUserInput;
|
|
@@ -1027,18 +1153,18 @@ class WechatDaemon {
|
|
|
1027
1153
|
appendDaemonLog(`user_input_answered: adapter=${activeSlot.adapter} preview=${parsed.preview}`);
|
|
1028
1154
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(activeSlot.adapter, "Answer submitted. Continuing..."));
|
|
1029
1155
|
}
|
|
1030
|
-
resolvePendingApprovalSlot(activeSlot
|
|
1031
|
-
if (
|
|
1032
|
-
return (Array.from(this.slots.values()).find((slot) => slot.pendingConfirmation?.code === code) ?? null);
|
|
1033
|
-
}
|
|
1034
|
-
if (activeSlot.pendingConfirmation && this.countPendingApprovals() <= 1) {
|
|
1156
|
+
resolvePendingApprovalSlot(activeSlot) {
|
|
1157
|
+
if (activeSlot.pendingConfirmations.length > 0) {
|
|
1035
1158
|
return activeSlot;
|
|
1036
1159
|
}
|
|
1037
|
-
return null;
|
|
1160
|
+
return (Array.from(this.slots.values()).find((slot) => slot.pendingConfirmations.length > 0) ?? null);
|
|
1038
1161
|
}
|
|
1039
1162
|
countPendingApprovals() {
|
|
1040
|
-
|
|
1041
|
-
|
|
1163
|
+
let count = 0;
|
|
1164
|
+
for (const slot of this.slots.values()) {
|
|
1165
|
+
count += slot.pendingConfirmations.length;
|
|
1166
|
+
}
|
|
1167
|
+
return count;
|
|
1042
1168
|
}
|
|
1043
1169
|
getActiveSlot() {
|
|
1044
1170
|
if (!this.activeAdapter) {
|
|
@@ -1322,7 +1448,7 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1322
1448
|
const readLock = deps.readLock ?? readBridgeLockFile;
|
|
1323
1449
|
const isAlive = deps.isAlive ?? isPidAlive;
|
|
1324
1450
|
const killProcess = deps.killProcess ?? ((pid, signal) => {
|
|
1325
|
-
if (signal === "SIGKILL") {
|
|
1451
|
+
if (signal === "SIGKILL" || process.platform === "win32") {
|
|
1326
1452
|
killProcessTreeSync(pid);
|
|
1327
1453
|
return;
|
|
1328
1454
|
}
|
|
@@ -1398,8 +1524,9 @@ export async function cleanupSingleBridgeBeforeDaemon(deps = {}) {
|
|
|
1398
1524
|
}
|
|
1399
1525
|
export async function runDaemon(options) {
|
|
1400
1526
|
migrateLegacyChannelFiles((message) => log(message));
|
|
1527
|
+
loadEmojiBindings();
|
|
1401
1528
|
await cleanupDaemonBeforeStart({ cwd: options.cwd });
|
|
1402
|
-
await cleanupSingleBridgeBeforeDaemon();
|
|
1529
|
+
const cleanupResult = await cleanupSingleBridgeBeforeDaemon();
|
|
1403
1530
|
const reapedPeerPids = await reapPeerBridgeProcesses({
|
|
1404
1531
|
logger: (message) => appendDaemonLog(message),
|
|
1405
1532
|
});
|
|
@@ -1426,17 +1553,26 @@ export async function runDaemon(options) {
|
|
|
1426
1553
|
authorizedUserId: credentials.userId,
|
|
1427
1554
|
transport: new WeChatTransport({ log, logError }),
|
|
1428
1555
|
});
|
|
1556
|
+
if (cleanupResult.action === "stopped" && isDaemonAdapterKind(cleanupResult.lock.adapter)) {
|
|
1557
|
+
daemon.takenOverAdapter = cleanupResult.lock.adapter;
|
|
1558
|
+
}
|
|
1429
1559
|
await daemon.startIpcServer();
|
|
1430
1560
|
await daemon.runInitialAdapter(options);
|
|
1431
|
-
|
|
1561
|
+
let shutdownInProgress = false;
|
|
1562
|
+
const handleSignal = (signal) => {
|
|
1563
|
+
if (shutdownInProgress) {
|
|
1564
|
+
log(`Received ${signal} during shutdown, forcing exit.`);
|
|
1565
|
+
process.exit(1);
|
|
1566
|
+
}
|
|
1567
|
+
shutdownInProgress = true;
|
|
1432
1568
|
log(`Received ${signal}. Stopping daemon.`);
|
|
1433
1569
|
void daemon.shutdown().finally(() => process.exit(0));
|
|
1434
1570
|
};
|
|
1435
|
-
process.
|
|
1436
|
-
process.
|
|
1437
|
-
process.
|
|
1571
|
+
process.on("SIGINT", () => handleSignal("SIGINT"));
|
|
1572
|
+
process.on("SIGTERM", () => handleSignal("SIGTERM"));
|
|
1573
|
+
process.on("SIGHUP", () => handleSignal("SIGHUP"));
|
|
1438
1574
|
if (process.platform === "win32") {
|
|
1439
|
-
process.
|
|
1575
|
+
process.on("SIGBREAK", () => handleSignal("SIGBREAK"));
|
|
1440
1576
|
}
|
|
1441
1577
|
process.on("exit", () => {
|
|
1442
1578
|
clearDaemonEndpoint();
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { messages as zhMessages } from "./messages-zh.js";
|
|
2
|
+
import { messages as enMessages } from "./messages-en.js";
|
|
3
|
+
const catalogs = {
|
|
4
|
+
zh: zhMessages,
|
|
5
|
+
en: enMessages,
|
|
6
|
+
};
|
|
7
|
+
let currentLocale = "zh";
|
|
8
|
+
export function getLocale() {
|
|
9
|
+
return currentLocale;
|
|
10
|
+
}
|
|
11
|
+
export function setLocale(locale) {
|
|
12
|
+
currentLocale = locale;
|
|
13
|
+
}
|
|
14
|
+
export function initLocaleFromEnv(env = process.env) {
|
|
15
|
+
const value = env.CLI_BRIDGE_LANG?.trim().toLowerCase();
|
|
16
|
+
if (value === "en" || value === "english") {
|
|
17
|
+
currentLocale = "en";
|
|
18
|
+
}
|
|
19
|
+
else {
|
|
20
|
+
currentLocale = "zh";
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
export function t(key, params) {
|
|
24
|
+
const catalog = catalogs[currentLocale];
|
|
25
|
+
let message = catalog[key] ?? catalogs.en[key] ?? key;
|
|
26
|
+
if (params) {
|
|
27
|
+
for (const [name, value] of Object.entries(params)) {
|
|
28
|
+
message = message.replaceAll(`{${name}}`, String(value));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return message;
|
|
32
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const messages = {
|
|
2
|
+
// === Bridge welcome & control ===
|
|
3
|
+
"bridge.welcome": "WeChat Bridge ready ({adapter}).\nCWD: {cwd}\n\nCommands: /stop, /confirm, /deny, /status, /new\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
|
|
4
|
+
"bridge.stopped": "Bridge stopped.",
|
|
5
|
+
"bridge.interrupt.sent": "Interrupt signal sent.",
|
|
6
|
+
"bridge.interrupt.notBusy": "No active task to interrupt.",
|
|
7
|
+
"bridge.reset.done": "Worker session has been reset.",
|
|
8
|
+
"bridge.status.notRunning": "{adapter} adapter is not running.",
|
|
9
|
+
"bridge.status.busy": "{adapter} is still working on the previous request. Wait for it to finish or send /interrupt.",
|
|
10
|
+
"bridge.newSession.done": "New session started.",
|
|
11
|
+
"bridge.fatalError": "Bridge error: {message}",
|
|
12
|
+
// === Daemon welcome & control ===
|
|
13
|
+
"daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /stop, /confirm, /deny, /status\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
|
|
14
|
+
"daemon.noActiveAdapter": "No active terminal is selected. Send /codex, /claude, or /opencode to start one.",
|
|
15
|
+
"daemon.switchResult.new": "Started a new visible CLI.",
|
|
16
|
+
"daemon.switchResult.reused": "Reused the existing visible CLI.",
|
|
17
|
+
// === Approval ===
|
|
18
|
+
"approval.prompt": "{adapter} permission request.\n{summary}\n\nReply /confirm or /deny.",
|
|
19
|
+
"approval.confirmed": "Approval confirmed. Continuing...",
|
|
20
|
+
"approval.denied": "Approval denied.",
|
|
21
|
+
"approval.noPending": "No pending approval request.",
|
|
22
|
+
"approval.batchConfirmed": "Batch confirmed {count} approval(s).",
|
|
23
|
+
"approval.batchDenied": "Batch denied {count} approval(s).",
|
|
24
|
+
// === Hook health check ===
|
|
25
|
+
"hook.healthCheck.warning": "[Warning] No hook events received from Claude after 15s.\nThe hook system may not be working — Claude output will not reach WeChat.",
|
|
26
|
+
"hook.healthCheck.logHint": "\nCheck: {logPath}",
|
|
27
|
+
"hook.healthCheck.fixes": "\nCommon fixes:\n- Ensure Node.js >= 22.6.0: node --version\n- Reinstall: npm install -g cli-wechat-bridge@latest\n- Check firewall: allow localhost TCP connections",
|
|
28
|
+
// === PTY fallback ===
|
|
29
|
+
"pty.fallback.warning": "[Warning] PTY unavailable, using fallback mode. Terminal rendering may be degraded.\n",
|
|
30
|
+
// === Spawn diagnostic ===
|
|
31
|
+
"spawn.diagnostic.title": "Failed to start CLI process: {target}\nError: {error}",
|
|
32
|
+
"spawn.diagnostic.fixesHeader": "\nPossible fixes:",
|
|
33
|
+
"spawn.diagnostic.nodePty": "- The node-pty native module is incompatible with your Node.js version.\n- Run: npm rebuild node-pty\n- Or reinstall: npm install -g cli-wechat-bridge@latest",
|
|
34
|
+
"spawn.diagnostic.xcode": "- Ensure Xcode CLI tools are installed: xcode-select --install",
|
|
35
|
+
"spawn.diagnostic.notFound": "- The command \"{target}\" was not found on PATH.\n- Verify it is installed and accessible from your terminal.",
|
|
36
|
+
"spawn.diagnostic.generic": "- Reinstall: npm install -g cli-wechat-bridge@latest",
|
|
37
|
+
"spawn.diagnostic.nodeVersion": "- Ensure Node.js >= 22.6.0: node --version",
|
|
38
|
+
"spawn.diagnostic.winAdmin": "- If using ConPTY, try running as Administrator.",
|
|
39
|
+
// === Emoji bindings ===
|
|
40
|
+
"binding.usage": "Invalid format.\nUsage: /bind [emoji] command\nExample: /bind 🚀 deploy --prod",
|
|
41
|
+
"binding.bound": "Bound {emoji} → {command}",
|
|
42
|
+
"binding.unbound": "Unbound {emoji}.",
|
|
43
|
+
"binding.notFound": "No binding found for {emoji}.",
|
|
44
|
+
"binding.listEmpty": "No emoji bindings configured.",
|
|
45
|
+
"binding.listHeader": "Emoji bindings:",
|
|
46
|
+
// === Version checker ===
|
|
47
|
+
"update.available": "[Update Available] Version {latest} is available (current: {current}).\nRun: npm install -g cli-wechat-bridge@latest",
|
|
48
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export const messages = {
|
|
2
|
+
// === 桥接欢迎 & 控制 ===
|
|
3
|
+
"bridge.welcome": "微信桥接就绪 ({adapter})。\n工作目录: {cwd}\n\n命令: /stop, /confirm, /deny, /status, /new\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
|
|
4
|
+
"bridge.stopped": "桥接已停止。",
|
|
5
|
+
"bridge.interrupt.sent": "已发送中断信号。",
|
|
6
|
+
"bridge.interrupt.notBusy": "没有正在执行的任务可中断。",
|
|
7
|
+
"bridge.reset.done": "会话已重置。",
|
|
8
|
+
"bridge.status.notRunning": "{adapter} 适配器未运行。",
|
|
9
|
+
"bridge.status.busy": "{adapter} 仍在处理上一个请求。请等待完成或发送 /interrupt。",
|
|
10
|
+
"bridge.newSession.done": "已开始新会话。",
|
|
11
|
+
"bridge.fatalError": "桥接错误: {message}",
|
|
12
|
+
// === Daemon 欢迎 & 控制 ===
|
|
13
|
+
"daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /stop, /confirm, /deny, /status\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
|
|
14
|
+
"daemon.noActiveAdapter": "未选择活跃终端。发送 /codex、/claude 或 /opencode 启动一个。",
|
|
15
|
+
"daemon.switchResult.new": "已启动新的可见 CLI。",
|
|
16
|
+
"daemon.switchResult.reused": "已复用现有的可见 CLI。",
|
|
17
|
+
// === 审批 ===
|
|
18
|
+
"approval.prompt": "{adapter} 权限请求。\n{summary}\n\n回复 /confirm 或 /deny。",
|
|
19
|
+
"approval.confirmed": "已确认审批,继续执行...",
|
|
20
|
+
"approval.denied": "已拒绝审批。",
|
|
21
|
+
"approval.noPending": "没有待处理的审批请求。",
|
|
22
|
+
"approval.batchConfirmed": "已批量确认 {count} 个审批。",
|
|
23
|
+
"approval.batchDenied": "已批量拒绝 {count} 个审批。",
|
|
24
|
+
// === Hook 健康检查 ===
|
|
25
|
+
"hook.healthCheck.warning": "[警告] 15 秒内未收到 Claude hook 事件。\nhook 系统可能未正常工作 — Claude 的输出将无法到达微信。",
|
|
26
|
+
"hook.healthCheck.logHint": "\n查看日志: {logPath}",
|
|
27
|
+
"hook.healthCheck.fixes": "\n常见修复方法:\n- 确保 Node.js >= 22.6.0: node --version\n- 重新安装: npm install -g cli-wechat-bridge@latest\n- 检查防火墙: 允许 localhost TCP 连接",
|
|
28
|
+
// === PTY 回退 ===
|
|
29
|
+
"pty.fallback.warning": "[警告] PTY 不可用,已切换到回退模式。终端渲染可能受限。\n",
|
|
30
|
+
// === 启动诊断 ===
|
|
31
|
+
"spawn.diagnostic.title": "无法启动 CLI 进程: {target}\n错误: {error}",
|
|
32
|
+
"spawn.diagnostic.fixesHeader": "\n可能的修复方法:",
|
|
33
|
+
"spawn.diagnostic.nodePty": "- node-pty 原生模块与当前 Node.js 版本不兼容。\n- 运行: npm rebuild node-pty\n- 或重新安装: npm install -g cli-wechat-bridge@latest",
|
|
34
|
+
"spawn.diagnostic.xcode": "- 确保已安装 Xcode 命令行工具: xcode-select --install",
|
|
35
|
+
"spawn.diagnostic.notFound": "- 命令 \"{target}\" 未在 PATH 中找到。\n- 请确认已安装并可从终端访问。",
|
|
36
|
+
"spawn.diagnostic.generic": "- 重新安装: npm install -g cli-wechat-bridge@latest",
|
|
37
|
+
"spawn.diagnostic.nodeVersion": "- 确保 Node.js >= 22.6.0: node --version",
|
|
38
|
+
"spawn.diagnostic.winAdmin": "- 如使用 ConPTY,请尝试以管理员身份运行。",
|
|
39
|
+
// === Emoji 绑定 ===
|
|
40
|
+
"binding.usage": "格式错误。\n用法: /bind [表情] 命令\n示例: /bind 🚀 deploy --prod",
|
|
41
|
+
"binding.bound": "已绑定 {emoji} → {command}",
|
|
42
|
+
"binding.unbound": "已解绑 {emoji}。",
|
|
43
|
+
"binding.notFound": "未找到 {emoji} 的绑定。",
|
|
44
|
+
"binding.listEmpty": "未配置 emoji 绑定。",
|
|
45
|
+
"binding.listHeader": "Emoji 绑定列表:",
|
|
46
|
+
// === 版本检查 ===
|
|
47
|
+
"update.available": "[有新版本] 版本 {latest} 可用(当前: {current})。\n运行: npm install -g cli-wechat-bridge@latest",
|
|
48
|
+
};
|
|
@@ -34,6 +34,9 @@ export class LegacyAdapterRuntime {
|
|
|
34
34
|
async resolveApproval(action) {
|
|
35
35
|
return await this.adapter.resolveApproval(action);
|
|
36
36
|
}
|
|
37
|
+
async resolveAllApprovals(action) {
|
|
38
|
+
return await this.adapter.resolveAllApprovals(action);
|
|
39
|
+
}
|
|
37
40
|
async submitUserInput(answers) {
|
|
38
41
|
return await this.adapter.submitUserInput(answers);
|
|
39
42
|
}
|
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
|
+
import { initLocaleFromEnv } from "../i18n/index.js";
|
|
6
7
|
const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url));
|
|
7
8
|
const PROJECT_DIR = path.resolve(MODULE_DIR, "..", "..");
|
|
8
9
|
export const DEFAULT_BASE_URL = process.env.WECHAT_ILINK_BASE_URL?.trim() || "https://ilinkai.weixin.qq.com";
|
|
@@ -23,6 +24,8 @@ export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-pane
|
|
|
23
24
|
export const WORKSPACES_DIR = path.join(CHANNEL_DATA_DIR, "workspaces");
|
|
24
25
|
export const INBOUND_MESSAGE_CLAIMS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-message-claims");
|
|
25
26
|
export const INBOUND_ATTACHMENTS_DIR = path.join(CHANNEL_DATA_DIR, "inbound-attachments");
|
|
27
|
+
export const EMOJI_BINDINGS_FILE = path.join(CHANNEL_DATA_DIR, "emoji-bindings.json");
|
|
28
|
+
initLocaleFromEnv();
|
|
26
29
|
const LEGACY_GLOBAL_CHANNEL_DATA_DIR = path.join(os.homedir(), ".claude", "channels", "wechat");
|
|
27
30
|
const LEGACY_REPO_CHANNEL_DATA_DIR = path.join(PROJECT_DIR, "~", ".claude", "channels", "wechat");
|
|
28
31
|
const LEGACY_ENV_CHANNEL_DATA_DIR = process.env.CLAUDE_WECHAT_CHANNEL_DATA_DIR?.trim()
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cli-wechat-bridge",
|
|
3
|
-
"version": "1.0
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"terminal"
|
|
20
20
|
],
|
|
21
21
|
"author": "UNLINEARITY",
|
|
22
|
-
"license": "
|
|
22
|
+
"license": "AGPL-3.0-or-later",
|
|
23
23
|
"repository": {
|
|
24
24
|
"type": "git",
|
|
25
25
|
"url": "git+https://github.com/UNLINEARITY/CLI-WeChat-Bridge.git"
|