@unlinearity/cli-wechat-bridge 1.2.0 → 1.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -10
- package/bin/wechat-check-update.mjs +0 -0
- package/bin/wechat-claude.mjs +0 -0
- package/bin/wechat-codex.mjs +0 -0
- package/bin/wechat-daemon.mjs +0 -0
- package/bin/wechat-opencode.mjs +0 -0
- package/bin/wechat-pi.mjs +0 -0
- package/bin/wechat-setup.mjs +0 -0
- package/bin/wecom-check-update.mjs +0 -0
- package/bin/wecom-claude.mjs +0 -0
- package/bin/wecom-codex.mjs +0 -0
- package/bin/wecom-daemon.mjs +0 -0
- package/bin/wecom-opencode.mjs +0 -0
- package/bin/wecom-pi.mjs +0 -0
- package/bin/wecom-setup.mjs +0 -0
- package/dist/bridge/adapter-control.js +19 -12
- package/dist/bridge/bridge-adapters.claude.js +219 -115
- package/dist/bridge/bridge-adapters.codex.js +295 -90
- package/dist/bridge/bridge-adapters.core.js +84 -7
- package/dist/bridge/bridge-adapters.opencode.js +344 -74
- package/dist/bridge/bridge-adapters.pi.js +23 -18
- package/dist/bridge/bridge-adapters.shared.js +9 -9
- package/dist/bridge/bridge-session-resume.js +13 -6
- package/dist/bridge/bridge-utils.js +94 -144
- package/dist/bridge/claude-hooks.js +60 -3
- package/dist/bridge/claude-native-controls.js +32 -13
- package/dist/bridge/codex-visible-thread-proxy.js +140 -0
- package/dist/bridge/language-control.js +33 -0
- package/dist/bridge/native-terminal-control.js +38 -10
- package/dist/bridge/opencode-v2-compat.js +458 -0
- package/dist/bridge/wechat-bridge.js +57 -42
- package/dist/channels/channel-driver-factory.js +44 -0
- package/dist/channels/local/local-channel-port.js +40 -0
- package/dist/channels/local/local-driver.js +217 -0
- package/dist/channels/wechat/wechat-driver.js +11 -2
- package/dist/channels/wecom/wecom-driver.js +8 -1
- package/dist/companion/local-companion.js +27 -7
- package/dist/companion/opencode-v2-tui-bridge-plugin.js +93 -0
- package/dist/companion/pi-tui-bridge-extension.js +10 -4
- package/dist/core/bridge-control.js +3 -0
- package/dist/core/bridge-defer.js +3 -2
- package/dist/core/bridge-event-forwarder.js +3 -0
- package/dist/core/channel-types.js +5 -1
- package/dist/daemon/wechat-daemon.js +202 -124
- package/dist/i18n/index.js +25 -11
- package/dist/i18n/messages-en.js +244 -1
- package/dist/i18n/messages-zh.js +244 -1
- package/dist/runtime/legacy-adapter-runtime.js +7 -4
- package/dist/wechat/channel-config.js +3 -2
- package/package.json +4 -2
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
// LICENSE.txt. Network services built on it must offer source to users.
|
|
8
8
|
import net from "node:net";
|
|
9
9
|
import path from "node:path";
|
|
10
|
+
import { parseLocale, setLocale, t } from "../i18n/index.js";
|
|
10
11
|
import { createBridgeAdapter } from "../bridge/bridge-adapters.js";
|
|
11
12
|
import { LOCAL_COMPANION_RECONNECT_GRACE_MS, } from "../bridge/bridge-adapters.shared.js";
|
|
12
13
|
import { runCodexRemoteClientFromEndpoint } from "./codex-remote-client.js";
|
|
@@ -170,7 +171,7 @@ export async function runLocalCompanion(options) {
|
|
|
170
171
|
}
|
|
171
172
|
sendLocalCompanionMessage(activeSocket, {
|
|
172
173
|
type: "state",
|
|
173
|
-
state: adapter.getState(),
|
|
174
|
+
state: { ...adapter.getState(), supportsLocaleUpdates: true },
|
|
174
175
|
});
|
|
175
176
|
};
|
|
176
177
|
const sendResponse = (socket, id, ok, result, error) => {
|
|
@@ -211,6 +212,15 @@ export async function runLocalCompanion(options) {
|
|
|
211
212
|
const handleBridgeRequest = async (socket, message) => {
|
|
212
213
|
try {
|
|
213
214
|
switch (message.payload.command) {
|
|
215
|
+
case "set_locale": {
|
|
216
|
+
const locale = parseLocale(message.payload.locale);
|
|
217
|
+
if (!locale)
|
|
218
|
+
throw new Error(t("language.usage"));
|
|
219
|
+
setLocale(locale);
|
|
220
|
+
process.env.CLI_BRIDGE_LANG = locale;
|
|
221
|
+
sendResponse(socket, message.id, true);
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
214
224
|
case "send_input":
|
|
215
225
|
await adapter.sendInput(message.payload.text);
|
|
216
226
|
sendResponse(socket, message.id, true);
|
|
@@ -239,18 +249,18 @@ export async function runLocalCompanion(options) {
|
|
|
239
249
|
break;
|
|
240
250
|
case "list_models":
|
|
241
251
|
if (!adapter.listModels)
|
|
242
|
-
throw new Error("
|
|
252
|
+
throw new Error(t("companion.modelsUnsupported"));
|
|
243
253
|
sendResponse(socket, message.id, true, await adapter.listModels());
|
|
244
254
|
break;
|
|
245
255
|
case "select_model":
|
|
246
256
|
if (!adapter.selectModel)
|
|
247
|
-
throw new Error("
|
|
257
|
+
throw new Error(t("companion.modelUnsupported"));
|
|
248
258
|
sendResponse(socket, message.id, true, await adapter.selectModel(message.payload.modelId));
|
|
249
259
|
publishState();
|
|
250
260
|
break;
|
|
251
261
|
case "set_plan_mode":
|
|
252
262
|
if (!adapter.setPlanMode)
|
|
253
|
-
throw new Error("
|
|
263
|
+
throw new Error(t("companion.planUnsupported"));
|
|
254
264
|
sendResponse(socket, message.id, true, await adapter.setPlanMode(message.payload.enabled));
|
|
255
265
|
publishState();
|
|
256
266
|
break;
|
|
@@ -263,10 +273,14 @@ export async function runLocalCompanion(options) {
|
|
|
263
273
|
sendResponse(socket, message.id, true);
|
|
264
274
|
break;
|
|
265
275
|
case "resolve_approval":
|
|
266
|
-
sendResponse(socket, message.id, true,
|
|
276
|
+
sendResponse(socket, message.id, true, message.payload.requestId === undefined
|
|
277
|
+
? false
|
|
278
|
+
: await adapter.resolveApproval(message.payload.action, message.payload.requestId));
|
|
267
279
|
break;
|
|
268
280
|
case "submit_user_input":
|
|
269
|
-
sendResponse(socket, message.id, true,
|
|
281
|
+
sendResponse(socket, message.id, true, adapter.getState().supportsUserInputRequestIds && !message.payload.requestId
|
|
282
|
+
? false
|
|
283
|
+
: await adapter.submitUserInput(message.payload.answers, message.payload.requestId));
|
|
270
284
|
break;
|
|
271
285
|
case "dispose":
|
|
272
286
|
sendResponse(socket, message.id, true);
|
|
@@ -308,6 +322,11 @@ export async function runLocalCompanion(options) {
|
|
|
308
322
|
localDetach = attachLocalCompanionMessageListener(socket, (message) => {
|
|
309
323
|
if (!helloAcknowledged) {
|
|
310
324
|
if (message.type === "hello_ack") {
|
|
325
|
+
const locale = parseLocale(message.locale);
|
|
326
|
+
if (locale) {
|
|
327
|
+
setLocale(locale);
|
|
328
|
+
process.env.CLI_BRIDGE_LANG = locale;
|
|
329
|
+
}
|
|
311
330
|
helloAcknowledged = true;
|
|
312
331
|
closeReason = null;
|
|
313
332
|
activeSocket = socket;
|
|
@@ -356,6 +375,7 @@ export async function runLocalCompanion(options) {
|
|
|
356
375
|
type: "hello",
|
|
357
376
|
token: endpoint.token,
|
|
358
377
|
companionPid: process.pid,
|
|
378
|
+
supportsLocaleUpdates: true,
|
|
359
379
|
});
|
|
360
380
|
});
|
|
361
381
|
socket.once("error", (error) => {
|
|
@@ -402,13 +422,13 @@ export async function runLocalCompanion(options) {
|
|
|
402
422
|
}
|
|
403
423
|
};
|
|
404
424
|
adapter.setEventSink((event) => {
|
|
425
|
+
publishState();
|
|
405
426
|
if (activeSocket) {
|
|
406
427
|
sendLocalCompanionMessage(activeSocket, {
|
|
407
428
|
type: "event",
|
|
408
429
|
event,
|
|
409
430
|
});
|
|
410
431
|
}
|
|
411
|
-
publishState();
|
|
412
432
|
if (event.type === "fatal_error") {
|
|
413
433
|
void closeCompanion(1, "fatal_error");
|
|
414
434
|
return;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
2
|
+
// Copyright (c) 2026 UNLINEARITY <unlinearity@gmail.com>
|
|
3
|
+
// Source: https://github.com/UNLINEARITY/CLI-WeChat-Bridge
|
|
4
|
+
// This file is part of CLI-WeChat-Bridge. Modifications and derivative works
|
|
5
|
+
// must be released under AGPL-3.0-or-later with full source code; see
|
|
6
|
+
// LICENSE.txt. Network services built on it must offer source to users.
|
|
7
|
+
import net from "node:net";
|
|
8
|
+
const ROUTE_POLL_INTERVAL_MS = 100;
|
|
9
|
+
const MAX_BUFFER_SIZE = 1024 * 1024;
|
|
10
|
+
export default {
|
|
11
|
+
id: "cli-wechat-bridge.session-route-v2",
|
|
12
|
+
setup(context) {
|
|
13
|
+
const port = Number(context.options.port);
|
|
14
|
+
const token = typeof context.options.token === "string" ? context.options.token : "";
|
|
15
|
+
if (!Number.isInteger(port) || port <= 0 || !token) {
|
|
16
|
+
return () => undefined;
|
|
17
|
+
}
|
|
18
|
+
const socket = net.connect({ host: "127.0.0.1", port });
|
|
19
|
+
let connected = false;
|
|
20
|
+
let inputBuffer = "";
|
|
21
|
+
let lastSessionId;
|
|
22
|
+
let queuedFrame = null;
|
|
23
|
+
const writeFrame = (frame) => {
|
|
24
|
+
if (socket.destroyed)
|
|
25
|
+
return;
|
|
26
|
+
if (!connected) {
|
|
27
|
+
queuedFrame = frame;
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
socket.write(`${JSON.stringify(frame)}\n`);
|
|
31
|
+
};
|
|
32
|
+
const publishRoute = () => {
|
|
33
|
+
const route = context.ui.router.current();
|
|
34
|
+
const sessionId = route.type === "session" && typeof route.sessionID === "string"
|
|
35
|
+
? route.sessionID
|
|
36
|
+
: null;
|
|
37
|
+
if (sessionId === lastSessionId)
|
|
38
|
+
return;
|
|
39
|
+
lastSessionId = sessionId;
|
|
40
|
+
writeFrame({
|
|
41
|
+
type: "route_state",
|
|
42
|
+
sessionId,
|
|
43
|
+
observedAt: new Date().toISOString(),
|
|
44
|
+
});
|
|
45
|
+
};
|
|
46
|
+
socket.setEncoding("utf8");
|
|
47
|
+
socket.on("connect", () => {
|
|
48
|
+
socket.setNoDelay(true);
|
|
49
|
+
socket.write(`${JSON.stringify({ type: "hello", token })}\n`);
|
|
50
|
+
connected = true;
|
|
51
|
+
publishRoute();
|
|
52
|
+
if (queuedFrame) {
|
|
53
|
+
socket.write(`${JSON.stringify(queuedFrame)}\n`);
|
|
54
|
+
queuedFrame = null;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
socket.on("data", (chunk) => {
|
|
58
|
+
inputBuffer += chunk;
|
|
59
|
+
if (inputBuffer.length > MAX_BUFFER_SIZE) {
|
|
60
|
+
socket.destroy();
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
for (;;) {
|
|
64
|
+
const newline = inputBuffer.indexOf("\n");
|
|
65
|
+
if (newline < 0)
|
|
66
|
+
break;
|
|
67
|
+
const line = inputBuffer.slice(0, newline).trim();
|
|
68
|
+
inputBuffer = inputBuffer.slice(newline + 1);
|
|
69
|
+
if (!line)
|
|
70
|
+
continue;
|
|
71
|
+
try {
|
|
72
|
+
const frame = JSON.parse(line);
|
|
73
|
+
if (frame.type === "select_session" && typeof frame.sessionId === "string") {
|
|
74
|
+
context.ui.router.navigate({ type: "session", sessionID: frame.sessionId });
|
|
75
|
+
publishRoute();
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Ignore malformed bridge frames and keep the visible TUI alive.
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
socket.on("error", () => {
|
|
84
|
+
// Keep the native TUI usable if the bridge-side observer disappears.
|
|
85
|
+
});
|
|
86
|
+
const timer = setInterval(publishRoute, ROUTE_POLL_INTERVAL_MS);
|
|
87
|
+
timer.unref?.();
|
|
88
|
+
return () => {
|
|
89
|
+
clearInterval(timer);
|
|
90
|
+
socket.destroy();
|
|
91
|
+
};
|
|
92
|
+
},
|
|
93
|
+
};
|
|
@@ -330,11 +330,14 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
330
330
|
});
|
|
331
331
|
pi.on("message_end", (event, context) => {
|
|
332
332
|
latestContext = context;
|
|
333
|
+
const message = isRecord(event.message) ? event.message : {};
|
|
334
|
+
if (message.role !== "assistant") {
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
333
337
|
const text = extractAssistantText(event.message);
|
|
334
|
-
if (!text) {
|
|
338
|
+
if (!text && message.stopReason !== "error") {
|
|
335
339
|
return;
|
|
336
340
|
}
|
|
337
|
-
const message = isRecord(event.message) ? event.message : {};
|
|
338
341
|
writeFrame({
|
|
339
342
|
type: "assistant_message",
|
|
340
343
|
text,
|
|
@@ -346,9 +349,12 @@ export default function piTuiBridgeExtension(pi) {
|
|
|
346
349
|
latestContext = context;
|
|
347
350
|
writeFrame({ type: "agent_start" });
|
|
348
351
|
});
|
|
349
|
-
pi.on("agent_settled", (
|
|
352
|
+
pi.on("agent_settled", (event, context) => {
|
|
350
353
|
latestContext = context;
|
|
351
354
|
writeSessionState(context, "settled");
|
|
352
|
-
writeFrame({
|
|
355
|
+
writeFrame({
|
|
356
|
+
type: "agent_settled",
|
|
357
|
+
...(typeof event.aborted === "boolean" ? { aborted: event.aborted } : {}),
|
|
358
|
+
});
|
|
353
359
|
});
|
|
354
360
|
}
|
|
@@ -7,6 +7,9 @@ export function parseBridgeControlCommand(text) {
|
|
|
7
7
|
const command = rawCommand.toLowerCase();
|
|
8
8
|
const argument = rest.join(" ").trim();
|
|
9
9
|
switch (command) {
|
|
10
|
+
case "/lang":
|
|
11
|
+
case "/language":
|
|
12
|
+
return { type: "language", target: argument || undefined };
|
|
10
13
|
case "/status":
|
|
11
14
|
return { type: "status" };
|
|
12
15
|
case "/resume":
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { t } from "../i18n/index.js";
|
|
1
2
|
export function shouldDeferCodexInboundMessage(params) {
|
|
2
3
|
return (params.adapter === "codex" &&
|
|
3
4
|
!params.hasPendingConfirmation &&
|
|
@@ -18,8 +19,8 @@ export function canDrainDeferredCodexInboundQueue(params) {
|
|
|
18
19
|
params.status !== "awaiting_input");
|
|
19
20
|
}
|
|
20
21
|
export function formatDeferredCodexInboundQueueMessage(queuePosition) {
|
|
21
|
-
return
|
|
22
|
+
return t("bridge.deferred", { position: queuePosition });
|
|
22
23
|
}
|
|
23
24
|
export function isRetryableDeferredCodexDrainError(errorText) {
|
|
24
|
-
return /still working|approval request is pending|waiting for local terminal input
|
|
25
|
+
return /still working|approval request is pending|waiting for local terminal input|仍在运行|有待审批请求|正在等待本地终端输入/i.test(errorText);
|
|
25
26
|
}
|
|
@@ -19,6 +19,9 @@ export async function forwardBridgeEvent(event, handlers) {
|
|
|
19
19
|
case "thinking":
|
|
20
20
|
await handlers.thinking?.(event);
|
|
21
21
|
return;
|
|
22
|
+
case "approval_state_changed":
|
|
23
|
+
await handlers.approvalStateChanged?.(event);
|
|
24
|
+
return;
|
|
22
25
|
case "approval_required":
|
|
23
26
|
await handlers.approvalRequired?.(event);
|
|
24
27
|
return;
|