@ailuntz/minilink 0.9.9 → 0.9.11
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 +4 -0
- package/bin/minilink.mjs +8 -1
- package/package.json +1 -1
- package/src/config.mjs +3 -3
- package/src/install.mjs +22 -3
- package/src/proxy.mjs +5 -3
- package/src/remote.mjs +125 -0
package/README.md
CHANGED
|
@@ -26,6 +26,10 @@ minilink on
|
|
|
26
26
|
|
|
27
27
|
`npm install` 只安装命令;`minilink on` 才接入 Desktop。更新包后再次运行 `minilink on`。
|
|
28
28
|
|
|
29
|
+
本机 Desktop 集成调用 Desktop 自带的 Codex;SSH 远程入口调用远程主机的独立 Codex。配置和会话仍使用官方的 `CODEX_HOME`(默认 `~/.codex`);MiniLink 自己的启动文件放在 `~/.local/share/minilink`。
|
|
30
|
+
|
|
31
|
+
从 MacBook 的 Desktop 通过 SSH 使用 Mac mini 会话时,也要在 **Mac mini** 上安装并运行 `minilink on`。远程入口使用 Mac mini 的独立 Codex,并把模型列表和选择接入 MiniLink。`on` 会在远程登录 shell 中设置仅供 Codex SSH 启动器使用的 `CODEX_INSTALL_DIR`;`off` 会撤销这段设置。重新连接远程主机后生效。
|
|
32
|
+
|
|
29
33
|
## 命令
|
|
30
34
|
|
|
31
35
|
| 命令 | 用途 |
|
package/bin/minilink.mjs
CHANGED
|
@@ -4,9 +4,10 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { VERSION, readConfig, configPath, codexHome, atomicJSON, discoverProfiles, resolveRealCodex, childEnvironment } from '../src/config.mjs';
|
|
5
5
|
import { startUpdate, updateStatus } from '../src/update.mjs';
|
|
6
6
|
import { runProxy } from '../src/proxy.mjs';
|
|
7
|
+
import { runRemoteProxy } from '../src/remote.mjs';
|
|
7
8
|
import { pickerEndpoints } from '../src/picker.mjs';
|
|
8
9
|
import { selectModel, text, language } from '../src/terminal.mjs';
|
|
9
|
-
import { installation, environmentStatus, syncLaunchEnvironment, ensureInstalled } from '../src/install.mjs';
|
|
10
|
+
import { installation, environmentStatus, syncLaunchEnvironment, syncRemoteEnvironment, ensureInstalled } from '../src/install.mjs';
|
|
10
11
|
|
|
11
12
|
const self = fileURLToPath(import.meta.url), args = process.argv.slice(2);
|
|
12
13
|
const commands = ['on', 'link', 'off', 'doctor'];
|
|
@@ -22,8 +23,14 @@ async function main() {
|
|
|
22
23
|
if (command === 'on') { discoverProfiles(); ensureInstalled(); }
|
|
23
24
|
atomicJSON(configPath(), { version: 1, enabled: command === 'on' });
|
|
24
25
|
syncLaunchEnvironment({ enabled: command === 'on' });
|
|
26
|
+
syncRemoteEnvironment({ enabled: command === 'on' });
|
|
25
27
|
return output(text(command));
|
|
26
28
|
}
|
|
29
|
+
if (command === 'remote-proxy') {
|
|
30
|
+
if (args.length !== 1) throw new Error('remote-proxy 不接受额外参数。');
|
|
31
|
+
process.exitCode = await runRemoteProxy();
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
27
34
|
if (command === 'link') {
|
|
28
35
|
if (!readConfig().enabled) throw new Error(text('disabled'));
|
|
29
36
|
return output(await selectModel(pickerEndpoints(codexHome())));
|
package/package.json
CHANGED
package/src/config.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { readFileSync, readdirSync, mkdirSync, writeFileSync, renameSync, rmSync, existsSync, realpathSync, accessSync, constants, statSync, copyFileSync, mkdtempSync } from 'node:fs';
|
|
2
2
|
import { homedir } from 'node:os';
|
|
3
|
-
import { resolve, join, dirname
|
|
3
|
+
import { resolve, join, dirname } from 'node:path';
|
|
4
4
|
import { randomUUID, createHash } from 'node:crypto';
|
|
5
5
|
import { execFileSync } from 'node:child_process';
|
|
6
6
|
import { parseEnv } from 'node:util';
|
|
@@ -104,8 +104,8 @@ export function findApp() {
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
export function resolveRealCodex(selfPath) {
|
|
107
|
-
const candidate =
|
|
108
|
-
if (!candidate
|
|
107
|
+
const candidate = officialCodex();
|
|
108
|
+
if (!candidate) throw new Error('找不到 ChatGPT 内置 Codex。');
|
|
109
109
|
let actual;
|
|
110
110
|
try { actual = realpathSync(candidate); accessSync(actual, constants.X_OK); } catch { throw new Error('真正的 Codex 不存在或不可执行。'); }
|
|
111
111
|
if (actual === realpathSync(selfPath) || actual === realpathSync(process.execPath)) throw new Error('检测到路由递归调用。');
|
package/src/install.mjs
CHANGED
|
@@ -14,6 +14,8 @@ export const xml = value => value.replaceAll('&', '&').replaceAll('<', '<
|
|
|
14
14
|
const launchctl = (args, required = true) => { try { return execFileSync('/bin/launchctl', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] }).trim(); } catch { if (required) throw new Error(`launchctl ${args[0]} 失败。`); return null; } };
|
|
15
15
|
const defaultPlist = () => join(homedir(), 'Library/LaunchAgents', `${label}.plist`);
|
|
16
16
|
const sourceRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
|
|
17
|
+
const remoteStart = '# >>> minilink remote';
|
|
18
|
+
const remoteEnd = '# <<< minilink remote';
|
|
17
19
|
export function installation(root = installRoot()) {
|
|
18
20
|
try { return JSON.parse(readFileSync(join(root, 'installation.json'), 'utf8')); } catch (e) { if (e.code === 'ENOENT') return null; throw new Error('安装状态损坏,请检查 installation.json。'); }
|
|
19
21
|
}
|
|
@@ -51,6 +53,19 @@ export function syncLaunchEnvironment({ state = installation(), run = launchctl,
|
|
|
51
53
|
}
|
|
52
54
|
}
|
|
53
55
|
|
|
56
|
+
// The Desktop SSH launcher places CODEX_INSTALL_DIR first in PATH. It starts
|
|
57
|
+
// an interactive login shell with CODEX_REMOTE_PAYLOAD set before doing so.
|
|
58
|
+
// Keep the change scoped to that launcher and preserve the user's shell file.
|
|
59
|
+
export function syncRemoteEnvironment({ state = installation(), enabled, rc = join(homedir(), '.zshrc') } = {}) {
|
|
60
|
+
if (process.platform !== 'darwin' || !state?.remoteDir) return;
|
|
61
|
+
const existing = existsSync(rc) ? readFileSync(rc, 'utf8') : '';
|
|
62
|
+
const pattern = /(?:\n|^)# >>> minilink remote\n[\s\S]*?# <<< minilink remote\n?/g;
|
|
63
|
+
const clean = existing.replace(pattern, '\n').replace(/\n+$/, '');
|
|
64
|
+
const block = `${remoteStart}\nif [ -n "\${CODEX_REMOTE_PAYLOAD:-}" ] && [ -z "\${CODEX_INSTALL_DIR:-}" ]; then\n export CODEX_INSTALL_DIR=${shellQuote(state.remoteDir)}\nfi\n${remoteEnd}`;
|
|
65
|
+
const updated = (clean ? clean + '\n' : '') + (enabled ? block + '\n' : '');
|
|
66
|
+
if (updated !== existing) atomicFile(rc, updated, existsSync(rc) ? statSync(rc).mode & 0o777 : 0o644);
|
|
67
|
+
}
|
|
68
|
+
|
|
54
69
|
function atomicFile(path, text, mode) {
|
|
55
70
|
const temporary = `${path}.${process.pid}.tmp`;
|
|
56
71
|
try { writeFileSync(temporary, text, { mode }); renameSync(temporary, path); }
|
|
@@ -130,6 +145,8 @@ function installWindows({ root = installRoot(), source = sourceRoot, home = code
|
|
|
130
145
|
export function install({ root = installRoot(), plist = defaultPlist(), source = sourceRoot, home = codexHome(), config = configPath(), node = process.execPath, npm = process.env.npm_execpath, prefix = process.env.npm_config_prefix, run = launchctl, legacy = root === installRoot() ? legacyInstallation() : null } = {}) {
|
|
131
146
|
if (process.platform !== 'darwin') throw new Error('Desktop 接入目前仅支持 macOS。');
|
|
132
147
|
const previous = installation(root), wrapper = join(root, 'minilink');
|
|
148
|
+
const remoteDir = join(root, 'remote/bin'), remoteWrapper = join(remoteDir, 'codex');
|
|
149
|
+
const remoteCodex = join(home, 'packages/standalone/current/bin/codex');
|
|
133
150
|
const packageManager = npm && prefix ? { npm, prefix } : previous?.npm && previous?.prefix ? { npm: previous.npm, prefix: previous.prefix } : npmDetails();
|
|
134
151
|
if (existsSync(plist) && !previous) throw new Error('同名 LaunchAgent 已存在且不属于本次安装。');
|
|
135
152
|
const env = run(['getenv', 'CODEX_CLI_PATH'], false) || null;
|
|
@@ -138,7 +155,7 @@ export function install({ root = installRoot(), plist = defaultPlist(), source =
|
|
|
138
155
|
const runtime = mkdtempSync(join(root, 'releases', 'runtime-'));
|
|
139
156
|
const activate = join(root, 'activate.sh'), stateFile = join(root, 'installation.json');
|
|
140
157
|
const migrationFiles = legacy ? [legacy.state.wrapper, legacy.stateFile, legacy.plist, legacy.activate, config, `${config}.selection.json`] : [];
|
|
141
|
-
const snapshots = [...new Set([wrapper, activate, plist, stateFile, ...migrationFiles])].map(path => ({ path, data: existsSync(path) ? readFileSync(path) : null }));
|
|
158
|
+
const snapshots = [...new Set([wrapper, remoteWrapper, activate, plist, stateFile, ...migrationFiles])].map(path => ({ path, data: existsSync(path) ? readFileSync(path) : null }));
|
|
142
159
|
let migrationLocked = false, retired = false, touched = false;
|
|
143
160
|
try {
|
|
144
161
|
if (legacy) {
|
|
@@ -161,10 +178,12 @@ export function install({ root = installRoot(), plist = defaultPlist(), source =
|
|
|
161
178
|
}
|
|
162
179
|
if (JSON.parse(readFileSync(join(dependency, 'package.json'), 'utf8')).name !== 'smol-toml') throw new Error('smol-toml 依赖路径无效。');
|
|
163
180
|
cpSync(dependency, join(runtime, 'node_modules/smol-toml'), { recursive: true });
|
|
164
|
-
const state = { version:
|
|
181
|
+
const state = { version: 3, packageVersion: VERSION, source, runtime, wrapper, remoteDir, node, npm: packageManager.npm || previous?.npm, prefix: packageManager.prefix || previous?.prefix, codexHome: home, configPath: config, previousEnv: previous?.previousEnv || legacy?.state.previousEnv || null, installedAt: new Date().toISOString() };
|
|
165
182
|
// A Desktop process can retain this launcher after npm removes the state.
|
|
166
183
|
const fallback = `if [ ! -f ${shellQuote(stateFile)} ]; then\n exec "\${CODEX_DESKTOP_APP:-/Applications/ChatGPT.app}/${bundledCodexRelativePath}" "$@"\nfi`;
|
|
167
184
|
atomicFile(wrapper, `#!/bin/sh\n${fallback}\nexport CODEX_HOME=${shellQuote(home)}\nexport MINILINK_CONFIG=${shellQuote(config)}\nexec ${shellQuote(node)} ${shellQuote(join(runtime, 'bin/minilink.mjs'))} "$@"\n`, 0o755);
|
|
185
|
+
mkdirSync(remoteDir, { recursive: true, mode: 0o700 });
|
|
186
|
+
atomicFile(remoteWrapper, `#!/bin/sh\nif [ "$1" = app-server ] && [ "$2" = proxy ] && [ "$#" -eq 2 ] && [ -f ${shellQuote(join(runtime, 'bin/minilink.mjs'))} ]; then\n export CODEX_HOME=${shellQuote(home)}\n export MINILINK_CONFIG=${shellQuote(config)}\n export MINILINK_REMOTE_CODEX=${shellQuote(remoteCodex)}\n exec ${shellQuote(node)} ${shellQuote(join(runtime, 'bin/minilink.mjs'))} remote-proxy\nfi\nexec ${shellQuote(remoteCodex)} "$@"\n`, 0o755);
|
|
168
187
|
atomicFile(activate, `#!/bin/sh\nexec ${shellQuote(node)} ${shellQuote(join(runtime, 'src/lifecycle.mjs'))}\n`, 0o700);
|
|
169
188
|
mkdirSync(dirname(plist), { recursive: true });
|
|
170
189
|
atomicFile(plist, `<?xml version="1.0" encoding="UTF-8"?>\n<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">\n<plist version="1.0"><dict><key>Label</key><string>${label}</string><key>ProgramArguments</key><array><string>/bin/sh</string><string>${xml(activate)}</string></array><key>RunAtLoad</key><true/><key>StartInterval</key><integer>30</integer><key>LimitLoadToSessionType</key><string>Aqua</string></dict></plist>\n`, 0o600);
|
|
@@ -183,7 +202,7 @@ export function install({ root = installRoot(), plist = defaultPlist(), source =
|
|
|
183
202
|
return state;
|
|
184
203
|
} catch (error) {
|
|
185
204
|
if (!touched) { rmSync(runtime, { recursive: true, force: true }); throw error; }
|
|
186
|
-
for (const { path, data } of snapshots) { if (data === null) rmSync(path, { force: true }); else atomicFile(path, data, [wrapper, legacy?.state.wrapper].includes(path) ? 0o755 : [activate, legacy?.activate].includes(path) ? 0o700 : 0o600); }
|
|
205
|
+
for (const { path, data } of snapshots) { if (data === null) rmSync(path, { force: true }); else atomicFile(path, data, [wrapper, remoteWrapper, legacy?.state.wrapper].includes(path) ? 0o755 : [activate, legacy?.activate].includes(path) ? 0o700 : 0o600); }
|
|
187
206
|
run(['bootout', `gui/${process.getuid()}/${label}`], false);
|
|
188
207
|
if (previous) run(['bootstrap', `gui/${process.getuid()}`, plist], false);
|
|
189
208
|
if (retired) run(['bootstrap', `gui/${process.getuid()}`, legacy.plist], false);
|
package/src/proxy.mjs
CHANGED
|
@@ -3,7 +3,7 @@ import { runtimeDirectory } from './runtime.mjs';
|
|
|
3
3
|
import { randomUUID } from 'node:crypto';
|
|
4
4
|
import { spawn } from 'node:child_process';
|
|
5
5
|
import { join } from 'node:path';
|
|
6
|
-
import { rmSync, existsSync, readFileSync } from 'node:fs';
|
|
6
|
+
import { rmSync, existsSync, readFileSync, realpathSync, accessSync, constants } from 'node:fs';
|
|
7
7
|
import { JsonLineRouter } from './protocol.mjs';
|
|
8
8
|
import { ModelMenu } from './model-menu.mjs';
|
|
9
9
|
import { ProviderSwitch } from './provider-switch.mjs';
|
|
@@ -32,11 +32,13 @@ export function isStdioServer(args) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
export async function runProxy(args, selfPath) {
|
|
35
|
-
if ((Number.parseInt(setting('ACTIVE'), 10) || 0) >= 8) throw new Error('检测到 minilink
|
|
35
|
+
if ((Number.parseInt(setting('ACTIVE'), 10) || 0) >= 8) throw new Error('检测到 minilink 递归调用。');
|
|
36
36
|
const path = configPath();
|
|
37
37
|
const enabled = readConfig(path).enabled;
|
|
38
38
|
const hadConfig = existsSync(path);
|
|
39
|
-
const executable =
|
|
39
|
+
const executable = process.env.MINILINK_REMOTE_BRIDGE === '1' && process.env.MINILINK_REMOTE_CODEX
|
|
40
|
+
? realpathSync(process.env.MINILINK_REMOTE_CODEX) : resolveRealCodex(selfPath);
|
|
41
|
+
accessSync(executable, constants.X_OK);
|
|
40
42
|
const routed = enabled && isStdioServer(args);
|
|
41
43
|
const routes = routed ? discoverProfiles() : [];
|
|
42
44
|
const env = enabled ? childEnvironment(routes) : { ...process.env, MINILINK_ACTIVE: String((Number.parseInt(setting('ACTIVE'), 10) || 0) + 1) };
|
package/src/remote.mjs
ADDED
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
import { TextDecoder } from 'node:util';
|
|
5
|
+
import { readConfig } from './config.mjs';
|
|
6
|
+
|
|
7
|
+
const MAX_MESSAGE = 64 * 1024 * 1024;
|
|
8
|
+
const MAX_HEADER = 16 * 1024;
|
|
9
|
+
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
|
|
10
|
+
|
|
11
|
+
function frame(opcode, payload = Buffer.alloc(0)) {
|
|
12
|
+
const size = payload.length;
|
|
13
|
+
const header = size < 126 ? Buffer.from([0x80 | opcode, size])
|
|
14
|
+
: size < 65536 ? Buffer.from([0x80 | opcode, 126, size >> 8, size & 255])
|
|
15
|
+
: Buffer.from([0x80 | opcode, 127, 0, 0, 0, 0, (size / 0x1000000) & 255, (size >>> 16) & 255, (size >>> 8) & 255, size & 255]);
|
|
16
|
+
return Buffer.concat([header, payload]);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// The Codex SSH launcher prints its own eight-byte marker, then runs
|
|
20
|
+
// `codex app-server proxy`. That proxy carries a WebSocket handshake and
|
|
21
|
+
// frames over stdio. Bridge those frames to MiniLink's existing JSONL router.
|
|
22
|
+
export class RemoteBridge {
|
|
23
|
+
constructor({ input, output, backend }) {
|
|
24
|
+
Object.assign(this, { input, output, backend });
|
|
25
|
+
this.buffer = Buffer.alloc(0);
|
|
26
|
+
this.lines = Buffer.alloc(0);
|
|
27
|
+
this.handshaken = false;
|
|
28
|
+
this.fragment = null;
|
|
29
|
+
this.fragmentSize = 0;
|
|
30
|
+
this.closed = false;
|
|
31
|
+
input.on('data', chunk => this.receive(chunk));
|
|
32
|
+
input.on('end', () => backend.stdin.end());
|
|
33
|
+
input.on('error', () => backend.kill());
|
|
34
|
+
backend.stdout.on('data', chunk => this.backendOutput(chunk));
|
|
35
|
+
backend.stdout.on('error', () => backend.kill());
|
|
36
|
+
backend.stderr.pipe(process.stderr, { end: false });
|
|
37
|
+
backend.on('close', () => { this.closed = true; output.end(); });
|
|
38
|
+
backend.on('error', () => { this.close(1011); });
|
|
39
|
+
}
|
|
40
|
+
close(code = 1002) {
|
|
41
|
+
if (this.closed) return;
|
|
42
|
+
this.closed = true;
|
|
43
|
+
this.output.write(frame(8, Buffer.from([code >> 8, code & 255])));
|
|
44
|
+
this.backend.kill();
|
|
45
|
+
this.output.end();
|
|
46
|
+
}
|
|
47
|
+
receive(chunk) {
|
|
48
|
+
if (this.closed) return;
|
|
49
|
+
this.buffer = Buffer.concat([this.buffer, chunk]);
|
|
50
|
+
if (!this.handshaken) {
|
|
51
|
+
if (this.buffer.length > MAX_HEADER) return this.close();
|
|
52
|
+
const end = this.buffer.indexOf('\r\n\r\n');
|
|
53
|
+
if (end < 0) return;
|
|
54
|
+
const header = this.buffer.subarray(0, end + 4).toString('latin1');
|
|
55
|
+
const key = /^sec-websocket-key:\s*([^\r\n]+)\s*$/im.exec(header)?.[1]?.trim();
|
|
56
|
+
if (!/^GET [^\r\n]+ HTTP\/1\.1\r\n/i.test(header) || !/^upgrade:\s*websocket\s*$/im.test(header) || !/^connection:.*\bupgrade\b/im.test(header) || !key || Buffer.from(key, 'base64').length !== 16) return this.close();
|
|
57
|
+
const accept = createHash('sha1').update(key + GUID).digest('base64');
|
|
58
|
+
this.output.write(`HTTP/1.1 101 Switching Protocols\r\nconnection: Upgrade\r\nupgrade: websocket\r\nsec-websocket-accept: ${accept}\r\nx-codex-websocket-max-unfragmented-message-bytes: 16777216\r\n\r\n`);
|
|
59
|
+
this.buffer = this.buffer.subarray(end + 4);
|
|
60
|
+
this.handshaken = true;
|
|
61
|
+
}
|
|
62
|
+
while (this.buffer.length >= 2 && !this.closed) {
|
|
63
|
+
const first = this.buffer[0], second = this.buffer[1];
|
|
64
|
+
const opcode = first & 15, fin = Boolean(first & 0x80);
|
|
65
|
+
if (first & 0x70 || !(second & 0x80)) return this.close();
|
|
66
|
+
let length = second & 127, offset = 2;
|
|
67
|
+
if (length === 126) {
|
|
68
|
+
if (this.buffer.length < 4) return;
|
|
69
|
+
length = this.buffer.readUInt16BE(2); offset = 4;
|
|
70
|
+
} else if (length === 127) {
|
|
71
|
+
if (this.buffer.length < 10) return;
|
|
72
|
+
length = Number(this.buffer.readBigUInt64BE(2)); offset = 10;
|
|
73
|
+
}
|
|
74
|
+
if (!Number.isSafeInteger(length) || length > MAX_MESSAGE || this.buffer.length > MAX_MESSAGE + MAX_HEADER) return this.close(1009);
|
|
75
|
+
if (this.buffer.length < offset + 4 + length) return;
|
|
76
|
+
if (opcode >= 8 && (!fin || length > 125)) return this.close();
|
|
77
|
+
const mask = this.buffer.subarray(offset, offset + 4), payload = Buffer.from(this.buffer.subarray(offset + 4, offset + 4 + length));
|
|
78
|
+
for (let i = 0; i < payload.length; i++) payload[i] ^= mask[i % 4];
|
|
79
|
+
this.buffer = this.buffer.subarray(offset + 4 + length);
|
|
80
|
+
if (opcode === 8) { this.close(1000); return; }
|
|
81
|
+
if (opcode === 9) { this.output.write(frame(10, payload)); continue; }
|
|
82
|
+
if (opcode === 10) continue;
|
|
83
|
+
if (opcode === 1) {
|
|
84
|
+
if (this.fragment) return this.close();
|
|
85
|
+
this.fragment = [payload]; this.fragmentSize = payload.length;
|
|
86
|
+
} else if (opcode === 0 && this.fragment) this.fragment.push(payload);
|
|
87
|
+
else return this.close();
|
|
88
|
+
if (opcode === 0) this.fragmentSize += payload.length;
|
|
89
|
+
if (this.fragmentSize > MAX_MESSAGE) return this.close(1009);
|
|
90
|
+
if (fin) {
|
|
91
|
+
const message = Buffer.concat(this.fragment);
|
|
92
|
+
this.fragment = null; this.fragmentSize = 0;
|
|
93
|
+
let normalized;
|
|
94
|
+
try { normalized = JSON.stringify(JSON.parse(new TextDecoder('utf-8', { fatal: true }).decode(message))); }
|
|
95
|
+
catch { return this.close(1007); }
|
|
96
|
+
this.backend.stdin.write(normalized);
|
|
97
|
+
this.backend.stdin.write('\n');
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
backendOutput(chunk) {
|
|
102
|
+
this.lines = Buffer.concat([this.lines, chunk]);
|
|
103
|
+
if (this.lines.length > MAX_MESSAGE) return this.close(1009);
|
|
104
|
+
let end;
|
|
105
|
+
while ((end = this.lines.indexOf(10)) >= 0) {
|
|
106
|
+
const line = this.lines.subarray(0, end);
|
|
107
|
+
this.lines = this.lines.subarray(end + 1);
|
|
108
|
+
if (line.length) this.output.write(frame(1, line));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function runRemoteProxy() {
|
|
114
|
+
if (!readConfig().enabled) {
|
|
115
|
+
const standalone = process.env.MINILINK_REMOTE_CODEX;
|
|
116
|
+
if (!standalone) throw new Error('找不到远程 Codex。');
|
|
117
|
+
const child = spawn(standalone, ['app-server', 'proxy'], { stdio: 'inherit', env: process.env });
|
|
118
|
+
return new Promise(resolve => { child.on('close', code => resolve(code ?? 1)); child.on('error', () => resolve(1)); });
|
|
119
|
+
}
|
|
120
|
+
const self = fileURLToPath(new URL('../bin/minilink.mjs', import.meta.url));
|
|
121
|
+
const backend = spawn(process.execPath, [self, 'app-server', '--stdio'], { stdio: ['pipe', 'pipe', 'pipe'], env: { ...process.env, MINILINK_REMOTE_BRIDGE: '1' } });
|
|
122
|
+
new RemoteBridge({ input: process.stdin, output: process.stdout, backend });
|
|
123
|
+
for (const signal of ['SIGINT', 'SIGTERM', 'SIGHUP']) process.on(signal, () => backend.kill(signal));
|
|
124
|
+
return new Promise(resolve => backend.on('close', code => resolve(code ?? 1)));
|
|
125
|
+
}
|