@walkhi/code-relax 0.1.0-beta.4 → 0.1.0-beta.5
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
CHANGED
|
@@ -80,4 +80,4 @@ npm uninstall -g @walkhi/code-relax
|
|
|
80
80
|
|
|
81
81
|
## 发布边界
|
|
82
82
|
|
|
83
|
-
registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.
|
|
83
|
+
registry 当前公开包为 `@walkhi/code-relax@0.1.0-beta.5`,主 CLI 为 `relax`,并保留 `code-relax` 兼容入口。该版让 WMI 所有的 Node broker 在目标进程期间保留隐藏控制台,避免 Codex 调用工具时反复弹出控制台窗口;同时沿用官方中继自助登记和持久安装日志,通信协议仍为 7。详细规则见 [npm 分发](../docs/npm-distribution.md)。
|
|
@@ -17,7 +17,6 @@ try {
|
|
|
17
17
|
const descriptors = (spec.logs || []).map(file => fs.openSync(file, 'a'));
|
|
18
18
|
try {
|
|
19
19
|
const child = spawn(spec.executable, spec.args || [], {
|
|
20
|
-
detached: true,
|
|
21
20
|
windowsHide: true,
|
|
22
21
|
env: spec.environment,
|
|
23
22
|
stdio: ['ignore', descriptors[0] ?? 'ignore', descriptors[1] ?? 'ignore'],
|
|
@@ -26,7 +25,8 @@ try {
|
|
|
26
25
|
child.once('spawn', resolve);
|
|
27
26
|
child.once('error', reject);
|
|
28
27
|
});
|
|
29
|
-
|
|
28
|
+
// Keep this WMI-owned broker alive with the target so Codex tools inherit
|
|
29
|
+
// its hidden console instead of creating a visible console per command.
|
|
30
30
|
result.pid = child.pid;
|
|
31
31
|
diagnostic(`target started (PID ${child.pid})`);
|
|
32
32
|
} finally {
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import fs from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { execFile } from 'node:child_process';
|
|
4
|
-
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
-
import { startBackground, processAlive } from '../background-process.mjs';
|
|
6
|
-
import { promisify } from 'node:util';
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
4
|
+
import { setTimeout as delay } from 'node:timers/promises';
|
|
5
|
+
import { startBackground, processAlive } from '../background-process.mjs';
|
|
6
|
+
import { promisify } from 'node:util';
|
|
7
7
|
import { randomBytes, randomUUID } from 'node:crypto';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
9
|
import { hostname } from 'node:os';
|
|
@@ -16,8 +16,8 @@ import { connectBridgeRelay } from './connector.mjs';
|
|
|
16
16
|
import { validProbeIceServers } from './p2p-probe.mjs';
|
|
17
17
|
import { relayAddress, validSecret } from './wire.mjs';
|
|
18
18
|
import { saveLanAuthorizations } from './lan-authorizations.mjs';
|
|
19
|
-
import { lanNetworks } from '../platform/windows/network-info.mjs';
|
|
20
|
-
|
|
19
|
+
import { lanNetworks } from '../platform/windows/network-info.mjs';
|
|
20
|
+
|
|
21
21
|
const exec = promisify(execFile);
|
|
22
22
|
const script = fileURLToPath(import.meta.url);
|
|
23
23
|
const recordPath = root => path.join(root, 'self-relay-session.json');
|
|
@@ -32,34 +32,33 @@ async function loadConfig(stateRoot) {
|
|
|
32
32
|
if (config.p2pWindowsRouteHelper !== undefined && typeof config.p2pWindowsRouteHelper !== 'boolean') throw new Error('p2pWindowsRouteHelper 必须是布尔值');
|
|
33
33
|
if (config.p2pWindowsRouteHelper && !/^[A-Za-z0-9_-]{32,128}$/.test(config.p2pWindowsRouteHelperToken || '')) throw new Error('p2pWindowsRouteHelperToken 无效');
|
|
34
34
|
const url = process.env.RELAY_URL || config.url;
|
|
35
|
-
if (!url) throw new Error('请先配置 self-relay-config.json 的 url 和 hostKeyFile(或 sshHost、sshKeyPath),详见自建中继文档。');
|
|
35
|
+
if (!url) throw new Error('请先配置 self-relay-config.json 的 url 和 hostKeyFile(或 sshHost、sshKeyPath),详见自建中继文档。');
|
|
36
36
|
relayAddress(url);
|
|
37
37
|
const resolved = relayAddress(url);
|
|
38
38
|
const pairingUrl = config.pairingUrl || `${resolved.startsWith('wss:') ? 'https:' : 'http:'}//${new URL(resolved).host}/pair`;
|
|
39
39
|
const pairing = new URL(pairingUrl);
|
|
40
40
|
if (!['https:', 'http:'].includes(pairing.protocol) || pairing.pathname !== '/pair' || pairing.search || pairing.hash || pairing.username || pairing.password ||
|
|
41
41
|
(pairing.protocol === 'http:' && pairing.hostname !== '127.0.0.1')) throw new Error('self-relay-config.json 的 pairingUrl 必须是 HTTPS /pair 地址。');
|
|
42
|
-
return { ...config, url: resolved, pairingUrl: pairing.href };
|
|
43
|
-
}
|
|
42
|
+
return { ...config, url: resolved, pairingUrl: pairing.href };
|
|
43
|
+
}
|
|
44
44
|
async function registrationKey(config) {
|
|
45
45
|
if (config.url === officialConfig.url) return undefined;
|
|
46
46
|
const hostKeyFile = process.env.RELAY_HOST_KEY_FILE || config.hostKeyFile;
|
|
47
|
-
let hostKey;
|
|
48
|
-
if (hostKeyFile) hostKey = fs.readFileSync(hostKeyFile, 'utf8').trim();
|
|
49
|
-
else {
|
|
50
|
-
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(config.sshHost || '') ||
|
|
51
|
-
!/^\/[a-zA-Z0-9/_.-]+$/.test(config.sshKeyPath || '')) throw new Error('中继注册凭据配置不完整或无效。');
|
|
52
|
-
try {
|
|
53
|
-
const result = await exec('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', config.sshHost,
|
|
54
|
-
`sudo -n cat ${config.sshKeyPath}`], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
55
|
-
hostKey = result.stdout.trim();
|
|
56
|
-
} catch { throw new Error('无法通过已配置 SSH 读取中继注册凭据。'); }
|
|
57
|
-
}
|
|
58
|
-
if (!validSecret(hostKey)) throw new Error('中继注册凭据格式无效。');
|
|
59
|
-
return hostKey;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export async function selfRelay(command, stateRoot, requiredMode = '') {
|
|
47
|
+
let hostKey;
|
|
48
|
+
if (hostKeyFile) hostKey = fs.readFileSync(hostKeyFile, 'utf8').trim();
|
|
49
|
+
else {
|
|
50
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(config.sshHost || '') ||
|
|
51
|
+
!/^\/[a-zA-Z0-9/_.-]+$/.test(config.sshKeyPath || '')) throw new Error('中继注册凭据配置不完整或无效。');
|
|
52
|
+
try {
|
|
53
|
+
const result = await exec('ssh', ['-o', 'BatchMode=yes', '-o', 'ConnectTimeout=10', config.sshHost,
|
|
54
|
+
`sudo -n cat ${config.sshKeyPath}`], { encoding: 'utf8', timeout: 15000, windowsHide: true });
|
|
55
|
+
hostKey = result.stdout.trim();
|
|
56
|
+
} catch { throw new Error('无法通过已配置 SSH 读取中继注册凭据。'); }
|
|
57
|
+
}
|
|
58
|
+
if (!validSecret(hostKey)) throw new Error('中继注册凭据格式无效。');
|
|
59
|
+
return hostKey;
|
|
60
|
+
}
|
|
61
|
+
export async function selfRelay(command, stateRoot, requiredMode = '') {
|
|
63
62
|
const previous = readRecord(recordPath(stateRoot));
|
|
64
63
|
let current;
|
|
65
64
|
if (previous) {
|
|
@@ -139,12 +138,12 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
|
|
|
139
138
|
fs.rmSync(legacyPath);
|
|
140
139
|
}
|
|
141
140
|
let identity = identities.servers[config.url];
|
|
142
|
-
if (!identity) {
|
|
143
|
-
identity = { id: randomUUID(), secret: randomBytes(32).toString('base64url'), registered: false };
|
|
144
|
-
identities.servers[config.url] = identity; await credentialStore('save', identities);
|
|
145
|
-
}
|
|
146
|
-
const hostKey = identity.registered ? undefined : await registrationKey(config);
|
|
147
|
-
let bridgeError = '';
|
|
141
|
+
if (!identity) {
|
|
142
|
+
identity = { id: randomUUID(), secret: randomBytes(32).toString('base64url'), registered: false };
|
|
143
|
+
identities.servers[config.url] = identity; await credentialStore('save', identities);
|
|
144
|
+
}
|
|
145
|
+
const hostKey = identity.registered ? undefined : await registrationKey(config);
|
|
146
|
+
let bridgeError = '';
|
|
148
147
|
async function refreshHealth() {
|
|
149
148
|
try { health = await serviceHealth(session()) || { transportMode: health.transportMode, status: 'offline', desktopConnected: false }; bridgeError = ''; }
|
|
150
149
|
catch (error) { bridgeError = error.message; health = { transportMode: health.transportMode, status: 'offline', desktopConnected: false }; }
|
|
@@ -204,7 +203,7 @@ async function worker(stateRoot, instanceId, refresh = false, requiredMode = '')
|
|
|
204
203
|
await refreshHealth();
|
|
205
204
|
const lanCandidates = await currentLanCandidates();
|
|
206
205
|
connector?.events.removeAllListeners(); connector?.close();
|
|
207
|
-
const current = connector = connectBridgeRelay({ relayUrl: config.url, hostKey: identity.registered ? undefined : hostKey, identity,
|
|
206
|
+
const current = connector = connectBridgeRelay({ relayUrl: config.url, hostKey: identity.registered ? undefined : hostKey, identity,
|
|
208
207
|
bridgeUrl: `ws://127.0.0.1:${bridge?.port || 45831}/api/socket`, bridgeToken: bridge?.accessUrl ? new URL(bridge.accessUrl).searchParams.get('token') : 'unavailable',
|
|
209
208
|
bridgeConnection: () => { const latest = session(); return { url: `ws://127.0.0.1:${latest.port}/api/socket`, token: new URL(latest.accessUrl).searchParams.get('token') }; },
|
|
210
209
|
hostName: hostname(), lanCandidates, p2pProbe: config.p2pProbe === true, iceServers: config.p2pIceServers || [],
|