@unlinearity/cli-wechat-bridge 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -43
- package/bin/_run-entry.mjs +24 -0
- package/bin/wechat-bridge-pi.mjs +5 -0
- package/bin/wechat-pi-start.mjs +5 -0
- package/bin/wechat-pi.mjs +5 -0
- package/dist/bridge/bridge-adapter-common.js +2 -0
- package/dist/bridge/bridge-adapters.claude.js +18 -0
- package/dist/bridge/bridge-adapters.codex.js +83 -2
- package/dist/bridge/bridge-adapters.core.js +36 -2
- package/dist/bridge/bridge-adapters.js +5 -0
- package/dist/bridge/bridge-adapters.opencode.js +278 -32
- package/dist/bridge/bridge-adapters.pi.js +697 -0
- package/dist/bridge/bridge-adapters.shared.js +44 -46
- package/dist/bridge/bridge-final-reply.js +8 -4
- package/dist/bridge/bridge-process-reaper.js +58 -3
- package/dist/bridge/bridge-state.js +15 -6
- package/dist/bridge/bridge-utils.js +129 -30
- package/dist/bridge/claude-hook.js +7 -0
- package/dist/bridge/claude-hooks.js +10 -2
- package/dist/bridge/wechat-bridge.js +20 -8
- package/dist/commands/check-update.js +15 -4
- package/dist/companion/local-companion-link.js +6 -1
- package/dist/companion/local-companion-start.js +29 -8
- package/dist/companion/local-companion.js +3 -3
- package/dist/companion/pi-tui-bridge-extension.js +256 -0
- package/dist/daemon/wechat-daemon.js +126 -25
- package/dist/i18n/messages-en.js +12 -2
- package/dist/i18n/messages-zh.js +12 -2
- package/dist/utils/doctor.js +85 -2
- package/dist/utils/version-checker.js +77 -44
- package/dist/wechat/channel-config.js +28 -0
- package/dist/wechat/setup.js +2 -0
- package/dist/wechat/wechat-transport.js +49 -13
- package/package.json +20 -4
|
@@ -13,7 +13,7 @@ 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
15
|
import { formatUserFacingBridgeFatalError, formatUserFacingInboundError, formatWechatContextTokenStaleLogEntry, formatWechatSendFailureLogEntry, isRetryableWechatSendError, shouldForwardBridgeEventToWechat, } from "../bridge/wechat-bridge.js";
|
|
16
|
-
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
16
|
+
import { BRIDGE_LOCK_FILE, BRIDGE_LOG_FILE, appendBoundedLog, ensureChannelDataDir, migrateLegacyChannelFiles, } from "../wechat/channel-config.js";
|
|
17
17
|
import { ensureWechatCredentials } from "../wechat/setup.js";
|
|
18
18
|
import { formatBindCommandUsage, formatBindingsListMessage, isBindCommandPrefix, listBindings, loadEmojiBindings, parseEmojiBindingsCommand, removeBinding, resolveEmojiCommand, setBinding, } from "./emoji-bindings.js";
|
|
19
19
|
import { classifyWechatTransportError, DEFAULT_LONG_POLL_TIMEOUT_MS, describeWechatTransportError, isWechatContextTokenStaleError, WeChatTransport, } from "../wechat/wechat-transport.js";
|
|
@@ -36,7 +36,7 @@ const DAEMON_TAKEOVER_FORCE_STOP_TIMEOUT_MS = 3_000;
|
|
|
36
36
|
const DAEMON_TAKEOVER_STOP_POLL_MS = 250;
|
|
37
37
|
const VISIBLE_CLIENT_CONNECT_TIMEOUT_MS = 15_000;
|
|
38
38
|
const VISIBLE_CLIENT_CONNECT_POLL_MS = 250;
|
|
39
|
-
const DAEMON_ADAPTERS = ["codex", "claude", "opencode"];
|
|
39
|
+
const DAEMON_ADAPTERS = ["codex", "claude", "opencode", "pi"];
|
|
40
40
|
function log(message) {
|
|
41
41
|
process.stderr.write(`[wechat-daemon] ${message}\n`);
|
|
42
42
|
}
|
|
@@ -45,7 +45,7 @@ function logError(message) {
|
|
|
45
45
|
}
|
|
46
46
|
function appendDaemonLog(message) {
|
|
47
47
|
ensureChannelDataDir();
|
|
48
|
-
|
|
48
|
+
appendBoundedLog(BRIDGE_LOG_FILE, `[${new Date().toISOString()}] daemon: ${message}\n`);
|
|
49
49
|
}
|
|
50
50
|
function computePollRetryDelayMs(consecutiveFailures) {
|
|
51
51
|
const normalizedFailures = Math.max(1, consecutiveFailures);
|
|
@@ -56,7 +56,7 @@ function computeWechatSendRetryDelayMs(attempt) {
|
|
|
56
56
|
return WECHAT_SEND_RETRY_BASE_MS * attempt;
|
|
57
57
|
}
|
|
58
58
|
function isDaemonAdapterKind(value) {
|
|
59
|
-
return value === "codex" || value === "claude" || value === "opencode";
|
|
59
|
+
return value === "codex" || value === "claude" || value === "opencode" || value === "pi";
|
|
60
60
|
}
|
|
61
61
|
function isSameWorkspaceCwd(left, right) {
|
|
62
62
|
const normalizedLeft = path.resolve(left);
|
|
@@ -84,10 +84,10 @@ export function parseDaemonCliArgs(argv) {
|
|
|
84
84
|
const next = argv[i + 1];
|
|
85
85
|
if (arg === "--help" || arg === "-h") {
|
|
86
86
|
process.stdout.write([
|
|
87
|
-
"Usage: wechat-daemon [--cwd <path>] [--adapter <codex|claude|opencode>] [--profile <name-or-path>] [--no-open]",
|
|
87
|
+
"Usage: wechat-daemon [--cwd <path>] [--adapter <codex|claude|opencode|pi>] [--profile <name-or-path>] [--no-open]",
|
|
88
88
|
"",
|
|
89
|
-
"Keeps one WeChat connection alive and switches between Codex, Claude Code, and
|
|
90
|
-
"Send /codex, /claude, or /
|
|
89
|
+
"Keeps one WeChat connection alive and switches between Codex, Claude Code, OpenCode, and Pi from WeChat.",
|
|
90
|
+
"Send /codex, /claude, /opencode, or /pi in WeChat to switch the active terminal.",
|
|
91
91
|
"",
|
|
92
92
|
].join("\n"));
|
|
93
93
|
process.exit(0);
|
|
@@ -133,12 +133,26 @@ export function parseDaemonSwitchCommand(text) {
|
|
|
133
133
|
return "claude";
|
|
134
134
|
case "/opencode":
|
|
135
135
|
return "opencode";
|
|
136
|
+
case "/pi":
|
|
137
|
+
return "pi";
|
|
136
138
|
default:
|
|
137
139
|
return null;
|
|
138
140
|
}
|
|
139
141
|
}
|
|
142
|
+
export function parseDaemonSwitchDirective(text) {
|
|
143
|
+
const match = text
|
|
144
|
+
.trim()
|
|
145
|
+
.match(/^\/(codex|claude|opencode|pi)(?:\s+([\s\S]+))?$/i);
|
|
146
|
+
if (!match) {
|
|
147
|
+
return null;
|
|
148
|
+
}
|
|
149
|
+
return {
|
|
150
|
+
adapter: match[1].toLowerCase(),
|
|
151
|
+
remainder: match[2]?.trim() ?? "",
|
|
152
|
+
};
|
|
153
|
+
}
|
|
140
154
|
export function defaultDaemonSessionStartMode(adapter) {
|
|
141
|
-
return adapter === "
|
|
155
|
+
return adapter === "codex" ? "restore" : "new";
|
|
142
156
|
}
|
|
143
157
|
export function resolveDaemonSessionStartMode(params) {
|
|
144
158
|
if (params.reuseExistingVisible && params.visibleConnected) {
|
|
@@ -340,6 +354,12 @@ function isVisibleClientAlive(cwd, adapter) {
|
|
|
340
354
|
clearLocalCompanionOccupancy(cwd, endpoint.instanceId, { adapter });
|
|
341
355
|
return false;
|
|
342
356
|
}
|
|
357
|
+
export function shouldRestartDeadCodexVisibleRuntime(params) {
|
|
358
|
+
return (params.adapter === "codex" &&
|
|
359
|
+
!params.slotCreated &&
|
|
360
|
+
params.hadVisibleClient &&
|
|
361
|
+
!params.visibleConnected);
|
|
362
|
+
}
|
|
343
363
|
function cleanupVisibleClientLauncher(launch) {
|
|
344
364
|
if (!launch.pid || !isPidAlive(launch.pid)) {
|
|
345
365
|
return false;
|
|
@@ -370,6 +390,24 @@ export async function waitForVisibleClientConnection(params, deps = {}) {
|
|
|
370
390
|
await sleepFn(Math.min(pollMs, remainingMs));
|
|
371
391
|
}
|
|
372
392
|
}
|
|
393
|
+
export async function waitForCodexVisibleThread(params, deps = {}) {
|
|
394
|
+
const timeoutMs = params.timeoutMs ?? VISIBLE_CLIENT_CONNECT_TIMEOUT_MS;
|
|
395
|
+
const pollMs = params.pollMs ?? VISIBLE_CLIENT_CONNECT_POLL_MS;
|
|
396
|
+
const sleepFn = deps.sleep ?? sleep;
|
|
397
|
+
const now = deps.now ?? (() => Date.now());
|
|
398
|
+
const deadline = now() + timeoutMs;
|
|
399
|
+
while (true) {
|
|
400
|
+
const threadId = params.getThreadId();
|
|
401
|
+
if (threadId) {
|
|
402
|
+
return threadId;
|
|
403
|
+
}
|
|
404
|
+
const remainingMs = deadline - now();
|
|
405
|
+
if (remainingMs <= 0) {
|
|
406
|
+
return null;
|
|
407
|
+
}
|
|
408
|
+
await sleepFn(Math.min(pollMs, remainingMs));
|
|
409
|
+
}
|
|
410
|
+
}
|
|
373
411
|
function formatInboundMessagePreview(message) {
|
|
374
412
|
if (message.text.trim()) {
|
|
375
413
|
return message.text;
|
|
@@ -384,7 +422,7 @@ function formatInboundMessagePreview(message) {
|
|
|
384
422
|
function formatNoActiveAdapterMessage() {
|
|
385
423
|
return [
|
|
386
424
|
"No active terminal is selected.",
|
|
387
|
-
"Send /codex, /claude, or /
|
|
425
|
+
"Send /codex, /claude, /opencode, or /pi to choose one.",
|
|
388
426
|
].join("\n");
|
|
389
427
|
}
|
|
390
428
|
export function formatDaemonSwitchResultDetail(result) {
|
|
@@ -392,6 +430,9 @@ export function formatDaemonSwitchResultDetail(result) {
|
|
|
392
430
|
const previous = result.previousActiveAdapter
|
|
393
431
|
? ` Active terminal remains ${result.previousActiveAdapter}.`
|
|
394
432
|
: " No terminal is active yet.";
|
|
433
|
+
if (result.visibleConnected && result.visibleReady === false) {
|
|
434
|
+
return `The visible Codex CLI connected, but its active thread is not ready yet.${previous} Check ${BRIDGE_LOG_FILE}.`;
|
|
435
|
+
}
|
|
395
436
|
if (result.openedVisible) {
|
|
396
437
|
return result.created
|
|
397
438
|
? `Started the bridge slot and tried to open the visible CLI, but it has not connected yet.${previous} Check ${BRIDGE_LOG_FILE}.`
|
|
@@ -444,6 +485,7 @@ class WechatDaemon {
|
|
|
444
485
|
slots = new Map();
|
|
445
486
|
startedAt = new Date().toISOString();
|
|
446
487
|
bridgeStartedAtMs = Date.now();
|
|
488
|
+
backlogNoticeSent = false;
|
|
447
489
|
activeAdapter = null;
|
|
448
490
|
takenOverAdapter;
|
|
449
491
|
textSendChain = Promise.resolve();
|
|
@@ -542,7 +584,7 @@ class WechatDaemon {
|
|
|
542
584
|
let consecutivePollFailures = 0;
|
|
543
585
|
log("WeChat daemon is ready.");
|
|
544
586
|
log(`Working directory: ${this.cwd}`);
|
|
545
|
-
log("Switch from WeChat with /codex, /claude, or /
|
|
587
|
+
log("Switch from WeChat with /codex, /claude, /opencode, or /pi.");
|
|
546
588
|
appendDaemonLog(`started: cwd=${this.cwd}`);
|
|
547
589
|
const activeSlot = this.getActiveSlot();
|
|
548
590
|
const welcomeText = t("daemon.welcome", {
|
|
@@ -582,6 +624,13 @@ class WechatDaemon {
|
|
|
582
624
|
}
|
|
583
625
|
if (pollResult.ignoredBacklogCount > 0) {
|
|
584
626
|
appendDaemonLog(`ignored_startup_backlog: count=${pollResult.ignoredBacklogCount}`);
|
|
627
|
+
if (!this.backlogNoticeSent) {
|
|
628
|
+
this.backlogNoticeSent = true;
|
|
629
|
+
await this.queueWechatMessage(this.authorizedUserId, t("bridge.backlogIgnored", {
|
|
630
|
+
count: pollResult.ignoredBacklogCount,
|
|
631
|
+
graceSeconds: Math.round(MESSAGE_START_GRACE_MS / 1000),
|
|
632
|
+
}), "notice");
|
|
633
|
+
}
|
|
585
634
|
}
|
|
586
635
|
for (const message of pollResult.messages) {
|
|
587
636
|
try {
|
|
@@ -689,7 +738,24 @@ class WechatDaemon {
|
|
|
689
738
|
created = true;
|
|
690
739
|
}
|
|
691
740
|
let openedVisible = false;
|
|
741
|
+
const visibleEndpointBeforeProbe = readLocalCompanionEndpoint(this.cwd, {
|
|
742
|
+
adapter,
|
|
743
|
+
});
|
|
744
|
+
const hadVisibleClient = Boolean(visibleEndpointBeforeProbe?.companionPid ||
|
|
745
|
+
visibleEndpointBeforeProbe?.companionConnectedAt ||
|
|
746
|
+
visibleEndpointBeforeProbe?.sharedThreadId ||
|
|
747
|
+
visibleEndpointBeforeProbe?.sharedSessionId);
|
|
692
748
|
let visibleConnected = isVisibleClientAlive(this.cwd, adapter);
|
|
749
|
+
if (shouldRestartDeadCodexVisibleRuntime({
|
|
750
|
+
adapter,
|
|
751
|
+
slotCreated: created,
|
|
752
|
+
hadVisibleClient,
|
|
753
|
+
visibleConnected,
|
|
754
|
+
})) {
|
|
755
|
+
await this.startFreshSlotSession(slot);
|
|
756
|
+
appendDaemonLog(`dead_visible_codex_runtime_restarted: cwd=${this.cwd}`);
|
|
757
|
+
}
|
|
758
|
+
const sharedSessionBeforeVisible = getSharedSessionIdFromAdapterState(slot.runtime.getState());
|
|
693
759
|
const sessionStartMode = resolveDaemonSessionStartMode({
|
|
694
760
|
adapter,
|
|
695
761
|
explicitSessionStartMode: options.sessionStartMode,
|
|
@@ -701,7 +767,7 @@ class WechatDaemon {
|
|
|
701
767
|
if (!created &&
|
|
702
768
|
options.reuseExistingVisible === false &&
|
|
703
769
|
sessionStartMode === "new" &&
|
|
704
|
-
(adapter === "claude" || adapter === "opencode") &&
|
|
770
|
+
(adapter === "claude" || adapter === "opencode" || adapter === "pi") &&
|
|
705
771
|
visibleConnected) {
|
|
706
772
|
await this.startFreshSlotSession(slot);
|
|
707
773
|
appendDaemonLog(`fresh_session_started: adapter=${adapter} source=start_command`);
|
|
@@ -732,16 +798,36 @@ class WechatDaemon {
|
|
|
732
798
|
appendDaemonLog(`visible_client_connect_timeout: adapter=${adapter} cwd=${this.cwd} timeout_ms=${VISIBLE_CLIENT_CONNECT_TIMEOUT_MS} cleaned_launcher=${cleanedLauncher}`);
|
|
733
799
|
}
|
|
734
800
|
}
|
|
735
|
-
|
|
801
|
+
let visibleReady = visibleConnected;
|
|
802
|
+
if (adapter === "codex" && visibleConnected && !sharedSessionBeforeVisible) {
|
|
803
|
+
const visibleThreadId = await waitForCodexVisibleThread({
|
|
804
|
+
getThreadId: () => {
|
|
805
|
+
const state = slot.runtime.getState();
|
|
806
|
+
if (state.lastThreadSwitchSource !== "local") {
|
|
807
|
+
return undefined;
|
|
808
|
+
}
|
|
809
|
+
return state.sharedThreadId ?? state.sharedSessionId;
|
|
810
|
+
},
|
|
811
|
+
});
|
|
812
|
+
visibleReady = Boolean(visibleThreadId);
|
|
813
|
+
if (visibleThreadId) {
|
|
814
|
+
appendDaemonLog(`visible_codex_thread_ready: thread=${visibleThreadId} cwd=${this.cwd}`);
|
|
815
|
+
}
|
|
816
|
+
else {
|
|
817
|
+
appendDaemonLog(`visible_codex_thread_timeout: cwd=${this.cwd} timeout_ms=${VISIBLE_CLIENT_CONNECT_TIMEOUT_MS}`);
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
const activated = options.openVisible === false || (visibleConnected && visibleReady);
|
|
736
821
|
if (activated) {
|
|
737
822
|
this.activeAdapter = adapter;
|
|
738
823
|
}
|
|
739
|
-
appendDaemonLog(`switch_adapter: adapter=${adapter} created=${created} opened_visible=${openedVisible} visible_connected=${visibleConnected} activated=${activated} previous_active=${previousActiveAdapter ?? "(none)"} session_start_mode=${sessionStartMode}`);
|
|
824
|
+
appendDaemonLog(`switch_adapter: adapter=${adapter} created=${created} opened_visible=${openedVisible} visible_connected=${visibleConnected} visible_ready=${visibleReady} activated=${activated} previous_active=${previousActiveAdapter ?? "(none)"} session_start_mode=${sessionStartMode}`);
|
|
740
825
|
return {
|
|
741
826
|
activeAdapter: adapter,
|
|
742
827
|
created,
|
|
743
828
|
openedVisible,
|
|
744
829
|
visibleConnected,
|
|
830
|
+
visibleReady,
|
|
745
831
|
activated,
|
|
746
832
|
previousActiveAdapter,
|
|
747
833
|
};
|
|
@@ -784,12 +870,12 @@ class WechatDaemon {
|
|
|
784
870
|
slot.pendingConfirmations = [];
|
|
785
871
|
slot.pendingUserInput = null;
|
|
786
872
|
slot.activeTask = null;
|
|
787
|
-
if (slot.adapter === "claude") {
|
|
873
|
+
if (slot.adapter === "codex" || slot.adapter === "claude") {
|
|
788
874
|
await slot.runtime.reset();
|
|
789
875
|
}
|
|
790
|
-
else if (slot.adapter === "opencode") {
|
|
876
|
+
else if (slot.adapter === "opencode" || slot.adapter === "pi") {
|
|
791
877
|
if (!slot.runtime.createSession) {
|
|
792
|
-
throw new Error(
|
|
878
|
+
throw new Error(`/new is not available in ${slot.adapter} mode.`);
|
|
793
879
|
}
|
|
794
880
|
await slot.runtime.createSession();
|
|
795
881
|
}
|
|
@@ -967,18 +1053,24 @@ class WechatDaemon {
|
|
|
967
1053
|
message = { ...message, text: rewritten };
|
|
968
1054
|
}
|
|
969
1055
|
}
|
|
970
|
-
const
|
|
971
|
-
if (
|
|
972
|
-
const result = await this.ensureSlot(
|
|
1056
|
+
const switchDirective = parseDaemonSwitchDirective(message.text);
|
|
1057
|
+
if (switchDirective) {
|
|
1058
|
+
const result = await this.ensureSlot(switchDirective.adapter, {
|
|
973
1059
|
openVisible: true,
|
|
974
1060
|
reuseExistingVisible: true,
|
|
975
1061
|
});
|
|
976
1062
|
const detail = formatDaemonSwitchResultDetail(result);
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1063
|
+
if (!result.activated) {
|
|
1064
|
+
await this.queueWechatMessage(message.senderId, `Could not activate terminal: ${switchDirective.adapter}.\n${detail}`);
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
if (switchDirective.remainder) {
|
|
1068
|
+
message = { ...message, text: switchDirective.remainder };
|
|
1069
|
+
}
|
|
1070
|
+
else {
|
|
1071
|
+
await this.queueWechatMessage(message.senderId, `Active terminal: ${switchDirective.adapter}.\n${detail}`);
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
982
1074
|
}
|
|
983
1075
|
if (message.text.trim().toLowerCase() === "/daemon-stop") {
|
|
984
1076
|
await this.queueWechatMessage(message.senderId, "Stopping wechat-daemon...");
|
|
@@ -996,7 +1088,7 @@ class WechatDaemon {
|
|
|
996
1088
|
await this.queueWechatMessage(message.senderId, formatBindCommandUsage());
|
|
997
1089
|
return;
|
|
998
1090
|
}
|
|
999
|
-
|
|
1091
|
+
let slot = this.getActiveSlot();
|
|
1000
1092
|
if (!slot) {
|
|
1001
1093
|
await this.queueWechatMessage(message.senderId, formatNoActiveAdapterMessage());
|
|
1002
1094
|
return;
|
|
@@ -1018,6 +1110,15 @@ class WechatDaemon {
|
|
|
1018
1110
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatPendingUserInputReminder(slot.pendingUserInput)));
|
|
1019
1111
|
return;
|
|
1020
1112
|
}
|
|
1113
|
+
const visibleResult = await this.ensureSlot(slot.adapter, {
|
|
1114
|
+
openVisible: true,
|
|
1115
|
+
reuseExistingVisible: true,
|
|
1116
|
+
});
|
|
1117
|
+
if (!visibleResult.activated) {
|
|
1118
|
+
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, formatDaemonSwitchResultDetail(visibleResult)));
|
|
1119
|
+
return;
|
|
1120
|
+
}
|
|
1121
|
+
slot = this.getActiveSlot() ?? slot;
|
|
1021
1122
|
const adapterState = slot.runtime.getState();
|
|
1022
1123
|
if (adapterState.status === "busy" || adapterState.status === "awaiting_approval") {
|
|
1023
1124
|
await this.queueWechatMessage(message.senderId, prefixDaemonAdapterMessage(slot.adapter, `${slot.adapter} is still working. Wait for the current reply or use /stop.`));
|
package/dist/i18n/messages-en.js
CHANGED
|
@@ -9,9 +9,10 @@ export const messages = {
|
|
|
9
9
|
"bridge.status.busy": "{adapter} is still working on the previous request. Wait for it to finish or send /interrupt.",
|
|
10
10
|
"bridge.newSession.done": "New session started.",
|
|
11
11
|
"bridge.fatalError": "Bridge error: {message}",
|
|
12
|
+
"bridge.backlogIgnored": "Ignored {count} backlog message(s) sent before the bridge started (they will not be processed).\nIf you actually sent them after the bridge started, your system clock is likely skewed by more than {graceSeconds}s — sync the clock and restart the bridge.",
|
|
12
13
|
// === 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 /
|
|
14
|
+
"daemon.welcome": "WeChat Daemon ready.\nCWD: {cwd}\nActive: {adapter}\n\nCommands: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\nManage: /bind [emoji] cmd, /unbind [emoji], /bindings",
|
|
15
|
+
"daemon.noActiveAdapter": "No active terminal is selected. Send /codex, /claude, /opencode, or /pi to start one.",
|
|
15
16
|
"daemon.switchResult.new": "Started a new visible CLI.",
|
|
16
17
|
"daemon.switchResult.reused": "Reused the existing visible CLI.",
|
|
17
18
|
// === Approval ===
|
|
@@ -55,6 +56,15 @@ export const messages = {
|
|
|
55
56
|
"doctor.label.node": "Node.js",
|
|
56
57
|
"doctor.label.platform": "Platform",
|
|
57
58
|
"doctor.label.winBuild": "Windows build",
|
|
59
|
+
"doctor.label.codePage": "Console code page",
|
|
60
|
+
"doctor.codePage.nonAsciiWarning": "{codePage} (not UTF-8 and the data directory path contains non-ASCII characters; hook scripts from older versions break in this environment — upgrade to the latest release)",
|
|
61
|
+
"doctor.label.connectivity": "WeChat connectivity",
|
|
62
|
+
"doctor.connectivity.ok": "reachable ({baseUrl})",
|
|
63
|
+
"doctor.connectivity.unreachable": "cannot reach {baseUrl} (check network and firewall)",
|
|
64
|
+
"doctor.connectivity.proxyHint": "cannot reach {baseUrl}. Proxy {proxy} detected, but Node fetch ignores proxy environment variables by default; if your network requires the proxy, set NODE_USE_ENV_PROXY=1 and retry.",
|
|
65
|
+
"doctor.label.clock": "System clock",
|
|
66
|
+
"doctor.clock.ok": "~{skewSeconds}s skew vs server time",
|
|
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)",
|
|
58
68
|
"doctor.label.nodePty": "node-pty",
|
|
59
69
|
"doctor.label.shell": "shell",
|
|
60
70
|
"doctor.label.dataDir": "Data dir",
|
package/dist/i18n/messages-zh.js
CHANGED
|
@@ -9,9 +9,10 @@ export const messages = {
|
|
|
9
9
|
"bridge.status.busy": "{adapter} 仍在处理上一个请求。请等待完成或发送 /interrupt。",
|
|
10
10
|
"bridge.newSession.done": "已开始新会话。",
|
|
11
11
|
"bridge.fatalError": "桥接错误: {message}",
|
|
12
|
+
"bridge.backlogIgnored": "已忽略 {count} 条桥接启动前的积压消息(不会自动处理)。\n如果你是在桥接启动后才发送的消息却看到此提示,说明本机系统时钟可能不准(偏差超过 {graceSeconds} 秒),请校准时间后重启桥接。",
|
|
12
13
|
// === 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 或 /
|
|
14
|
+
"daemon.welcome": "微信 Daemon 就绪。\n工作目录: {cwd}\n当前: {adapter}\n\n命令: /claude, /codex, /opencode, /pi, /stop, /confirm, /deny, /status\n{bindings}\n\n管理: /bind [表情] 命令, /unbind [表情], /bindings",
|
|
15
|
+
"daemon.noActiveAdapter": "未选择活跃终端。发送 /codex、/claude、/opencode 或 /pi 启动一个。",
|
|
15
16
|
"daemon.switchResult.new": "已启动新的可见 CLI。",
|
|
16
17
|
"daemon.switchResult.reused": "已复用现有的可见 CLI。",
|
|
17
18
|
// === 审批 ===
|
|
@@ -55,6 +56,15 @@ export const messages = {
|
|
|
55
56
|
"doctor.label.node": "Node.js",
|
|
56
57
|
"doctor.label.platform": "平台",
|
|
57
58
|
"doctor.label.winBuild": "Windows build",
|
|
59
|
+
"doctor.label.codePage": "控制台代码页",
|
|
60
|
+
"doctor.codePage.nonAsciiWarning": "{codePage}(非 UTF-8,且数据目录路径含非 ASCII 字符;旧版本的 hook 脚本在此环境下会乱码失效,请升级到最新版本)",
|
|
61
|
+
"doctor.label.connectivity": "微信服务可达性",
|
|
62
|
+
"doctor.connectivity.ok": "可达 ({baseUrl})",
|
|
63
|
+
"doctor.connectivity.unreachable": "无法访问 {baseUrl}(检查网络与防火墙)",
|
|
64
|
+
"doctor.connectivity.proxyHint": "无法访问 {baseUrl}。检测到代理 {proxy},但 Node fetch 默认不使用代理环境变量;若必须经代理出网,请设置 NODE_USE_ENV_PROXY=1 后重试。",
|
|
65
|
+
"doctor.label.clock": "系统时钟",
|
|
66
|
+
"doctor.clock.ok": "与服务器时间偏差约 {skewSeconds}s",
|
|
67
|
+
"doctor.clock.skewWarning": "与服务器时间偏差约 {skewSeconds}s(偏差过大会导致桥接启动后的消息被当作积压忽略,请校准系统时间)",
|
|
58
68
|
"doctor.label.nodePty": "node-pty",
|
|
59
69
|
"doctor.label.shell": "shell",
|
|
60
70
|
"doctor.label.dataDir": "数据目录",
|
package/dist/utils/doctor.js
CHANGED
|
@@ -7,12 +7,14 @@ 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 { resolveChannelDataDir } from "../wechat/channel-config.js";
|
|
10
|
+
import { DEFAULT_BASE_URL, resolveChannelDataDir } from "../wechat/channel-config.js";
|
|
11
11
|
const STATE_OK = "[ok]";
|
|
12
12
|
const STATE_WARN = "[warn]";
|
|
13
13
|
const STATE_FAIL = "[fail]";
|
|
14
14
|
const RUNTIME_ENDPOINT_TIMEOUT_MS = 500;
|
|
15
15
|
const DAEMON_ENDPOINT_TIMEOUT_MS = 500;
|
|
16
|
+
const SERVER_DATE_TIMEOUT_MS = 3_000;
|
|
17
|
+
const CLOCK_SKEW_WARN_MS = 30_000;
|
|
16
18
|
function msg(key, params) {
|
|
17
19
|
return t(`doctor.${key}`, params);
|
|
18
20
|
}
|
|
@@ -58,6 +60,53 @@ function getPlatformPtyFix(platform) {
|
|
|
58
60
|
return "npm rebuild node-pty";
|
|
59
61
|
}
|
|
60
62
|
}
|
|
63
|
+
function defaultGetWindowsCodePage() {
|
|
64
|
+
try {
|
|
65
|
+
const output = execFileSync("cmd.exe", ["/d", "/c", "chcp"], {
|
|
66
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
67
|
+
encoding: "utf8",
|
|
68
|
+
windowsHide: true,
|
|
69
|
+
});
|
|
70
|
+
const match = /(\d+)\s*\.?\s*$/.exec(output.trim());
|
|
71
|
+
return match ? Number(match[1]) : null;
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async function defaultFetchServerDate() {
|
|
78
|
+
const controller = new AbortController();
|
|
79
|
+
const timer = setTimeout(() => controller.abort(), SERVER_DATE_TIMEOUT_MS);
|
|
80
|
+
try {
|
|
81
|
+
const res = await fetch(DEFAULT_BASE_URL, {
|
|
82
|
+
method: "GET",
|
|
83
|
+
signal: controller.signal,
|
|
84
|
+
});
|
|
85
|
+
const dateHeader = res.headers.get("date");
|
|
86
|
+
if (!dateHeader) {
|
|
87
|
+
return null;
|
|
88
|
+
}
|
|
89
|
+
const parsed = new Date(dateHeader);
|
|
90
|
+
return Number.isFinite(parsed.getTime()) ? parsed : null;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
finally {
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function containsNonAscii(value) {
|
|
100
|
+
return Array.from(value).some((character) => character.charCodeAt(0) > 0x7f);
|
|
101
|
+
}
|
|
102
|
+
function pickProxyEnvValue(env) {
|
|
103
|
+
return (env.HTTPS_PROXY ??
|
|
104
|
+
env.https_proxy ??
|
|
105
|
+
env.HTTP_PROXY ??
|
|
106
|
+
env.http_proxy ??
|
|
107
|
+
env.ALL_PROXY ??
|
|
108
|
+
env.all_proxy)?.trim() || undefined;
|
|
109
|
+
}
|
|
61
110
|
function defaultIsProcessAlive(pid) {
|
|
62
111
|
if (!Number.isInteger(pid) || pid <= 0) {
|
|
63
112
|
return false;
|
|
@@ -100,6 +149,10 @@ function resolveDeps(deps = {}) {
|
|
|
100
149
|
arch: deps.arch ?? process.arch,
|
|
101
150
|
nodeVersion: deps.nodeVersion ?? process.version,
|
|
102
151
|
osRelease: deps.osRelease ?? os.release,
|
|
152
|
+
env: deps.env ?? process.env,
|
|
153
|
+
now: deps.now ?? Date.now,
|
|
154
|
+
getWindowsCodePage: deps.getWindowsCodePage ?? defaultGetWindowsCodePage,
|
|
155
|
+
fetchServerDate: deps.fetchServerDate ?? defaultFetchServerDate,
|
|
103
156
|
findExecutable: deps.findExecutable ?? findExecutable,
|
|
104
157
|
loadNodePty: deps.loadNodePty ??
|
|
105
158
|
(async () => {
|
|
@@ -121,6 +174,7 @@ function isBridgeAdapterKind(value) {
|
|
|
121
174
|
return (value === "codex" ||
|
|
122
175
|
value === "claude" ||
|
|
123
176
|
value === "opencode" ||
|
|
177
|
+
value === "pi" ||
|
|
124
178
|
value === "shell");
|
|
125
179
|
}
|
|
126
180
|
export function parseDoctorCliArgs(argv, mode = "generic", fallbackCwd = process.cwd()) {
|
|
@@ -349,7 +403,7 @@ function chooseEndpointAdapters(options, lock) {
|
|
|
349
403
|
return [lock.adapter];
|
|
350
404
|
}
|
|
351
405
|
return options.mode === "daemon"
|
|
352
|
-
? ["codex", "claude", "opencode"]
|
|
406
|
+
? ["codex", "claude", "opencode", "pi"]
|
|
353
407
|
: ["codex"];
|
|
354
408
|
}
|
|
355
409
|
function chooseCliChecks(options) {
|
|
@@ -361,6 +415,8 @@ function chooseCliChecks(options) {
|
|
|
361
415
|
return [{ name: "claude", label: "Claude CLI", optional: false }];
|
|
362
416
|
case "opencode":
|
|
363
417
|
return [{ name: "opencode", label: "OpenCode", optional: false }];
|
|
418
|
+
case "pi":
|
|
419
|
+
return [{ name: "pi", label: "Pi", optional: false }];
|
|
364
420
|
case "shell":
|
|
365
421
|
return [];
|
|
366
422
|
default:
|
|
@@ -371,6 +427,7 @@ function chooseCliChecks(options) {
|
|
|
371
427
|
{ name: "codex", label: "Codex CLI", optional: true },
|
|
372
428
|
{ name: "claude", label: "Claude CLI", optional: true },
|
|
373
429
|
{ name: "opencode", label: "OpenCode", optional: true },
|
|
430
|
+
{ name: "pi", label: "Pi", optional: true },
|
|
374
431
|
];
|
|
375
432
|
}
|
|
376
433
|
function appendCliLines(lines, options, deps) {
|
|
@@ -503,6 +560,32 @@ export async function buildDoctorReport(options = {}, rawDeps = {}) {
|
|
|
503
560
|
const build = parseInt(release.split(".").pop() ?? "0", 10);
|
|
504
561
|
const ok = build >= 18309;
|
|
505
562
|
lines.push(row(ok ? STATE_OK : STATE_FAIL, msg("label.winBuild"), ok ? String(build) : `${build} ${msg("winBuildConpty")}`));
|
|
563
|
+
const codePage = deps.getWindowsCodePage();
|
|
564
|
+
if (codePage !== null) {
|
|
565
|
+
const dataDirHasNonAscii = containsNonAscii(deps.resolveDataDir());
|
|
566
|
+
const risky = codePage !== 65001 && dataDirHasNonAscii;
|
|
567
|
+
lines.push(row(risky ? STATE_WARN : STATE_OK, msg("label.codePage"), risky
|
|
568
|
+
? msg("codePage.nonAsciiWarning", { codePage })
|
|
569
|
+
: String(codePage)));
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
const proxyUrl = pickProxyEnvValue(deps.env);
|
|
573
|
+
const serverDate = await deps.fetchServerDate();
|
|
574
|
+
if (serverDate) {
|
|
575
|
+
lines.push(row(STATE_OK, msg("label.connectivity"), msg("connectivity.ok", {
|
|
576
|
+
baseUrl: DEFAULT_BASE_URL,
|
|
577
|
+
})));
|
|
578
|
+
const skewMs = deps.now() - serverDate.getTime();
|
|
579
|
+
const skewSeconds = Math.round(Math.abs(skewMs) / 1000);
|
|
580
|
+
const clockOk = Math.abs(skewMs) <= CLOCK_SKEW_WARN_MS;
|
|
581
|
+
lines.push(row(clockOk ? STATE_OK : STATE_WARN, msg("label.clock"), clockOk
|
|
582
|
+
? msg("clock.ok", { skewSeconds })
|
|
583
|
+
: msg("clock.skewWarning", { skewSeconds })));
|
|
584
|
+
}
|
|
585
|
+
else {
|
|
586
|
+
lines.push(row(STATE_WARN, msg("label.connectivity"), proxyUrl && deps.env.NODE_USE_ENV_PROXY !== "1"
|
|
587
|
+
? msg("connectivity.proxyHint", { baseUrl: DEFAULT_BASE_URL, proxy: proxyUrl })
|
|
588
|
+
: msg("connectivity.unreachable", { baseUrl: DEFAULT_BASE_URL })));
|
|
506
589
|
}
|
|
507
590
|
try {
|
|
508
591
|
await deps.loadNodePty();
|
|
@@ -1,48 +1,70 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { execSync } from "node:child_process";
|
|
4
3
|
import { CHANNEL_DATA_DIR, ensureChannelDataDir } from "../wechat/channel-config.js";
|
|
5
4
|
const UPDATE_CHECK_FILE = path.join(CHANNEL_DATA_DIR, "update-check.json");
|
|
6
5
|
const CACHE_DURATION_MS = 24 * 60 * 60 * 1000; // 24小时
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
// npm registry 是权威更新源——它是用户实际安装的渠道;GitHub tags 作为回退,
|
|
7
|
+
// 以便在 registry 暂时不可达或包尚未发布时仍能给出最新版本。
|
|
8
|
+
const NPM_PACKAGE_NAME = "cli-wechat-bridge";
|
|
9
|
+
const DEFAULT_NPM_REGISTRY_URL = `https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`;
|
|
10
|
+
const DEFAULT_GITHUB_TAGS_URL = "https://api.github.com/repos/UNLINEARITY/CLI-WeChat-Bridge/tags?per_page=20";
|
|
11
|
+
const FETCH_TIMEOUT_MS = 10_000;
|
|
12
|
+
function resolveFetchDeps(options) {
|
|
13
|
+
return {
|
|
14
|
+
fetch: options.fetchImpl ?? globalThis.fetch,
|
|
15
|
+
npmRegistryUrl: options.npmRegistryUrl ?? DEFAULT_NPM_REGISTRY_URL,
|
|
16
|
+
githubTagsUrl: options.githubTagsUrl ?? DEFAULT_GITHUB_TAGS_URL,
|
|
17
|
+
timeoutMs: options.timeoutMs ?? FETCH_TIMEOUT_MS,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function fetchJsonWithTimeout(url, deps) {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const timer = setTimeout(() => controller.abort(), deps.timeoutMs);
|
|
11
23
|
try {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
stdio: ['ignore', 'pipe', 'ignore']
|
|
24
|
+
const res = await deps.fetch(url, {
|
|
25
|
+
method: "GET",
|
|
26
|
+
signal: controller.signal,
|
|
27
|
+
headers: { Accept: "application/json" },
|
|
17
28
|
});
|
|
18
|
-
if (!
|
|
19
|
-
return null;
|
|
20
|
-
}
|
|
21
|
-
// 解析 tags,找到版本号格式(如 0.1.0, 0.2.0 等)
|
|
22
|
-
const versionTags = tagsOutput
|
|
23
|
-
.split('\n')
|
|
24
|
-
.filter(line => {
|
|
25
|
-
// 匹配 refs/tags/0.5.0 格式(不是 v0.5.0)
|
|
26
|
-
return line.includes('refs/tags/') &&
|
|
27
|
-
!line.includes('^{}') &&
|
|
28
|
-
/^\w+\s+refs\/tags\/\d+\.\d+\.\d+$/.test(line);
|
|
29
|
-
})
|
|
30
|
-
.map(line => {
|
|
31
|
-
const match = line.match(/refs\/tags\/(\d+\.\d+\.\d+)$/);
|
|
32
|
-
return match ? match[1] : null;
|
|
33
|
-
})
|
|
34
|
-
.filter((v) => v !== null);
|
|
35
|
-
if (versionTags.length === 0) {
|
|
29
|
+
if (!res.ok) {
|
|
36
30
|
return null;
|
|
37
31
|
}
|
|
38
|
-
|
|
39
|
-
versionTags.sort((a, b) => compareVersions(b, a));
|
|
40
|
-
return versionTags[0] ?? null;
|
|
32
|
+
return await res.json();
|
|
41
33
|
}
|
|
42
|
-
catch
|
|
43
|
-
//
|
|
34
|
+
catch {
|
|
35
|
+
// 网络/超时/解析失败一律视为该源不可用,交由调用方回退。
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
clearTimeout(timer);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
async function fetchVersionFromNpm(deps) {
|
|
43
|
+
const data = await fetchJsonWithTimeout(deps.npmRegistryUrl, deps);
|
|
44
|
+
if (!data || typeof data !== "object") {
|
|
44
45
|
return null;
|
|
45
46
|
}
|
|
47
|
+
const version = data.version;
|
|
48
|
+
return typeof version === "string" ? parseVersion(version) : null;
|
|
49
|
+
}
|
|
50
|
+
async function fetchVersionFromGithub(deps) {
|
|
51
|
+
const data = await fetchJsonWithTimeout(deps.githubTagsUrl, deps);
|
|
52
|
+
if (!Array.isArray(data)) {
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
55
|
+
const versions = data
|
|
56
|
+
.map((tag) => parseVersion(tag.name))
|
|
57
|
+
.filter((v) => v !== null)
|
|
58
|
+
.sort((a, b) => compareVersions(b, a));
|
|
59
|
+
return versions[0] ?? null;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* 从 npm registry 获取最新版本号(回退到 GitHub tags)。
|
|
63
|
+
* 通过 HTTPS 请求而非本地 git 命令,因此对全局安装的命令(wechat-check-update)同样可用。
|
|
64
|
+
*/
|
|
65
|
+
export async function fetchLatestVersion(options = {}) {
|
|
66
|
+
const deps = resolveFetchDeps(options);
|
|
67
|
+
return (await fetchVersionFromNpm(deps)) ?? (await fetchVersionFromGithub(deps));
|
|
46
68
|
}
|
|
47
69
|
/**
|
|
48
70
|
* 从本地 package.json 读取当前版本
|
|
@@ -85,6 +107,17 @@ function writeUpdateCache(cache) {
|
|
|
85
107
|
// 静默失败,不影响正常使用
|
|
86
108
|
}
|
|
87
109
|
}
|
|
110
|
+
const VERSION_PATTERN = /(\d+\.\d+\.\d+)/;
|
|
111
|
+
/**
|
|
112
|
+
* 从任意字符串中提取版本号,兼容带 "v" 前缀的 tag(如 "v1.1.1")。
|
|
113
|
+
*/
|
|
114
|
+
export function parseVersion(raw) {
|
|
115
|
+
if (typeof raw !== "string") {
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
const match = VERSION_PATTERN.exec(raw.trim());
|
|
119
|
+
return match ? (match[1] ?? null) : null;
|
|
120
|
+
}
|
|
88
121
|
/**
|
|
89
122
|
* 比较两个版本号
|
|
90
123
|
* @returns 返回值 > 0 表示 version1 更新,< 0 表示 version2 更新,= 0 表示相等
|
|
@@ -146,16 +179,16 @@ export async function checkForUpdate(forceCheck = false) {
|
|
|
146
179
|
*/
|
|
147
180
|
export function formatUpdateMessage(versionInfo) {
|
|
148
181
|
const { current, latest } = versionInfo;
|
|
149
|
-
return `
|
|
150
|
-
[Update Available] Version ${latest} is available (current: ${current})
|
|
151
|
-
|
|
152
|
-
Update instructions:
|
|
153
|
-
cd CLI-WeChat-Bridge
|
|
154
|
-
git pull
|
|
155
|
-
bun install
|
|
156
|
-
npm install -g .
|
|
157
|
-
|
|
158
|
-
For more information:
|
|
159
|
-
https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
|
|
182
|
+
return `
|
|
183
|
+
[Update Available] Version ${latest} is available (current: ${current})
|
|
184
|
+
|
|
185
|
+
Update instructions:
|
|
186
|
+
cd CLI-WeChat-Bridge
|
|
187
|
+
git pull
|
|
188
|
+
bun install
|
|
189
|
+
npm install -g .
|
|
190
|
+
|
|
191
|
+
For more information:
|
|
192
|
+
https://github.com/UNLINEARITY/CLI-WeChat-Bridge/releases
|
|
160
193
|
`;
|
|
161
194
|
}
|