@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.
@@ -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
- const fields = ['configFile', 'helperLogFile', 'startupObservationSeconds', 'shutdownObservationSeconds', 'controlResponseMaxBytes', 'downloadMaxRedirects'];
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
- return Object.fromEntries(fields.map(k => [k, s[k]]));
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 (Object.keys(initial).length)
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', '我需要有效的连接配置。', 'L提供六项具名网络设置');
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
- req.setTimeout(seconds * 1000, () => req.destroy(new Error('CONTROL_OBSERVATION_EXPIRED')));
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
- req.setTimeout(seconds * 1000, () => req.destroy(new Error('DOWNLOAD_CONNECT_EXPIRED')));
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
- req.setTimeout(seconds * 1000, () => req.destroy(new Error('DOWNLOAD_HEADERS_EXPIRED')));
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: z.number().step(1).min(1).description('本机连接控制响应的最大字节数(无默认值)'),
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
- block('CONTROL_UNAVAILABLE', '连接中断,正在等待网络组件恢复;根因未查明。', '等待恢复后自动重新连接;持续无响应时联系管理员');
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 timer = setTimeout(() => finish(new Error('HELPER_STARTUP_EXPIRED')), settings.startupObservationSeconds * 1000);
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 = '自有helper未在shutdownObservationSeconds内退出,已SIGKILL清理';
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: needs('HELPER_SHUTDOWN_FORCED', '网络组件未在配置期限内退出,已终止自有进程', '管理员检查组件退出日志') } : { phase: 'stopped' });
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
+ }
@@ -1,25 +1,29 @@
1
- import { createRequire } from 'node:module';
2
- import { readFileSync } from 'node:fs';
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.optionalDependencies?.[name];
10
- if (expected === undefined)
11
- return fail('HELPER_PLATFORM_UNSUPPORTED', `我需要支持 ${platform}-${arch} 的联网助手包。`, '当前提供 macOS arm64 和 x64 平台包,请管理员核对设备平台');
12
- if (expected !== root.version)
13
- throw new Error('平台助手依赖必须与主包版本精确一致');
14
- const require = createRequire(moduleURL), metadata = JSON.parse(readFileSync(require.resolve(name + '/package.json'), 'utf8'));
15
- if (metadata.name !== name || metadata.version !== expected || JSON.stringify(metadata.os) !== JSON.stringify([platform]) || JSON.stringify(metadata.cpu) !== JSON.stringify([arch]))
16
- throw new Error('平台助手包的名称、版本或平台不匹配');
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('平台助手 manifest 的架构或摘要无效');
20
- return ok({ path: require.resolve(name + '/bin/aw-net'), sha256: manifest.sha256, platform, arch });
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: '我需要完整的当前平台联网助手包。', needs: `请管理员安装 ${name}${expected ? '@' + expected : '(与主包同版本)'};不要切换架构或改用旧助手`, actor: 'network', cause: cause instanceof Error ? cause.message : String(cause) } };
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[];
@@ -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.1",
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"