@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
|
@@ -18,6 +18,34 @@ 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
19
|
export const BRIDGE_STATE_FILE = path.join(CHANNEL_DATA_DIR, "bridge-state.json");
|
|
20
20
|
export const BRIDGE_LOG_FILE = path.join(CHANNEL_DATA_DIR, "bridge.log");
|
|
21
|
+
// Hard cap for bridge.log. Long-running daemons otherwise grow it without bound.
|
|
22
|
+
export const BRIDGE_LOG_MAX_BYTES = 5 * 1024 * 1024; // 5 MiB
|
|
23
|
+
/**
|
|
24
|
+
* Append a line to a log file, trimming it down to the tail half whenever it
|
|
25
|
+
* exceeds BRIDGE_LOG_MAX_BYTES. Trimming is best-effort under concurrent
|
|
26
|
+
* writers (a few lines may be lost during a trim) but keeps the file bounded.
|
|
27
|
+
*/
|
|
28
|
+
export function appendBoundedLog(filePath, line) {
|
|
29
|
+
try {
|
|
30
|
+
const stat = fs.statSync(filePath);
|
|
31
|
+
if (stat.size > BRIDGE_LOG_MAX_BYTES) {
|
|
32
|
+
const keepSize = Math.floor(BRIDGE_LOG_MAX_BYTES / 2);
|
|
33
|
+
const fd = fs.openSync(filePath, "r");
|
|
34
|
+
try {
|
|
35
|
+
const tail = Buffer.alloc(keepSize);
|
|
36
|
+
const bytesRead = fs.readSync(fd, tail, 0, keepSize, stat.size - keepSize);
|
|
37
|
+
fs.writeFileSync(filePath, tail.subarray(0, bytesRead));
|
|
38
|
+
}
|
|
39
|
+
finally {
|
|
40
|
+
fs.closeSync(fd);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// File missing or unreadable: fall through to a plain append.
|
|
46
|
+
}
|
|
47
|
+
fs.appendFileSync(filePath, line);
|
|
48
|
+
}
|
|
21
49
|
export const BRIDGE_LOCK_FILE = path.join(CHANNEL_DATA_DIR, "bridge.lock.json");
|
|
22
50
|
export const DAEMON_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "daemon-endpoint.json");
|
|
23
51
|
export const CODEX_PANEL_ENDPOINT_FILE = path.join(CHANNEL_DATA_DIR, "codex-panel-endpoint.json");
|
package/dist/wechat/setup.js
CHANGED
|
@@ -169,12 +169,14 @@ function printPostLoginHelp(log) {
|
|
|
169
169
|
log(" wechat-codex-start");
|
|
170
170
|
log(" wechat-claude-start");
|
|
171
171
|
log(" wechat-opencode-start");
|
|
172
|
+
log(" wechat-pi-start");
|
|
172
173
|
log(" wechat-bridge-shell");
|
|
173
174
|
log("");
|
|
174
175
|
log("Manual two-terminal mode is also available:");
|
|
175
176
|
log(" wechat-bridge-codex + wechat-codex");
|
|
176
177
|
log(" wechat-bridge-claude + wechat-claude");
|
|
177
178
|
log(" wechat-bridge-opencode + wechat-opencode");
|
|
179
|
+
log(" wechat-bridge-pi + wechat-pi");
|
|
178
180
|
log("");
|
|
179
181
|
log("Run wechat-setup again any time you need to refresh the login.");
|
|
180
182
|
}
|
|
@@ -306,7 +306,12 @@ export function classifyWechatTransportError(error) {
|
|
|
306
306
|
}
|
|
307
307
|
function writeJsonFile(filePath, value) {
|
|
308
308
|
ensureChannelDataDir();
|
|
309
|
-
|
|
309
|
+
const data = JSON.stringify(value, null, 2);
|
|
310
|
+
// Atomic write via temp file + rename, so a crash mid-write cannot leave a
|
|
311
|
+
// truncated/half-written JSON that would break readers on the next launch.
|
|
312
|
+
const tempPath = `${filePath}.${process.pid}.tmp`;
|
|
313
|
+
fs.writeFileSync(tempPath, data, "utf-8");
|
|
314
|
+
fs.renameSync(tempPath, filePath);
|
|
310
315
|
}
|
|
311
316
|
function randomWechatUin() {
|
|
312
317
|
const uint32 = crypto.randomBytes(4).readUInt32BE(0);
|
|
@@ -445,7 +450,15 @@ async function getUploadUrl(account, params) {
|
|
|
445
450
|
token: account.token,
|
|
446
451
|
timeoutMs: SEND_TIMEOUT_MS,
|
|
447
452
|
});
|
|
448
|
-
|
|
453
|
+
try {
|
|
454
|
+
return JSON.parse(raw);
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
// A 200-OK but non-JSON body (e.g. an HTML error page, empty body) would
|
|
458
|
+
// otherwise throw an unclassified SyntaxError that bypasses transport error
|
|
459
|
+
// classification/retry. Surface it as a meaningful error instead.
|
|
460
|
+
throw new Error(`getUploadUrl returned a non-JSON response (${raw.length} bytes): ${String(error)}`, { cause: error });
|
|
461
|
+
}
|
|
449
462
|
}
|
|
450
463
|
function buildCdnUploadUrl(cdnBaseUrl, uploadParam, filekey) {
|
|
451
464
|
return `${cdnBaseUrl}/upload?encrypted_query_param=${encodeURIComponent(uploadParam)}&filekey=${encodeURIComponent(filekey)}`;
|
|
@@ -678,11 +691,25 @@ export function buildInboundMessageClaimPath(messageKey, claimsDir = INBOUND_MES
|
|
|
678
691
|
return path.join(claimsDir, fileName);
|
|
679
692
|
}
|
|
680
693
|
export function clearInboundMessageClaims(claimsDir = INBOUND_MESSAGE_CLAIMS_DIR) {
|
|
694
|
+
// Delete claim files individually from a directory snapshot rather than
|
|
695
|
+
// `rmSync(claimsDir, { recursive })`. A recursive rmdir can remove a file a
|
|
696
|
+
// concurrent process just created via tryClaimInboundMessage, silently
|
|
697
|
+
// invalidating its claim and letting the same message be double-processed.
|
|
698
|
+
// Per-file removal only deletes files present at snapshot time.
|
|
699
|
+
let entries;
|
|
681
700
|
try {
|
|
682
|
-
fs.
|
|
701
|
+
entries = fs.readdirSync(claimsDir);
|
|
683
702
|
}
|
|
684
703
|
catch {
|
|
685
|
-
//
|
|
704
|
+
return; // directory does not exist; nothing to clear
|
|
705
|
+
}
|
|
706
|
+
for (const entry of entries) {
|
|
707
|
+
try {
|
|
708
|
+
fs.rmSync(path.join(claimsDir, entry), { force: true });
|
|
709
|
+
}
|
|
710
|
+
catch {
|
|
711
|
+
// best effort
|
|
712
|
+
}
|
|
686
713
|
}
|
|
687
714
|
}
|
|
688
715
|
export function tryClaimInboundMessage(messageKey, options = {}) {
|
|
@@ -719,7 +746,11 @@ export function tryClaimInboundMessage(messageKey, options = {}) {
|
|
|
719
746
|
? error.code
|
|
720
747
|
: "";
|
|
721
748
|
if (code !== "EEXIST") {
|
|
722
|
-
|
|
749
|
+
// Real persistence failure (permissions, disk full, missing dir, ...).
|
|
750
|
+
// Returning true here would claim success WITHOUT writing the claim file,
|
|
751
|
+
// letting concurrent processes all think they own this message and
|
|
752
|
+
// double-process it. Fail closed so the message is retried instead.
|
|
753
|
+
return false;
|
|
723
754
|
}
|
|
724
755
|
}
|
|
725
756
|
try {
|
|
@@ -849,23 +880,28 @@ export class WeChatTransport {
|
|
|
849
880
|
if (!extracted.text && extracted.attachments.length === 0) {
|
|
850
881
|
continue;
|
|
851
882
|
}
|
|
852
|
-
const messageKey = buildMessageKey(rawMessage);
|
|
853
|
-
if (!this.rememberMessage(messageKey)) {
|
|
854
|
-
continue;
|
|
855
|
-
}
|
|
856
|
-
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
857
|
-
continue;
|
|
858
|
-
}
|
|
859
883
|
const senderId = rawMessage.from_user_id ?? "unknown";
|
|
860
884
|
if (rawMessage.context_token) {
|
|
861
885
|
this.cacheContextToken(senderId, rawMessage.context_token);
|
|
862
886
|
}
|
|
887
|
+
// Filter pre-start backlog BEFORE claiming/remembering the message so a
|
|
888
|
+
// skipped message is not permanently locked away from other processes.
|
|
889
|
+
// A missing create_time_ms is treated as fresh rather than dropped.
|
|
863
890
|
const createdAtMs = rawMessage.create_time_ms ?? 0;
|
|
864
891
|
if (typeof options.minCreatedAtMs === "number" &&
|
|
865
|
-
|
|
892
|
+
Number.isFinite(createdAtMs) &&
|
|
893
|
+
createdAtMs > 0 &&
|
|
894
|
+
createdAtMs < options.minCreatedAtMs) {
|
|
866
895
|
ignoredBacklogCount += 1;
|
|
867
896
|
continue;
|
|
868
897
|
}
|
|
898
|
+
const messageKey = buildMessageKey(rawMessage);
|
|
899
|
+
if (!this.rememberMessage(messageKey)) {
|
|
900
|
+
continue;
|
|
901
|
+
}
|
|
902
|
+
if (!tryClaimInboundMessage(buildScopedMessageClaimKey(account.accountId, messageKey))) {
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
869
905
|
const { attachments, failureLines } = await this.downloadInboundAttachments(extracted.attachments, rawMessage);
|
|
870
906
|
const text = appendInboundAttachmentFailureText(extracted.text, failureLines);
|
|
871
907
|
messages.push({
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@unlinearity/cli-wechat-bridge",
|
|
3
|
-
"version": "1.1.
|
|
4
|
-
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, and PowerShell over WeChat",
|
|
3
|
+
"version": "1.1.3",
|
|
4
|
+
"description": "CLI WeChat Bridge for Codex, Claude Code, OpenCode, Pi, and PowerShell over WeChat",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"bin/",
|
|
8
8
|
"dist/",
|
|
9
|
+
"scripts/ensure-node-pty-permissions.mjs",
|
|
9
10
|
"README.md",
|
|
10
11
|
"LICENSE.txt"
|
|
11
12
|
],
|
|
@@ -16,6 +17,7 @@
|
|
|
16
17
|
"codex",
|
|
17
18
|
"claude",
|
|
18
19
|
"opencode",
|
|
20
|
+
"pi",
|
|
19
21
|
"terminal"
|
|
20
22
|
],
|
|
21
23
|
"author": "UNLINEARITY",
|
|
@@ -43,16 +45,30 @@
|
|
|
43
45
|
"wechat-bridge-opencode": "bin/wechat-bridge-opencode.mjs",
|
|
44
46
|
"wechat-opencode": "bin/wechat-opencode.mjs",
|
|
45
47
|
"wechat-opencode-start": "bin/wechat-opencode-start.mjs",
|
|
48
|
+
"wechat-bridge-pi": "bin/wechat-bridge-pi.mjs",
|
|
49
|
+
"wechat-pi": "bin/wechat-pi.mjs",
|
|
50
|
+
"wechat-pi-start": "bin/wechat-pi-start.mjs",
|
|
46
51
|
"wechat-daemon": "bin/wechat-daemon.mjs",
|
|
47
52
|
"wechat-setup": "bin/wechat-setup.mjs",
|
|
48
53
|
"wechat-check-update": "bin/wechat-check-update.mjs"
|
|
49
54
|
},
|
|
50
55
|
"dependencies": {
|
|
51
|
-
"@modelcontextprotocol/sdk": "^1.
|
|
52
|
-
"@opencode-ai/sdk": "^1.
|
|
56
|
+
"@modelcontextprotocol/sdk": "^1.30.0",
|
|
57
|
+
"@opencode-ai/sdk": "^1.18.0",
|
|
53
58
|
"node-pty": "^1.1.0",
|
|
54
59
|
"qrcode-terminal": "^0.12.0"
|
|
55
60
|
},
|
|
61
|
+
"overrides": {
|
|
62
|
+
"@hono/node-server": "2.1.1",
|
|
63
|
+
"body-parser": "2.3.0",
|
|
64
|
+
"brace-expansion": "5.0.9",
|
|
65
|
+
"express-rate-limit": "8.6.2",
|
|
66
|
+
"fast-uri": "3.1.5",
|
|
67
|
+
"hono": "4.13.3",
|
|
68
|
+
"ip-address": "10.5.0",
|
|
69
|
+
"path-to-regexp": "8.4.2",
|
|
70
|
+
"qs": "6.15.3"
|
|
71
|
+
},
|
|
56
72
|
"engines": {
|
|
57
73
|
"bun": ">=1.0.0",
|
|
58
74
|
"node": ">=24.0.0"
|