@ai-workbench/network 0.4.1 → 0.4.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/NOTICE.md +2 -0
- package/README.md +31 -4
- package/helpers/darwin-arm64/NOTICE.md +20 -0
- package/helpers/darwin-arm64/README.md +1 -0
- package/helpers/darwin-arm64/THIRD_PARTY_NOTICES.txt +3006 -0
- package/helpers/darwin-arm64/bin/aw-net +0 -0
- package/helpers/darwin-arm64/helper-manifest.json +1 -0
- package/helpers/darwin-arm64/licenses-manifest.json +2505 -0
- package/helpers/darwin-arm64/package.json +16 -0
- package/helpers/darwin-x64/NOTICE.md +20 -0
- package/helpers/darwin-x64/README.md +1 -0
- package/helpers/darwin-x64/THIRD_PARTY_NOTICES.txt +3061 -0
- package/helpers/darwin-x64/bin/aw-net +0 -0
- package/helpers/darwin-x64/helper-manifest.json +1 -0
- package/helpers/darwin-x64/licenses-manifest.json +2521 -0
- package/helpers/darwin-x64/package.json +16 -0
- package/lib/activation.js +7 -5
- package/lib/api.d.ts +6 -0
- package/lib/control.js +3 -1
- package/lib/download.js +8 -2
- package/lib/index.js +13 -6
- package/lib/network.js +27 -12
- package/lib/observation.d.ts +6 -0
- package/lib/observation.js +14 -0
- package/lib/packaged-helper.js +18 -14
- package/lib/settings.d.ts +10 -0
- package/lib/settings.js +34 -0
- package/package.json +3 -6
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ai-workbench/net-helper-darwin-x64",
|
|
3
|
+
"version": "0.4.3",
|
|
4
|
+
"description": "远程主机网络助手(macOS x64,仅随network主包内嵌分发)",
|
|
5
|
+
"os": [
|
|
6
|
+
"darwin"
|
|
7
|
+
],
|
|
8
|
+
"cpu": [
|
|
9
|
+
"x64"
|
|
10
|
+
],
|
|
11
|
+
"repository": {
|
|
12
|
+
"type": "git",
|
|
13
|
+
"url": "git+https://github.com/yzsnstotz/ai-workbench-network.git"
|
|
14
|
+
},
|
|
15
|
+
"private": true
|
|
16
|
+
}
|
package/lib/activation.js
CHANGED
|
@@ -2,7 +2,7 @@ import { randomUUID } from 'node:crypto';
|
|
|
2
2
|
import { isAbsolute } from 'node:path';
|
|
3
3
|
import { createNetwork } from "./network.js";
|
|
4
4
|
import { fail, ok } from "./api.js";
|
|
5
|
-
|
|
5
|
+
import { settingsFields as fields, validHelperPolicy, mergedSettings } from "./settings.js";
|
|
6
6
|
function normalized(s) {
|
|
7
7
|
if (!s || typeof s !== 'object' || Object.keys(s).some(k => !fields.includes(k)))
|
|
8
8
|
return;
|
|
@@ -13,7 +13,9 @@ function normalized(s) {
|
|
|
13
13
|
return;
|
|
14
14
|
if (!Number.isSafeInteger(s.controlResponseMaxBytes) || s.controlResponseMaxBytes <= 0 || !Number.isSafeInteger(s.downloadMaxRedirects) || s.downloadMaxRedirects < 0)
|
|
15
15
|
return;
|
|
16
|
-
|
|
16
|
+
if (!validHelperPolicy(s))
|
|
17
|
+
return;
|
|
18
|
+
return Object.fromEntries(fields.filter(k => s[k] !== undefined).map(k => [k, s[k]]));
|
|
17
19
|
}
|
|
18
20
|
// One stable Cordis service; callers never own, restart or borrow helper processes.
|
|
19
21
|
export function createActivationService(initial, executable, emit = () => { }, factory = createNetwork) {
|
|
@@ -104,7 +106,7 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
104
106
|
}
|
|
105
107
|
const api = {
|
|
106
108
|
async start() { if (disposed)
|
|
107
|
-
return; if (
|
|
109
|
+
return; if (initial.configFile || initial.helperLogFile)
|
|
108
110
|
await api.activate(initial, new AbortController().signal);
|
|
109
111
|
else
|
|
110
112
|
emit(structuredClone(network)); },
|
|
@@ -114,9 +116,9 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
114
116
|
async activate(settings, signal) {
|
|
115
117
|
if (disposed || signal.aborted)
|
|
116
118
|
return fail('ACTIVATION_CANCELLED', '连接激活等待已取消。', '查询当前激活结果;不要重复提交密钥');
|
|
117
|
-
const config = normalized(settings);
|
|
119
|
+
const config = normalized(mergedSettings(initial, settings));
|
|
118
120
|
if (!config)
|
|
119
|
-
return fail('SETTINGS_REQUIRED', '我需要有效的连接配置。', '
|
|
121
|
+
return fail('SETTINGS_REQUIRED', '我需要有效的连接配置。', '管理员核对网络插件具名设置及已导入的连接资料');
|
|
120
122
|
const key = JSON.stringify(config);
|
|
121
123
|
if (selected !== undefined && selected !== key)
|
|
122
124
|
return fail('ACTIVATION_CONFLICT', '已有连接激活,不能替换身份。', '管理员核对当前开通上下文');
|
package/lib/api.d.ts
CHANGED
|
@@ -90,6 +90,12 @@ export interface PluginSettings {
|
|
|
90
90
|
shutdownObservationSeconds?: number;
|
|
91
91
|
controlResponseMaxBytes?: number;
|
|
92
92
|
downloadMaxRedirects?: number;
|
|
93
|
+
statusRefreshIntervalSeconds?: number;
|
|
94
|
+
dialTimeoutSeconds?: number;
|
|
95
|
+
headerTimeoutSeconds?: number;
|
|
96
|
+
enrollmentObservationSeconds?: number;
|
|
97
|
+
httpMaxHeaderBytes?: number;
|
|
98
|
+
httpMaxResponseHeaderBytes?: number;
|
|
93
99
|
}
|
|
94
100
|
export interface HelperConfig {
|
|
95
101
|
apiVersion: 1;
|
package/lib/control.js
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import { privateSocket } from "./private.js";
|
|
3
|
+
import { ObservationExpired } from "./observation.js";
|
|
3
4
|
export async function control(socket, method, path, signal, seconds, maxHeaderBytes) {
|
|
4
5
|
await privateSocket(socket);
|
|
5
6
|
signal.throwIfAborted();
|
|
6
7
|
return new Promise((resolve, reject) => {
|
|
7
8
|
const req = http.request({ socketPath: socket, method, path, agent: false, signal, maxHeaderSize: maxHeaderBytes }, res => { req.setTimeout(0); resolve(res); });
|
|
8
|
-
|
|
9
|
+
const started = performance.now();
|
|
10
|
+
req.setTimeout(seconds * 1000, () => req.destroy(new ObservationExpired('CONTROL_OBSERVATION_EXPIRED', 'dialTimeoutSeconds', seconds, started, path === '/v1/status' ? '网络组件状态响应头' : '本机控制响应头', '检查自有组件私有日志、控制socket和当前网络状态;不要重交开通材料')));
|
|
9
11
|
req.on('error', reject);
|
|
10
12
|
req.end();
|
|
11
13
|
});
|
package/lib/download.js
CHANGED
|
@@ -4,6 +4,7 @@ import tls from 'node:tls';
|
|
|
4
4
|
import { isIP } from 'node:net';
|
|
5
5
|
import { Readable } from 'node:stream';
|
|
6
6
|
import { fail, ok } from "./api.js";
|
|
7
|
+
import { ObservationExpired, observationNeed } from "./observation.js";
|
|
7
8
|
function sourceURL(source) { const u = new URL(source); if (u.protocol !== 'https:' || u.username || u.password)
|
|
8
9
|
throw new Error('DOWNLOAD_SOURCE_INVALID'); return u; }
|
|
9
10
|
async function requestViaProxy(target, lease, signal, seconds, maxHeaderBytes) {
|
|
@@ -14,7 +15,8 @@ async function requestViaProxy(target, lease, signal, seconds, maxHeaderBytes) {
|
|
|
14
15
|
const authority = (isIP(hostname) === 6 ? '[' + hostname + ']' : hostname) + ':' + (target.port || '443');
|
|
15
16
|
const tunnel = await new Promise((resolve, reject) => {
|
|
16
17
|
const req = http.request({ hostname: proxy.hostname, port: proxy.port, method: 'CONNECT', path: authority, agent: false, signal, maxHeaderSize: maxHeaderBytes, headers: { 'Proxy-Authorization': 'Basic ' + Buffer.from(lease.credentials.username + ':' + lease.credentials.password).toString('base64') } });
|
|
17
|
-
|
|
18
|
+
const started = performance.now();
|
|
19
|
+
req.setTimeout(seconds * 1000, () => req.destroy(new ObservationExpired('DOWNLOAD_CONNECT_EXPIRED', 'dialTimeoutSeconds', seconds, started, '主机代理CONNECT响应头', '检查网络组件私有日志、主机SSH状态及代理握手;不重试或改走本机出口')));
|
|
18
20
|
req.on('connect', (response, socket, head) => { req.setTimeout(0); if (response.statusCode !== 200) {
|
|
19
21
|
socket.destroy();
|
|
20
22
|
reject(new Error('HOST_PROXY_REJECTED'));
|
|
@@ -31,7 +33,8 @@ async function requestViaProxy(target, lease, signal, seconds, maxHeaderBytes) {
|
|
|
31
33
|
try {
|
|
32
34
|
return await new Promise((resolve, reject) => {
|
|
33
35
|
const req = https.request(target, { agent, signal, maxHeaderSize: maxHeaderBytes }, response => { req.setTimeout(0); response.once('close', () => agent.destroy()); resolve(response); });
|
|
34
|
-
|
|
36
|
+
const started = performance.now();
|
|
37
|
+
req.setTimeout(seconds * 1000, () => req.destroy(new ObservationExpired('DOWNLOAD_HEADERS_EXPIRED', 'dialTimeoutSeconds', seconds, started, '经主机下载的TLS握手及HTTP响应头', '检查主机出口、TLS与下载源响应;不重试或静默换源')));
|
|
35
38
|
req.on('error', reject);
|
|
36
39
|
req.end();
|
|
37
40
|
});
|
|
@@ -116,6 +119,9 @@ export async function downloadThroughProxy(source, lease, signal, maxRedirects,
|
|
|
116
119
|
}
|
|
117
120
|
catch (error) {
|
|
118
121
|
await lease.release();
|
|
122
|
+
const observed = observationNeed(error);
|
|
123
|
+
if (observed)
|
|
124
|
+
return { ok: false, error: observed };
|
|
119
125
|
const code = error instanceof Error && ['DOWNLOAD_REDIRECT_LIMIT', 'DOWNLOAD_SOURCE_INVALID'].includes(error.message) ? error.message : 'HOST_DOWNLOAD_FAILED';
|
|
120
126
|
return fail(code, '经主机下载失败,根因未查明', '检查主机出口、下载源和具名重定向上限;无本机直出替代');
|
|
121
127
|
}
|
package/lib/index.js
CHANGED
|
@@ -2,15 +2,22 @@ import z from '@deepseek-ai/schemastery';
|
|
|
2
2
|
import { createActivationService } from "./activation.js";
|
|
3
3
|
import { resolvePackagedHelper } from "./packaged-helper.js";
|
|
4
4
|
export const name = 'ai-workbench-network';
|
|
5
|
-
const positive = () => z.number().min(0.001);
|
|
5
|
+
const positive = () => z.number().min(0.001).max(2147483.647);
|
|
6
|
+
const bytes = () => z.number().step(1).min(1).max(Number.MAX_SAFE_INTEGER);
|
|
6
7
|
export const Config = z.object({
|
|
7
8
|
configFile: z.string().description('远程主机连接配置文件(由插件提供;无默认值)'),
|
|
8
9
|
helperLogFile: z.string().description('远程主机连接诊断日志文件(无默认值)'),
|
|
9
|
-
startupObservationSeconds: positive().description('
|
|
10
|
-
shutdownObservationSeconds: positive().description('
|
|
11
|
-
controlResponseMaxBytes:
|
|
12
|
-
downloadMaxRedirects: z.number().step(1).min(0).description('
|
|
13
|
-
|
|
10
|
+
startupObservationSeconds: positive().description('启动观察 startupObservationSeconds(秒):默认未设,沿开通值(现值35);等待组件ready仅为可观测性;增大等更久,减小可能提前报未知;无关闭值,超时给等待事项、已等时长和排查步骤'),
|
|
11
|
+
shutdownObservationSeconds: positive().description('退出观察 shutdownObservationSeconds(秒):默认未设,沿开通值(现值10);等待自有组件正常退出,仅为可观测性;增大清理等更久,减小可能强制结束并报告;无关闭值,不终止未知进程'),
|
|
12
|
+
controlResponseMaxBytes: bytes().description('控制响应上限 controlResponseMaxBytes(字节):默认未设,沿开通值(现值65536);控制JSON、ready及代理首帧;增大增加内存,减小可能拒绝合法帧;无关闭或无限值,不限制下载正文'),
|
|
13
|
+
downloadMaxRedirects: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).description('下载重定向上限 downloadMaxRedirects:默认未设,沿开通值(现值3);增大允许更多HTTPS跳转,减小可能拒绝下载,0关闭跳转;不自动换源'),
|
|
14
|
+
statusRefreshIntervalSeconds: positive().description('状态刷新间隔 statusRefreshIntervalSeconds(秒):默认沿已开通配置(现值2);影响组件状态查询及插件轮询;增大提示更迟,减小增加查询开销;无关闭值,不代表失败重试已修复'),
|
|
15
|
+
dialTimeoutSeconds: positive().description('拨号观察 dialTimeoutSeconds(秒):默认沿已开通配置(现值20);远端TCP/SSH、控制响应头、下载CONNECT/响应头;Node请求采用阶段空闲观察,仅为可观测性,增大等更久,减小可能提前中止;无关闭值,超时说明等待事项、已等时长、下一步检查,不是修复'),
|
|
16
|
+
headerTimeoutSeconds: z.number().min(1e-9).max(9223372036.854774).description('请求头观察 headerTimeoutSeconds(秒):默认沿已开通配置(现值10);本机控制与登录代理请求头;仅为可观测性,增大保留连接更久,减小可能拒绝慢请求头;无关闭值,超时在私有日志说明等待事项、已等时长、下一步检查'),
|
|
17
|
+
enrollmentObservationSeconds: z.number().min(1e-9).max(9223372036.854774).description('首次入网观察 enrollmentObservationSeconds(秒):默认沿已开通配置(现值30);仅观察首次开通结果,不用于删除已有身份;增大等更久,减小可能更早报未知;无关闭值,超时说明等待事项、已等时长、下一步检查,不重交开通材料'),
|
|
18
|
+
httpMaxHeaderBytes: bytes().description('本机HTTP头上限 httpMaxHeaderBytes(字节):默认沿已开通配置(现值1048576);控制/登录代理请求头及控制响应头;增大增加解析内存,减小可能拒绝合法头;无关闭或无限值,不是正文上限'),
|
|
19
|
+
httpMaxResponseHeaderBytes: bytes().description('下载HTTP响应头上限 httpMaxResponseHeaderBytes(字节):默认沿已开通配置(现值1048576);主机代理及下载响应头/CONNECT头;增大增加解析内存,减小可能拒绝合法头;无关闭或无限值,不是正文上限'),
|
|
20
|
+
}).description('运行策略优先级:本插件显式值 > 开通方传入值 > 原私有配置;未设置沿用原值,不注入新数值默认。活动连接改策略须正常重载插件,不重交开通材料。固定正确性约束不可关闭:macOS控制socket路径≤103字节、端口1–65535、计时器/整数可表示范围、私有700/600权限、当前用户与单身份隔离、SHA256及加密随机凭据强度。');
|
|
14
21
|
export function register(ctx, api) {
|
|
15
22
|
ctx.provide('aiWorkbenchNetwork', api);
|
|
16
23
|
ctx.effect(() => { void api.start(); return () => api.dispose(); });
|
package/lib/network.js
CHANGED
|
@@ -5,6 +5,8 @@ import { privateFile, verifyExecutable } from "./private.js";
|
|
|
5
5
|
import { control, jsonResponse, firstFrame } from "./control.js";
|
|
6
6
|
import { fail, ok } from "./api.js";
|
|
7
7
|
import { downloadThroughProxy } from "./download.js";
|
|
8
|
+
import { effectiveConfig, helperPolicyArgs, validHelperPolicy } from "./settings.js";
|
|
9
|
+
import { ObservationExpired, observationNeed } from "./observation.js";
|
|
8
10
|
const seconds = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0.001 && v * 1000 <= 2147483647;
|
|
9
11
|
const integer = (v) => Number.isSafeInteger(v) && Number(v) > 0;
|
|
10
12
|
const quote = (s) => "'" + s.replaceAll("'", "'\\''") + "'";
|
|
@@ -39,7 +41,7 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
39
41
|
let state = { phase: 'starting' }, cfg, child;
|
|
40
42
|
let starting, disposing, disposed = false, online = false;
|
|
41
43
|
let poll;
|
|
42
|
-
let processFailed = false, startupReceipt, terminalNeed, stoppingChild;
|
|
44
|
+
let processFailed = false, startupReceipt, terminalNeed, stoppingChild, shutdownNeed;
|
|
43
45
|
const childGone = new AbortController();
|
|
44
46
|
const lifetime = new AbortController(), listeners = new Set(), updates = new Set();
|
|
45
47
|
const leases = new Set();
|
|
@@ -72,9 +74,10 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
72
74
|
if (!owned?.pid || owned.exitCode !== null || owned.signalCode !== null)
|
|
73
75
|
return false;
|
|
74
76
|
const exit = new Promise(resolve => owned.once('exit', () => resolve()));
|
|
77
|
+
const started = performance.now();
|
|
75
78
|
owned.kill('SIGTERM');
|
|
76
79
|
let forced = false, timer;
|
|
77
|
-
await Promise.race([exit, new Promise(resolve => { timer = setTimeout(() => { forced = true; owned.kill('SIGKILL'); resolve(); }, settings.shutdownObservationSeconds * 1000); })]);
|
|
80
|
+
await Promise.race([exit, new Promise(resolve => { timer = setTimeout(() => { forced = true; shutdownNeed = observationNeed(new ObservationExpired('HELPER_SHUTDOWN_FORCED', 'shutdownObservationSeconds', settings.shutdownObservationSeconds, started, '自有网络组件正常退出', '检查组件退出日志;本次仅强制结束自有进程,不终止未知进程')); owned.kill('SIGKILL'); resolve(); }, settings.shutdownObservationSeconds * 1000); })]);
|
|
78
81
|
if (timer)
|
|
79
82
|
clearTimeout(timer);
|
|
80
83
|
if (forced)
|
|
@@ -94,9 +97,14 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
94
97
|
if (!disposed && alive())
|
|
95
98
|
publish({ phase: s.enrollment === 'online' && !s.error ? 'online' : s.error ? 'blocked' : 'starting', status: s, ...s.error ? { error: s.error } : {} });
|
|
96
99
|
}
|
|
97
|
-
catch {
|
|
98
|
-
if (!disposed && alive())
|
|
99
|
-
|
|
100
|
+
catch (error) {
|
|
101
|
+
if (!disposed && alive()) {
|
|
102
|
+
const observed = observationNeed(error);
|
|
103
|
+
if (observed)
|
|
104
|
+
publish({ phase: 'blocked', error: observed });
|
|
105
|
+
else
|
|
106
|
+
block('CONTROL_UNAVAILABLE', '连接中断,正在等待网络组件恢复;根因未查明。', '检查组件私有日志、控制socket和当前网络;状态刷新不是修复,不重启或重交材料');
|
|
107
|
+
}
|
|
100
108
|
}
|
|
101
109
|
if (!disposed && alive())
|
|
102
110
|
poll = setTimeout(() => void observe(), cfg.statusRefreshIntervalSeconds * 1000);
|
|
@@ -119,6 +127,9 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
119
127
|
finally {
|
|
120
128
|
await file.close();
|
|
121
129
|
}
|
|
130
|
+
if (!validHelperPolicy(settings))
|
|
131
|
+
throw new Error('CONFIG_INVALID');
|
|
132
|
+
cfg = effectiveConfig(cfg, settings);
|
|
122
133
|
if (normalize(settings.helperLogFile) !== settings.helperLogFile || normalize(settings.configFile) !== settings.configFile)
|
|
123
134
|
throw new Error('CONFIG_INVALID');
|
|
124
135
|
const canonical = async (p) => { try {
|
|
@@ -175,7 +186,7 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
175
186
|
return;
|
|
176
187
|
}
|
|
177
188
|
try {
|
|
178
|
-
child = spawn(executable.path, ['serve', '--config', settings.configFile], { stdio: ['ignore', 'pipe', log.fd], env: { ...process.env } });
|
|
189
|
+
child = spawn(executable.path, ['serve', '--config', settings.configFile, ...helperPolicyArgs(settings)], { stdio: ['ignore', 'pipe', log.fd], env: { ...process.env } });
|
|
179
190
|
}
|
|
180
191
|
catch (error) {
|
|
181
192
|
await log.close();
|
|
@@ -196,7 +207,8 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
196
207
|
processChild.once('exit', exited);
|
|
197
208
|
const ready = new Promise((resolve, reject) => {
|
|
198
209
|
let data = Buffer.alloc(0), settled = false;
|
|
199
|
-
const
|
|
210
|
+
const started = performance.now();
|
|
211
|
+
const timer = setTimeout(() => finish(new ObservationExpired('HELPER_STARTUP_EXPIRED', 'startupObservationSeconds', settings.startupObservationSeconds, started, '自有网络组件ready首帧', '检查组件私有日志、完整性与启动状态;保留身份,不重交开通材料')), settings.startupObservationSeconds * 1000);
|
|
200
212
|
const aborted = () => finish(new Error('DISPOSED'));
|
|
201
213
|
const earlyExit = () => finish(new Error('HELPER_EXITED'));
|
|
202
214
|
const errored = () => finish(new Error('HELPER_START_FAILED'));
|
|
@@ -258,10 +270,12 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
258
270
|
if (disposed)
|
|
259
271
|
return;
|
|
260
272
|
const code = error instanceof Error && /^(LOCAL_PERMISSION_DENIED|LOCAL_PATH_INVALID|HELPER_[A-Z_]+|CONTROL_[A-Z_]+|CONFIG_INVALID)$/.test(error.message) ? error.message : 'CONFIG_INVALID';
|
|
261
|
-
terminal(startupNeed ?? needs(code, '网络组件未能启动,根因未查明', '管理员检查安装配置、helper完整性和私有日志'));
|
|
273
|
+
terminal(startupNeed ?? observationNeed(error) ?? needs(code, '网络组件未能启动,根因未查明', '管理员检查安装配置、helper完整性和私有日志'));
|
|
262
274
|
const forced = await stopOwned();
|
|
263
|
-
if (forced && terminalNeed)
|
|
264
|
-
terminalNeed.cause = '
|
|
275
|
+
if (forced && terminalNeed) {
|
|
276
|
+
terminalNeed.cause = [terminalNeed.cause, shutdownNeed?.message, shutdownNeed?.needs].filter(Boolean).join(';');
|
|
277
|
+
publish({ ...state, error: terminalNeed });
|
|
278
|
+
}
|
|
265
279
|
}
|
|
266
280
|
})();
|
|
267
281
|
const api = {
|
|
@@ -325,11 +339,12 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
325
339
|
return fail('DISPOSED', '登录代理已取消', '重新发起登录');
|
|
326
340
|
return ok(current);
|
|
327
341
|
}
|
|
328
|
-
catch {
|
|
342
|
+
catch (error) {
|
|
329
343
|
owner.abort();
|
|
330
344
|
signal.removeEventListener('abort', cancel);
|
|
331
345
|
lifetime.signal.removeEventListener('abort', cancel);
|
|
332
346
|
childGone.signal.removeEventListener('abort', cancel);
|
|
347
|
+
diagnostic ??= observationNeed(error);
|
|
333
348
|
return diagnostic ? { ok: false, error: diagnostic } : fail('PROXY_UNAVAILABLE', '登录代理未能建立,根因未查明', '查看主机网络与信任材料');
|
|
334
349
|
}
|
|
335
350
|
},
|
|
@@ -346,7 +361,7 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
346
361
|
await starting;
|
|
347
362
|
await Promise.all([...leases].map(x => x.release()));
|
|
348
363
|
const forced = await stopOwned();
|
|
349
|
-
publish(forced ? { phase: 'blocked', error:
|
|
364
|
+
publish(forced ? { phase: 'blocked', error: shutdownNeed } : { phase: 'stopped' });
|
|
350
365
|
listeners.clear();
|
|
351
366
|
updates.clear();
|
|
352
367
|
})();
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import type { Need } from './api.ts';
|
|
2
|
+
export declare class ObservationExpired extends Error {
|
|
3
|
+
readonly need: Need;
|
|
4
|
+
constructor(code: string, setting: string, seconds: number, started: number, waitingFor: string, nextStep: string);
|
|
5
|
+
}
|
|
6
|
+
export declare function observationNeed(error: unknown): Need | undefined;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Only fixed stage descriptions and numeric timings enter public diagnostics.
|
|
2
|
+
export class ObservationExpired extends Error {
|
|
3
|
+
need;
|
|
4
|
+
constructor(code, setting, seconds, started, waitingFor, nextStep) {
|
|
5
|
+
const elapsed = Math.max(0, (performance.now() - started) / 1000);
|
|
6
|
+
const message = `等待${waitingFor},已等${elapsed.toFixed(3)}秒(${setting}=${seconds}秒),观察期限已到,根因未查明。`;
|
|
7
|
+
super(message + ' 下一步:' + nextStep);
|
|
8
|
+
this.name = 'ObservationExpired';
|
|
9
|
+
this.need = { code, message, needs: nextStep, actor: 'network', cause: JSON.stringify({ setting, configuredSeconds: seconds, elapsedSeconds: elapsed, waitingFor }) };
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
export function observationNeed(error) {
|
|
13
|
+
return error instanceof ObservationExpired ? structuredClone(error.need) : undefined;
|
|
14
|
+
}
|
package/lib/packaged-helper.js
CHANGED
|
@@ -1,25 +1,29 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
1
|
+
import { readFileSync, lstatSync } from 'node:fs';
|
|
2
|
+
import { fileURLToPath } from 'node:url';
|
|
3
3
|
import { fail, ok } from "./api.js";
|
|
4
4
|
export function resolvePackagedHelper(moduleURL, platform = process.platform, arch = process.arch) {
|
|
5
|
+
if (platform !== 'darwin' || !['arm64', 'x64'].includes(arch))
|
|
6
|
+
return fail('HELPER_PLATFORM_UNSUPPORTED', `我需要支持 ${platform}-${arch} 的联网助手。`, '当前提供 macOS arm64 和 x64,请管理员核对设备平台');
|
|
5
7
|
const name = `@ai-workbench/net-helper-${platform}-${arch}`;
|
|
6
8
|
let expected;
|
|
7
9
|
try {
|
|
8
10
|
const root = JSON.parse(readFileSync(new URL('../package.json', moduleURL), 'utf8'));
|
|
9
|
-
expected = root.
|
|
10
|
-
if (expected
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
const manifest = JSON.parse(readFileSync(require.resolve(name + '/helper-manifest.json'), 'utf8'));
|
|
11
|
+
expected = root.version;
|
|
12
|
+
if (root.name !== '@ai-workbench/network' || typeof expected !== 'string' || !expected)
|
|
13
|
+
throw new Error('network主包的名称或版本无效');
|
|
14
|
+
const dir = new URL(`../helpers/${platform}-${arch}/`, moduleURL);
|
|
15
|
+
const metadata = JSON.parse(readFileSync(new URL('package.json', dir), 'utf8'));
|
|
16
|
+
if (metadata.name !== name || metadata.version !== expected || metadata.private !== true || JSON.stringify(metadata.os) !== JSON.stringify([platform]) || JSON.stringify(metadata.cpu) !== JSON.stringify([arch]))
|
|
17
|
+
throw new Error('内嵌助手的名称、版本或平台不匹配');
|
|
18
|
+
const manifest = JSON.parse(readFileSync(new URL('helper-manifest.json', dir), 'utf8'));
|
|
18
19
|
if (manifest.platform !== platform || manifest.arch !== arch || typeof manifest.sha256 !== 'string' || !/^[a-f0-9]{64}$/.test(manifest.sha256))
|
|
19
|
-
throw new Error('
|
|
20
|
-
|
|
20
|
+
throw new Error('内嵌助手 manifest 的架构或摘要无效');
|
|
21
|
+
const path = fileURLToPath(new URL('bin/aw-net', dir)), st = lstatSync(path);
|
|
22
|
+
if (!st.isFile() || st.isSymbolicLink())
|
|
23
|
+
throw new Error('内嵌助手不是普通二进制文件');
|
|
24
|
+
return ok({ path, sha256: manifest.sha256, platform, arch });
|
|
21
25
|
}
|
|
22
26
|
catch (cause) {
|
|
23
|
-
return { ok: false, error: { code: 'HELPER_NOT_PACKAGED', message: '
|
|
27
|
+
return { ok: false, error: { code: 'HELPER_NOT_PACKAGED', message: '我需要完整的当前版本远程主机网络插件。', needs: `请管理员重新安装 @ai-workbench/network${expected ? '@' + expected : ''} 的完整包;不要切换架构或改用旧助手`, actor: 'network', cause: cause instanceof Error ? cause.message : String(cause) } };
|
|
24
28
|
}
|
|
25
29
|
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { PluginSettings, HelperConfig } from './api.ts';
|
|
2
|
+
export declare const helperPolicyFields: readonly ["statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes", "httpMaxResponseHeaderBytes"];
|
|
3
|
+
export declare const pluginPolicyFields: readonly ["startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "downloadMaxRedirects"];
|
|
4
|
+
export declare const settingsFields: readonly ["configFile", "helperLogFile", "startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "downloadMaxRedirects", "statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes", "httpMaxResponseHeaderBytes"];
|
|
5
|
+
export declare const timerSeconds: (v: unknown) => v is number;
|
|
6
|
+
export declare const positiveInteger: (v: unknown) => v is number;
|
|
7
|
+
export declare function validHelperPolicy(settings: PluginSettings): boolean;
|
|
8
|
+
export declare function mergedSettings(initial: PluginSettings, provided: PluginSettings): PluginSettings;
|
|
9
|
+
export declare function effectiveConfig(config: HelperConfig, settings: PluginSettings): HelperConfig;
|
|
10
|
+
export declare function helperPolicyArgs(settings: PluginSettings): string[];
|
package/lib/settings.js
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export const helperPolicyFields = ['statusRefreshIntervalSeconds', 'dialTimeoutSeconds', 'headerTimeoutSeconds', 'enrollmentObservationSeconds', 'httpMaxHeaderBytes', 'httpMaxResponseHeaderBytes'];
|
|
2
|
+
export const pluginPolicyFields = ['startupObservationSeconds', 'shutdownObservationSeconds', 'controlResponseMaxBytes', 'downloadMaxRedirects'];
|
|
3
|
+
export const settingsFields = ['configFile', 'helperLogFile', ...pluginPolicyFields, ...helperPolicyFields];
|
|
4
|
+
export const timerSeconds = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0.001 && v * 1000 <= 2147483647;
|
|
5
|
+
export const positiveInteger = (v) => Number.isSafeInteger(v) && Number(v) > 0;
|
|
6
|
+
export function validHelperPolicy(settings) {
|
|
7
|
+
return helperPolicyFields.every(key => {
|
|
8
|
+
const v = settings[key];
|
|
9
|
+
if (v === undefined)
|
|
10
|
+
return true;
|
|
11
|
+
if (key === 'httpMaxHeaderBytes' || key === 'httpMaxResponseHeaderBytes')
|
|
12
|
+
return positiveInteger(v);
|
|
13
|
+
if (key === 'dialTimeoutSeconds' || key === 'statusRefreshIntervalSeconds')
|
|
14
|
+
return timerSeconds(v);
|
|
15
|
+
return typeof v === 'number' && Number.isFinite(v) && v >= 1e-9 && v < 9223372036.854775;
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
export function mergedSettings(initial, provided) {
|
|
19
|
+
const merged = { ...provided };
|
|
20
|
+
for (const key of [...pluginPolicyFields, ...helperPolicyFields])
|
|
21
|
+
if (initial[key] !== undefined)
|
|
22
|
+
merged[key] = initial[key];
|
|
23
|
+
return merged;
|
|
24
|
+
}
|
|
25
|
+
export function effectiveConfig(config, settings) {
|
|
26
|
+
const result = { ...config };
|
|
27
|
+
for (const key of helperPolicyFields)
|
|
28
|
+
if (settings[key] !== undefined)
|
|
29
|
+
result[key] = settings[key];
|
|
30
|
+
return result;
|
|
31
|
+
}
|
|
32
|
+
export function helperPolicyArgs(settings) {
|
|
33
|
+
return helperPolicyFields.flatMap(key => settings[key] === undefined ? [] : ['--' + key, String(settings[key])]);
|
|
34
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/network",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24.13.1"
|
|
@@ -25,7 +25,8 @@
|
|
|
25
25
|
"lib",
|
|
26
26
|
"NOTICE.md",
|
|
27
27
|
"cordis.patch.yml",
|
|
28
|
-
"locale"
|
|
28
|
+
"locale",
|
|
29
|
+
"helpers"
|
|
29
30
|
],
|
|
30
31
|
"dsh": {
|
|
31
32
|
"bundle": {
|
|
@@ -55,10 +56,6 @@
|
|
|
55
56
|
"dsh-plugin",
|
|
56
57
|
"deepseek-harness"
|
|
57
58
|
],
|
|
58
|
-
"optionalDependencies": {
|
|
59
|
-
"@ai-workbench/net-helper-darwin-arm64": "0.4.1",
|
|
60
|
-
"@ai-workbench/net-helper-darwin-x64": "0.4.1"
|
|
61
|
-
},
|
|
62
59
|
"repository": {
|
|
63
60
|
"type": "git",
|
|
64
61
|
"url": "git+https://github.com/yzsnstotz/ai-workbench-network.git"
|