@ai-workbench/network 0.4.4 → 0.4.6
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 +22 -6
- package/client/index.cjs +54 -0
- package/cordis.patch.yml +2 -0
- package/helpers/darwin-arm64/NOTICE.md +2 -0
- package/helpers/darwin-arm64/bin/aw-net +0 -0
- package/helpers/darwin-arm64/helper-manifest.json +1 -1
- package/helpers/darwin-arm64/package.json +1 -1
- package/helpers/darwin-x64/NOTICE.md +2 -0
- package/helpers/darwin-x64/bin/aw-net +0 -0
- package/helpers/darwin-x64/helper-manifest.json +1 -1
- package/helpers/darwin-x64/package.json +1 -1
- package/lib/activation.js +76 -7
- package/lib/api.d.ts +3 -0
- package/lib/index.js +4 -1
- package/lib/network.d.ts +2 -1
- package/lib/network.js +59 -18
- package/lib/policy-management.d.ts +113 -0
- package/lib/policy-management.js +123 -0
- package/lib/settings-host.d.ts +3 -0
- package/lib/settings-host.js +12 -0
- package/lib/settings.d.ts +14 -2
- package/lib/settings.js +10 -1
- package/package.json +17 -2
package/NOTICE.md
CHANGED
|
@@ -21,3 +21,5 @@ N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MI
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
D20(0.4.4)实际端点复用:x/crypto/ssh v0.57.0(BSD-3-Clause),creack/pty v1.1.24(MIT,revision edfbf75025b0ba4ee17c19f52d9b600fad80a787),kr/fs v0.1.0(BSD-3-Clause),x/sys v0.48.0(BSD-3-Clause)。新增SFTP本地fork源自github.com/pkg/sftp v1.13.11(BSD-2-Clause,revision fc82c354c0d87349411e30a08bef297c9f132105),只在本仓third_party/sftp维护有序操作及原生APPEND补丁;UPSTREAM.json逐文件来源SHA、D20.patch和原LICENSE均保留。不使用gliderlabs/ssh或go-shlex,不要求上游修改。以上固定模块的archive SHA、sum与许可摘要及最终两目标实际编译闭包见本轮依赖证据/各helpers许可证清单;不将未链接研究依赖冒充产品闭包。系统账户shell来自OS,不捆绑系统shell;N内建SFTP子模式随同一Go二进制交付,不依赖员工额外安装sshd、Node或SFTP程序。S主机桥工件独立由S提供、编排安装,N不修改或分发S源码。
|
|
24
|
+
|
|
25
|
+
0.4.6 管理界面:沿官方公开 plugins.row.config、configEditor.edit、webServer.register 与 connection.requestRejection 接口,不复制或修改 DSH 核心。浏览器 React 由官方冻结平台模块提供,没有将 React 分发进本包;开发/表单测试固定 react18.3.1、react-test-renderer18.3.1(MIT),其实际锁定测试闭包 react-is18.3.1、react-shallow-renderer16.15.0、scheduler0.23.2、loose-envify1.4.0、js-tokens4.0.0、object-assign4.1.1(均MIT)只用于开发,来源 npm 固定包与 package-lock.json integrity,不加入运行时依赖。设置伴生入口使用 Node 内置 crypto/http 类型,无新运行时外部依赖。
|
package/README.md
CHANGED
|
@@ -1,8 +1,20 @@
|
|
|
1
|
-
安装:在 DSH「插件 → 添加插件」填写 `@ai-workbench/network@0.4.
|
|
1
|
+
安装:在 DSH「插件 → 添加插件」填写 `@ai-workbench/network@0.4.6`,或运行 `dsh plugin --profile desktop add @ai-workbench/network@0.4.6`。
|
|
2
2
|
|
|
3
3
|
# 远程主机网络
|
|
4
4
|
|
|
5
|
-
0.4.
|
|
5
|
+
0.4.6 当前为待发布版本,由编排统一发布公开 npm;工人不执行 publish。发布前可由官方添加插件入口安装本轮固定主包 tgz 的绝对路径,无需再解析平台依赖。
|
|
6
|
+
|
|
7
|
+
0.4.6 由稳定插件服务监督自有 helper。已确认可复用身份的 helper 意外退出后,先撤回旧通道和员工执行器、发布 lost,等待旧实例清理完成,再走原私有路径、SHA 校验、ready 和身份确认路径启动新实例。恢复成功发布 restored,由 L 更新连接 epoch;不重放 SSH、模型输入、命令或写文件请求。正常卸载不重启;未知开通结果、身份/配置/权限/日志异常明确停止,不重新消费开通密钥。恢复内部启用 resumeIdentityOnly,在插件与 helper 两次配置读取中拒绝首次开通材料;这不是员工参数。正常identity实例保存实际读取配置的SHA256,恢复在插件/helper两端对同一读取快照核比;配置变化明确HELPER_RECOVERY_CONFIG_CHANGED,不改指另一身份目录。首次key清理交接后以新identity配置建立摘要。私有stateDir仍由N维护,本轮不新增跨实例NodeID协议,不声称防住整套受保护身份文件被外部替换。
|
|
8
|
+
|
|
9
|
+
新增三个官方插件管理设置(此前十项策略不变):
|
|
10
|
+
|
|
11
|
+
| 名称 | 未设置时 | 效果 |
|
|
12
|
+
|---|---:|---|
|
|
13
|
+
| helperRecoveryMaxAttempts | 3 次 | 0 关闭;连续恢复耗尽后明确停止,显示退出原因及管理员排查步骤。增大允许更多连续恢复,减小更早停止。 |
|
|
14
|
+
| helperRecoveryDelaySeconds | 1 秒 | 每次恢复前等待;增大会延后恢复并减少频繁拉起,减小增加启动开销。 |
|
|
15
|
+
| helperRecoveryStableSeconds | 60 秒 | 连续 online 达到此时长才清零失败计数;增大更严格,减小可能把短暂在线当成稳定。 |
|
|
16
|
+
|
|
17
|
+
本插件显式设置优先于开通方设置;只在缺省时采用上述三项默认,不写回原配置。改活动连接策略须正常重载插件;恢复耗尽后也须先排查再重载,不要求员工重新开通。恢复间隔与稳定时长范围 0.001..2147483.647 秒,次数为 0..Number.MAX_SAFE_INTEGER 的整数;这些是计时器/整数的可表示范围。每次启动、退出和启动进程错误以 HELPER_PROCESS 固定 JSON 追加到原私有 helperLogFile,含时间、PID、实例 ID、退出码、实际信号、预期标记和耗时,不记录命令行、环境、密钥或请求正文。捕获 TERM 后自行 exit 0 的实际信号为 null,不猜作 SIGTERM。日志写入/旧实例清理失败停止恢复并给出具体 Need。
|
|
6
18
|
|
|
7
19
|
插件通过官方 bundle/Cordis 接口提供远程主机网络、设备身份、SSH 通道、认证登录代理及主机下载能力。不替换 DSH 服务、布局或默认设置,不增加工作区/登录界面。L 负责可信连接资料导入和登录入口,员工不手填主机、用户名、路径或密钥。没有连接资料时明确说明需要导入,不把插件 Running 当作网络已连接。
|
|
8
20
|
|
|
@@ -10,7 +22,7 @@
|
|
|
10
22
|
|
|
11
23
|
旧0.4.1/0.4.2安装保留的锁/依赖树仍可能触发D16,直接add新主包不是迁移方案;owner已取消产品迁移;测试机的一次remove旧N→add新N整理由编排处理,保留原身份/配置,不把add当清理旧图的替代。本仓不移动员工profile/状态、不要求员工手删锁;公开`/api`、`/cordis`及wire apiVersion=1不变。
|
|
12
24
|
|
|
13
|
-
一个默认 profile
|
|
25
|
+
一个默认 profile 只启用一组 N bundle(网络 runtime 与同包设置伴生行)。独立安装与 L 聚合安装不要重复挂载 N。N 的网络身份按系统用户隔离、持久保存;不重交未知结果的一次性 key。公开 `aiWorkbenchNetwork.activate(settings, signal)`、`getActivationState/onActivationState` 和通道 API;公开类型入口 `/api`,Cordis 类型入口 `/cordis`。`active` 表示身份恢复与消费交接完成,不等于主机 SSH 或 AI 登录已通过。原六项安装设置仍由可信开通提供;下述策略可在N自己的官方Config独立设置,不要求员工手填路径/身份,不新增数值默认。
|
|
14
26
|
|
|
15
27
|
## 本插件设置
|
|
16
28
|
|
|
@@ -35,13 +47,13 @@
|
|
|
35
47
|
|
|
36
48
|
所有超时只为可观测性,触发后提供等待事项、实际已等秒数、具名期限与下一步检查,根因仍未知,不重试/换源/换出口。Node控制/下载的req.setTimeout沿用原**阶段空闲**计时;远端拨号/SSH和请求头沿用原阶段deadline。到响应头/握手结束即移除该阶段期限,正文、NDJSON租约、SSH流仍由显式取消/释放/进程退出管理,不增加流总时限。headerTimeout在HTTP handler之前由实际Read超时处记入脱敏私有日志,不记录请求内容/授权参数;主动取消后台读不误报。首次开通到期保留未知结果及身份,不自动重新交key;已有身份不套首次窗口。
|
|
37
49
|
|
|
38
|
-
固定正确性约束不是可任意调大的运行策略:Unix socket≤103字节、端口1..65535、计时器/整数可表示范围、HTTP实现溢出保护、700/600权限、当前UID/单身份、SHA256和凭据熵均不可关闭,官方Config
|
|
50
|
+
固定正确性约束不是可任意调大的运行策略:Unix socket≤103字节、端口1..65535、计时器/整数可表示范围、HTTP实现溢出保护、700/600权限、当前UID/单身份、SHA256和凭据熵均不可关闭,官方Config总说明及盘点表明确列出。本节旧0.4.2未新增重试;0.4.6新增的三项helper恢复策略见上,不是SSH或模型业务重试。下载正文与会话寿命未新增上限。D14旧manifest空catch已在0.4.0修复,当前packaged-helper继续返回原始cause,本轮加坏主manifest回归。
|
|
39
51
|
|
|
40
52
|
开发:Node >=24.13.1、Go 1.27.3、固定 tsnet v1.104.1。运行 `npm ci --ignore-scripts --omit=optional`、`npm test`、`npm run check`、`make test`、`make test-race`、`make vet`,再执行 `npm run build`。build 显式交叉构建两个 macOS 平台包并编译 TypeScript;Go 参数固定 ts_omit_logtail、禁 VCS 元数据、trimpath、CGO_ENABLED=0。这些是开发命令,员工安装不执行。二进制、manifest 和 NOTICE 副本由构建生成,不提交。
|
|
41
53
|
|
|
42
|
-
|
|
54
|
+
发布准备构建 arm64/x64 内嵌助手,再对唯一主包 pack/dry-run,版本必须一致;固定 tgz、SHA256、文件清单及证据见仓库 `docs/evidence/2026-10-21-N-npm/` 和 N 回执。不要把先前单平台 0.4.0 的旧 lib/bin 混入本轮产物。根盘按当前编排规则低于8GiB停;临时 HOME、依赖、日志和构建缓存放自有 Elements APFS,用后精确清理,不删除公共缓存。
|
|
43
55
|
|
|
44
|
-
|
|
56
|
+
完整提供方合同见 `Docs/Projects/ai-workbench/contracts/net-helper.md`,第三方来源见包内 NOTICE.md。只做中文,既有英文名称保留。官方 DSH 与 HanaMesh 分别验证;不使用 owner 当前实例,GUI 加载门不替代真实入网、登录、对话、物理断网或 leo E 验收。
|
|
45
57
|
|
|
46
58
|
|
|
47
59
|
D20员工端点:0.4.4新增后端 `prepareEmployeeEndpoint(request, signal)`,按当前L本地工作区CAS与真实目录身份取目标;普通N先提供服务,运行时才消费L可选getter。设备SSH从真实OS账户home启动,每工作区的cwd由X逐请求显式指定,不成为路径权限根。L撤回/所有local消失只关闭D20,普通网络不变;一个工作区切换不重建设备。主机经S固定桥回接同一helper内嵌的完整exec/shell/PTY/SFTP端点,员工不开系统Remote Login、不填账户/密码/key。私有持久普通SSH身份在原N stateDir/employee-ssh下,坏身份明确失败、不自动换key,与tailnet入网身份分开。
|
|
@@ -49,3 +61,7 @@ D20员工端点:0.4.4新增后端 `prepareEmployeeEndpoint(request, signal)`
|
|
|
49
61
|
N从官方后端自行取得OS shell和原umask,经同UID子进程继承交付;L进程/环境对象不进HTTP或网络。exec为账户shell -c,无PTY shell普通启动、PTY为-i,不额外强制login;SSH env仅覆盖该channel,正常初始化由shell处理。会话子执行恢复DSH原umask,私有helper维持077。完整环境只留本机,不复制到主机/renderer/日志。固定S policy逐项消费,无新本地数值默认;N原controlResponseMaxBytes亦约束D20私有控制请求/响应和桥首帧,观察沿原dialTimeoutSeconds,不作为工具运行限时。生产SSH MaxAuthTries显式-1关闭认证次数限制,普通唯一client公钥认证仍必需;加密算法/重协商采用固定x/crypto公开默认。
|
|
50
62
|
|
|
51
63
|
本包提供N端点与管理能力;两品牌原生工具改道、每RPC撤回、自动工具闭包、真实GUI/TCC/首次员工/leo及Intel实机须各自验收,不能以组件/开发桥验证代替。断线或缺命令完成确认保留UNKNOWN,不自动重连或重放文件/命令。mini原支持节点仍保留;禁止另起同身份N或把它用作D20实验。
|
|
64
|
+
|
|
65
|
+
管理入口:官方「插件 → 远程主机网络 → ai-workbench-network」组件详情页,三项中文恢复设置带默认值、范围、关闭及耗尽后果。保存由官方 configEditor 持久化并正常重载 runtime;同包 settings 伴生行仅保持设置应答,不持有入网材料。撤销N指定值直接删除该字段,转由开通方或新默认提供;不把默认值写成显式覆盖;保存冲突或失败明确显示,旧操作不重放。页面需要客户端公开 slots、webServer、connection 与 configEditor;缺失时明确说明所需服务,核心联网能力不因此强依赖客户端设置。客户端仍归 runtime 包根,重载后可重新挂载并重新读取。HanaMesh 管理入口须另行验证。
|
|
66
|
+
|
|
67
|
+
员工具体原因投影:L既有断线提示会泛化Need,N自己的同一官方设置页增加“读取时状态”,显示恢复/耗尽的固定Need code/message/needs;不投原始cause/Error,只从可信退出记录格式提取实际exitCode/signal,缺证据明确未知。页面标采样UTC、重新打开可取最新,不冒充实时监控。折叠技术详情只含L公开instanceId/epoch/phase,供只读复核,无控制或重放操作。
|
package/client/index.cjs
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
window.__ModuleLoader__.load({id:'@ai-workbench/network',factory:(require)=>{
|
|
2
|
+
const React=require('react'),h=React.createElement;
|
|
3
|
+
const definitions=[
|
|
4
|
+
['helperRecoveryMaxAttempts','自动恢复次数','默认3次,0关闭自动恢复。次数为非负安全整数。增大允许更多连续恢复;减小更早停止。耗尽后停止并显示原因,需要管理员排查后正常重载插件。'],
|
|
5
|
+
['helperRecoveryDelaySeconds','恢复间隔(秒)','默认1秒,范围0.001至2147483.647。增大减少频繁启动但恢复更慢;减小增加连续启动开销。无关闭值,关闭恢复请将次数设为0。'],
|
|
6
|
+
['helperRecoveryStableSeconds','计数清零所需连续在线时间(秒)','默认60秒,范围0.001至2147483.647。增大更严格识别反复崩溃;减小可能把短暂在线当成稳定。无关闭值,刚收到就绪不会清零。'],
|
|
7
|
+
];
|
|
8
|
+
const controlStyle={font:'inherit',padding:'6px',background:'transparent',color:'inherit',border:'1px solid #8886',borderRadius:'5px'};
|
|
9
|
+
async function request(params,signal){
|
|
10
|
+
const response=await fetch('/ai-workbench-network/policy'+(params?'?'+params:''),{method:params?'POST':'GET',credentials:'same-origin',signal});
|
|
11
|
+
const result=await response.json();
|
|
12
|
+
if(!response.ok||!result.ok)throw new Error(result.error?.message??'官方恢复设置服务未确认操作成功。');
|
|
13
|
+
return result.value;
|
|
14
|
+
}
|
|
15
|
+
function RecoveryPolicyForm(){
|
|
16
|
+
const [snapshot,setSnapshot]=React.useState(null),[draft,setDraft]=React.useState(null),[error,setError]=React.useState(''),[busy,setBusy]=React.useState(false),[message,setMessage]=React.useState('');
|
|
17
|
+
const life=React.useRef(null);
|
|
18
|
+
const adopt=value=>{setSnapshot(value);setDraft(Object.fromEntries(definitions.map(([key])=>[key,{inherit:!value.fields[key].explicit,value:String(value.fields[key].value??value.fields[key].defaultValue)}])));};
|
|
19
|
+
React.useEffect(()=>{const controller=new AbortController();life.current=controller;request(null,controller.signal).then(value=>{if(!controller.signal.aborted)adopt(value);},()=>{if(!controller.signal.aborted)setError('我需要官方客户端的网络设置服务(配置编辑、浏览器连接和网页服务)才能显示本页。请在插件管理查看组件错误。');});return ()=>controller.abort();},[]);
|
|
20
|
+
async function save(){
|
|
21
|
+
if(busy||!snapshot||!draft)return;setError('');setMessage('');
|
|
22
|
+
const params=new URLSearchParams({revision:snapshot.revision});
|
|
23
|
+
for(const [key] of definitions){
|
|
24
|
+
const item=draft[key],value=Number(item.value);
|
|
25
|
+
if(!item.inherit&&(!/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(item.value)||!Number.isFinite(value)||(key==='helperRecoveryMaxAttempts'?(!Number.isSafeInteger(value)||value<0):(value<0.001||value>2147483.647)))){setError('设置未保存:次数须为非负安全整数;秒数须在0.001到2147483.647之间。');return;}
|
|
26
|
+
params.set(key,item.inherit?'unset':item.value);
|
|
27
|
+
}
|
|
28
|
+
const controller=life.current;setBusy(true);
|
|
29
|
+
try{const value=await request(params,controller.signal);if(!controller.signal.aborted){adopt(value);setMessage('官方配置保存与正常重载已完成。恢复连接后请发起新操作。');}}
|
|
30
|
+
catch(e){if(!controller.signal.aborted)setError(e instanceof Error?e.message:'保存未确认,请重新打开本页核对。');}
|
|
31
|
+
finally{if(!controller.signal.aborted)setBusy(false);}
|
|
32
|
+
}
|
|
33
|
+
return h('section',{'aria-label':'网络组件自动恢复设置',style:{maxWidth:'780px',padding:'16px'}},
|
|
34
|
+
h('h3',null,'网络组件自动恢复'),
|
|
35
|
+
snapshot?.runtime?h('div',{'aria-label':'读取时组件状态'},
|
|
36
|
+
h('p',null,'读取时状态:'+({online:'在线',starting:'正在启动',blocked:'不可用',stopped:'已停止',idle:'未启动',stopping:'正在停止',missing:'组件未提供',unknown:'未知'}[snapshot.runtime.phase]??'未知')+'('+snapshot.runtime.observedAt+';重新打开本页可读取最新状态)'),
|
|
37
|
+
snapshot.runtime.need?h('div',{role:'alert'},h('p',null,snapshot.runtime.need.code+':'+snapshot.runtime.need.message),h('p',null,snapshot.runtime.need.needs),h('p',null,snapshot.runtime.need.exit?'退出码:'+(snapshot.runtime.need.exit.exitCode??'无(信号退出)')+';退出信号:'+(snapshot.runtime.need.exit.signal??'无(组件自行退出)'):'退出码及信号:未知')):null,
|
|
38
|
+
snapshot.runtime.connection?h('details',null,h('summary',null,'连接技术详情'),h('pre',null,JSON.stringify(snapshot.runtime.connection,null,2))):null
|
|
39
|
+
):null,
|
|
40
|
+
h('p',null,'仅恢复意外退出的自有联网组件,复用原入网身份,不重交密钥。不会重试 SSH 或模型请求。'),
|
|
41
|
+
h('p',null,'保存修改将通过官方流程正常重载网络插件,活动连接会断开。恢复后请发起新操作,旧请求不会重放。'),
|
|
42
|
+
snapshot&&draft?definitions.map(([key,title,description])=>h('fieldset',{key,disabled:busy,style:{margin:'12px 0',padding:'10px',border:'1px solid #8886',borderRadius:'6px'}},
|
|
43
|
+
h('legend',null,title),h('code',null,key),h('p',{style:{margin:'8px 0',lineHeight:1.5}},description),
|
|
44
|
+
h('label',{style:{display:'block',marginBottom:'8px'}},h('input',{type:'checkbox',checked:draft[key].inherit,onChange:e=>setDraft({...draft,[key]:{...draft[key],inherit:e.target.checked}})}),' 由开通方提供(N不指定;开通方未设时使用上述默认值)'),
|
|
45
|
+
h('input',{'aria-label':key,type:'number',min:key==='helperRecoveryMaxAttempts'?0:0.001,max:key==='helperRecoveryMaxAttempts'?Number.MAX_SAFE_INTEGER:2147483.647,step:key==='helperRecoveryMaxAttempts'?1:'any',disabled:draft[key].inherit,value:draft[key].value,style:controlStyle,onChange:e=>setDraft({...draft,[key]:{...draft[key],value:e.target.value}})})
|
|
46
|
+
)):h('p',null,error?'设置不可用':'正在读取恢复设置…'),
|
|
47
|
+
error?h('p',{role:'alert',style:{color:'#df6659'}},error):null,
|
|
48
|
+
message?h('p',{role:'status'},message):null,
|
|
49
|
+
snapshot?h('button',{type:'button',disabled:busy,onClick:save,style:controlStyle},busy?'正在保存并正常重载…':'保存并正常重载网络组件'):null);
|
|
50
|
+
}
|
|
51
|
+
return {name:'ai-workbench-network-client',inject:['slots'],RecoveryPolicyForm,apply(ctx){
|
|
52
|
+
ctx.effect(()=>ctx.slots.inject('plugins.row.config',()=>ctx.slots.register({name:'plugins.row.config',key:'@ai-workbench/network#ai-workbench-network'},({view})=>view==='summary'?'组件意外退出后的自动恢复设置':h(RecoveryPolicyForm))));
|
|
53
|
+
}};
|
|
54
|
+
}});
|
package/cordis.patch.yml
CHANGED
|
@@ -21,3 +21,5 @@ N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MI
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
D20(0.4.4)实际端点复用:x/crypto/ssh v0.57.0(BSD-3-Clause),creack/pty v1.1.24(MIT,revision edfbf75025b0ba4ee17c19f52d9b600fad80a787),kr/fs v0.1.0(BSD-3-Clause),x/sys v0.48.0(BSD-3-Clause)。新增SFTP本地fork源自github.com/pkg/sftp v1.13.11(BSD-2-Clause,revision fc82c354c0d87349411e30a08bef297c9f132105),只在本仓third_party/sftp维护有序操作及原生APPEND补丁;UPSTREAM.json逐文件来源SHA、D20.patch和原LICENSE均保留。不使用gliderlabs/ssh或go-shlex,不要求上游修改。以上固定模块的archive SHA、sum与许可摘要及最终两目标实际编译闭包见本轮依赖证据/各helpers许可证清单;不将未链接研究依赖冒充产品闭包。系统账户shell来自OS,不捆绑系统shell;N内建SFTP子模式随同一Go二进制交付,不依赖员工额外安装sshd、Node或SFTP程序。S主机桥工件独立由S提供、编排安装,N不修改或分发S源码。
|
|
24
|
+
|
|
25
|
+
0.4.6 管理界面:沿官方公开 plugins.row.config、configEditor.edit、webServer.register 与 connection.requestRejection 接口,不复制或修改 DSH 核心。浏览器 React 由官方冻结平台模块提供,没有将 React 分发进本包;开发/表单测试固定 react18.3.1、react-test-renderer18.3.1(MIT),其实际锁定测试闭包 react-is18.3.1、react-shallow-renderer16.15.0、scheduler0.23.2、loose-envify1.4.0、js-tokens4.0.0、object-assign4.1.1(均MIT)只用于开发,来源 npm 固定包与 package-lock.json integrity,不加入运行时依赖。设置伴生入口使用 Node 内置 crypto/http 类型,无新运行时外部依赖。
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"platform":"darwin","arch":"arm64","sha256":"
|
|
1
|
+
{"platform":"darwin","arch":"arm64","sha256":"d27016f9242ea6f96e44c4f55e24794e7092dee5f1ef2cad2e9e4198809fc017"}
|
|
@@ -21,3 +21,5 @@ N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MI
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
D20(0.4.4)实际端点复用:x/crypto/ssh v0.57.0(BSD-3-Clause),creack/pty v1.1.24(MIT,revision edfbf75025b0ba4ee17c19f52d9b600fad80a787),kr/fs v0.1.0(BSD-3-Clause),x/sys v0.48.0(BSD-3-Clause)。新增SFTP本地fork源自github.com/pkg/sftp v1.13.11(BSD-2-Clause,revision fc82c354c0d87349411e30a08bef297c9f132105),只在本仓third_party/sftp维护有序操作及原生APPEND补丁;UPSTREAM.json逐文件来源SHA、D20.patch和原LICENSE均保留。不使用gliderlabs/ssh或go-shlex,不要求上游修改。以上固定模块的archive SHA、sum与许可摘要及最终两目标实际编译闭包见本轮依赖证据/各helpers许可证清单;不将未链接研究依赖冒充产品闭包。系统账户shell来自OS,不捆绑系统shell;N内建SFTP子模式随同一Go二进制交付,不依赖员工额外安装sshd、Node或SFTP程序。S主机桥工件独立由S提供、编排安装,N不修改或分发S源码。
|
|
24
|
+
|
|
25
|
+
0.4.6 管理界面:沿官方公开 plugins.row.config、configEditor.edit、webServer.register 与 connection.requestRejection 接口,不复制或修改 DSH 核心。浏览器 React 由官方冻结平台模块提供,没有将 React 分发进本包;开发/表单测试固定 react18.3.1、react-test-renderer18.3.1(MIT),其实际锁定测试闭包 react-is18.3.1、react-shallow-renderer16.15.0、scheduler0.23.2、loose-envify1.4.0、js-tokens4.0.0、object-assign4.1.1(均MIT)只用于开发,来源 npm 固定包与 package-lock.json integrity,不加入运行时依赖。设置伴生入口使用 Node 内置 crypto/http 类型,无新运行时外部依赖。
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"platform":"darwin","arch":"x64","sha256":"
|
|
1
|
+
{"platform":"darwin","arch":"x64","sha256":"5d8e2284bc3bebced282131225cab583abcb66dc83a338ce7d6de9edb67844f1"}
|
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
|
-
import { settingsFields as fields, validHelperPolicy, mergedSettings } from "./settings.js";
|
|
5
|
+
import { settingsFields as fields, validHelperPolicy, validRecoveryPolicy, recoveryPolicy, mergedSettings } from "./settings.js";
|
|
6
6
|
import { createEmployeeEndpointController } from "./employee-endpoint.js";
|
|
7
7
|
function normalized(s) {
|
|
8
8
|
if (!s || typeof s !== 'object' || Object.keys(s).some(k => !fields.includes(k)))
|
|
@@ -14,7 +14,7 @@ function normalized(s) {
|
|
|
14
14
|
return;
|
|
15
15
|
if (!Number.isSafeInteger(s.controlResponseMaxBytes) || s.controlResponseMaxBytes <= 0 || !Number.isSafeInteger(s.downloadMaxRedirects) || s.downloadMaxRedirects < 0)
|
|
16
16
|
return;
|
|
17
|
-
if (!validHelperPolicy(s))
|
|
17
|
+
if (!validHelperPolicy(s) || !validRecoveryPolicy(s))
|
|
18
18
|
return;
|
|
19
19
|
return Object.fromEntries(fields.filter(k => s[k] !== undefined).map(k => [k, s[k]]));
|
|
20
20
|
}
|
|
@@ -23,6 +23,9 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
23
23
|
let network = { phase: 'blocked', error: { code: 'SETTINGS_REQUIRED', message: '我需要导入连接资料。', needs: '在设置 → 远程主机连接 导入连接资料', actor: 'network' } };
|
|
24
24
|
let activation = { instanceId: randomUUID(), revision: 0, phase: 'unconfigured', receipt: null, error: network.error };
|
|
25
25
|
let runtime, selected, disposed = false, disposing, handoff, generation = 0, handedOff = false, consumerOnline = false;
|
|
26
|
+
let recoveryTimer, stableTimer, recovering;
|
|
27
|
+
let recoveryAttempts = 0, recoverAgain = false, handledFailure, lastFailure, policy = recoveryPolicy(initial);
|
|
28
|
+
const processFailures = new Set(['HELPER_EXITED', 'HELPER_PROCESS_FAILED', 'HELPER_START_FAILED', 'HELPER_STARTUP_EXPIRED']);
|
|
26
29
|
const listeners = new Set(), edges = new Set(), updates = new Set();
|
|
27
30
|
const snapshot = () => structuredClone(activation);
|
|
28
31
|
const employee = createEmployeeEndpointController({ getConnect, getRuntime: () => activation.phase === 'active' ? runtime : undefined, getState: () => network, connect: (target, signal) => api.connect(target, signal) });
|
|
@@ -72,7 +75,7 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
72
75
|
return;
|
|
73
76
|
}
|
|
74
77
|
handedOff = true;
|
|
75
|
-
install(JSON.parse(selected));
|
|
78
|
+
install(JSON.parse(selected), true);
|
|
76
79
|
launch();
|
|
77
80
|
}).catch(() => observed({ phase: 'blocked', error: { code: 'HELPER_HANDOFF_FAILED', message: '网络身份交接失败,根因未查明。', needs: '保留身份并检查私有日志;不要重交密钥', actor: 'network' } }));
|
|
78
81
|
}
|
|
@@ -94,15 +97,77 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
94
97
|
catch {
|
|
95
98
|
console.error('NETWORK_OBSERVER_FAILED');
|
|
96
99
|
}
|
|
100
|
+
if (next.phase === 'online') {
|
|
101
|
+
if (recoveryAttempts > 0 && !stableTimer) {
|
|
102
|
+
const ownGeneration = generation;
|
|
103
|
+
stableTimer = setTimeout(() => { stableTimer = undefined; if (!disposed && generation === ownGeneration && network.phase === 'online')
|
|
104
|
+
recoveryAttempts = 0; }, policy.helperRecoveryStableSeconds * 1000);
|
|
105
|
+
stableTimer.unref?.();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
else if (stableTimer) {
|
|
109
|
+
clearTimeout(stableTimer);
|
|
110
|
+
stableTimer = undefined;
|
|
111
|
+
}
|
|
112
|
+
const resumable = current?.identity === 'resumable' && (current.cleanup === 'complete' || current.cleanup === 'not-required') && (current.session === 'identity' || handedOff);
|
|
113
|
+
if (next.error && processFailures.has(next.error.code) && resumable && runtime && handledFailure !== runtime) {
|
|
114
|
+
handledFailure = runtime;
|
|
115
|
+
lastFailure = next.error;
|
|
116
|
+
if (recovering || recoveryTimer)
|
|
117
|
+
recoverAgain = true;
|
|
118
|
+
else
|
|
119
|
+
scheduleRecovery(next.error);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
function recoveryState(error) {
|
|
123
|
+
const status = network.status ? { ...network.status, enrollment: 'error', host: { ...network.status.host, online: null, observedAt: null }, error } : undefined;
|
|
124
|
+
observed({ phase: error.code === 'HELPER_RECOVERING' ? 'starting' : 'blocked', ...status ? { status } : {}, error });
|
|
125
|
+
}
|
|
126
|
+
function scheduleRecovery(cause) {
|
|
127
|
+
if (disposed)
|
|
128
|
+
return;
|
|
129
|
+
const detail = [cause.code, cause.message, cause.cause].filter(Boolean).join(';');
|
|
130
|
+
if (recoveryAttempts >= policy.helperRecoveryMaxAttempts) {
|
|
131
|
+
recoveryState({ code: policy.helperRecoveryMaxAttempts === 0 ? 'HELPER_RECOVERY_DISABLED' : 'HELPER_RECOVERY_EXHAUSTED', message: policy.helperRecoveryMaxAttempts === 0 ? '网络组件意外退出;自动恢复已关闭。' : `网络组件连续恢复${recoveryAttempts}次仍未稳定,已停止自动恢复。`, needs: `请管理员检查网络组件退出日志与原身份;helperRecoveryMaxAttempts=${policy.helperRecoveryMaxAttempts},连续在线helperRecoveryStableSeconds=${policy.helperRecoveryStableSeconds}秒后才清零;排查后正常重载插件,不重交密钥。`, actor: 'network', cause: detail });
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
recoveryAttempts++;
|
|
135
|
+
recoveryState({ code: 'HELPER_RECOVERING', message: `网络组件意外退出,正在第${recoveryAttempts}/${policy.helperRecoveryMaxAttempts}次恢复。`, needs: `等待helperRecoveryDelaySeconds=${policy.helperRecoveryDelaySeconds}秒后按原配置恢复;原连接已撤回,请在恢复后发起新操作,旧请求不会重放。`, actor: 'network', cause: detail });
|
|
136
|
+
if (disposed)
|
|
137
|
+
return;
|
|
138
|
+
recoveryTimer = setTimeout(() => {
|
|
139
|
+
recoveryTimer = undefined;
|
|
140
|
+
const owned = runtime;
|
|
141
|
+
generation++;
|
|
142
|
+
recovering = Promise.resolve().then(async () => {
|
|
143
|
+
await owned.dispose();
|
|
144
|
+
if (disposed)
|
|
145
|
+
return;
|
|
146
|
+
const error = owned.getState().error;
|
|
147
|
+
if (error) {
|
|
148
|
+
recoveryState({ ...error, cause: [error.cause, detail].filter(Boolean).join(';') });
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
install(JSON.parse(selected), true, owned.getRecoveryConfigSHA256?.());
|
|
152
|
+
await launch();
|
|
153
|
+
}).catch(() => recoveryState({ code: 'HELPER_RECOVERY_FAILED', message: '网络组件恢复未能完成,已停止自动恢复;根因未查明。', needs: '管理员检查私有日志、旧实例清理及安装状态;不启动第二个同身份实例,不重交密钥。', actor: 'network', cause: detail })).finally(() => {
|
|
154
|
+
recovering = undefined;
|
|
155
|
+
if (!disposed && recoverAgain) {
|
|
156
|
+
recoverAgain = false;
|
|
157
|
+
scheduleRecovery(lastFailure);
|
|
158
|
+
}
|
|
159
|
+
});
|
|
160
|
+
}, policy.helperRecoveryDelaySeconds * 1000);
|
|
161
|
+
recoveryTimer.unref?.();
|
|
97
162
|
}
|
|
98
|
-
function install(config) {
|
|
163
|
+
function install(config, resumeIdentityOnly = false, expectedConfigSHA256) {
|
|
99
164
|
const ownGeneration = ++generation;
|
|
100
165
|
runtime = factory(config, executable, next => { if (ownGeneration === generation)
|
|
101
|
-
observed(next); });
|
|
166
|
+
observed(next); }, undefined, resumeIdentityOnly, expectedConfigSHA256);
|
|
102
167
|
}
|
|
103
168
|
function launch() {
|
|
104
169
|
const owned = runtime, ownGeneration = generation;
|
|
105
|
-
|
|
170
|
+
return owned.start().then(() => {
|
|
106
171
|
if (!disposed && ownGeneration === generation && !network.error && !network.status?.receipt)
|
|
107
172
|
observed({ phase: 'blocked', error: { code: 'ENROLLMENT_RECEIPT_REQUIRED', message: '网络组件缺少消费确认。', needs: '管理员核对插件与helper版本', actor: 'network' } });
|
|
108
173
|
}).catch(() => { if (ownGeneration === generation)
|
|
@@ -129,6 +194,7 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
129
194
|
return fail('ACTIVATION_CONFLICT', '已有连接激活,不能替换身份。', '管理员核对当前开通上下文');
|
|
130
195
|
if (!runtime) {
|
|
131
196
|
selected = key;
|
|
197
|
+
policy = recoveryPolicy(config);
|
|
132
198
|
install(config);
|
|
133
199
|
publish('activating', null);
|
|
134
200
|
if (!disposed)
|
|
@@ -152,12 +218,15 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
152
218
|
async download(source, signal) { if (disposed)
|
|
153
219
|
return fail('DISPOSED', '网络服务已停止。', '重新加载插件'); return activation.phase === 'active' && runtime ? runtime.download(source, signal) : { ok: false, error: network.error ?? { code: 'NETWORK_NOT_READY', message: '连接尚未就绪。', needs: '等待N身份交接完成', actor: 'network' } }; },
|
|
154
220
|
dispose() { if (disposing)
|
|
155
|
-
return disposing; disposed = true;
|
|
221
|
+
return disposing; disposed = true; if (recoveryTimer)
|
|
222
|
+
clearTimeout(recoveryTimer); if (stableTimer)
|
|
223
|
+
clearTimeout(stableTimer); return disposing = Promise.resolve().then(async () => { publish('stopped', activation.error); try {
|
|
156
224
|
await employee.dispose();
|
|
157
225
|
}
|
|
158
226
|
finally {
|
|
159
227
|
await runtime?.dispose();
|
|
160
228
|
await handoff;
|
|
229
|
+
await recovering;
|
|
161
230
|
network = runtime?.getState() ?? { phase: 'stopped' };
|
|
162
231
|
emit(structuredClone(network));
|
|
163
232
|
publish('stopped', network.error ?? null);
|
package/lib/api.d.ts
CHANGED
|
@@ -129,6 +129,9 @@ export interface PluginSettings {
|
|
|
129
129
|
shutdownObservationSeconds?: number;
|
|
130
130
|
controlResponseMaxBytes?: number;
|
|
131
131
|
downloadMaxRedirects?: number;
|
|
132
|
+
helperRecoveryMaxAttempts?: number;
|
|
133
|
+
helperRecoveryDelaySeconds?: number;
|
|
134
|
+
helperRecoveryStableSeconds?: number;
|
|
132
135
|
statusRefreshIntervalSeconds?: number;
|
|
133
136
|
dialTimeoutSeconds?: number;
|
|
134
137
|
headerTimeoutSeconds?: number;
|
package/lib/index.js
CHANGED
|
@@ -12,13 +12,16 @@ export const Config = z.object({
|
|
|
12
12
|
shutdownObservationSeconds: positive().description('退出观察 shutdownObservationSeconds(秒):默认未设,沿开通值(现值10);等待自有组件正常退出,仅为可观测性;增大清理等更久,减小可能强制结束并报告;无关闭值,不终止未知进程'),
|
|
13
13
|
controlResponseMaxBytes: bytes().description('控制响应上限 controlResponseMaxBytes(字节):默认未设,沿开通值(现值65536);控制JSON、ready及代理首帧,也约束员工端点私有控制请求/响应和主机桥首帧;增大增加内存,减小可能拒绝合法帧;无关闭或无限值,不限制下载或SFTP正文'),
|
|
14
14
|
downloadMaxRedirects: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).description('下载重定向上限 downloadMaxRedirects:默认未设,沿开通值(现值3);增大允许更多HTTPS跳转,减小可能拒绝下载,0关闭跳转;不自动换源'),
|
|
15
|
+
helperRecoveryMaxAttempts: z.number().step(1).min(0).max(Number.MAX_SAFE_INTEGER).description('组件自动恢复次数 helperRecoveryMaxAttempts:默认3;仅复用已有身份恢复意外退出的自有helper,不重放SSH或模型请求。增大允许更多连续恢复,减小更早停止;0关闭自动恢复。次数耗尽停止并显示退出原因,需要管理员排查后正常重载插件;不重交密钥'),
|
|
16
|
+
helperRecoveryDelaySeconds: positive().description('组件恢复间隔 helperRecoveryDelaySeconds(秒):默认1;增大减少频繁拉起但恢复更慢,减小恢复更快且增加连续启动开销;无关闭值,关闭自动恢复请将helperRecoveryMaxAttempts设为0'),
|
|
17
|
+
helperRecoveryStableSeconds: positive().description('组件恢复计数清零 helperRecoveryStableSeconds(秒):默认60;连续在线达到此时长才清零。增大更严格识别反复崩溃,减小更快清零但可能把短暂在线当成稳定;无关闭值,不以刚收到ready立即清零'),
|
|
15
18
|
statusRefreshIntervalSeconds: positive().description('状态刷新间隔 statusRefreshIntervalSeconds(秒):默认沿已开通配置(现值2);影响组件状态查询及插件轮询;增大提示更迟,减小增加查询开销;无关闭值,不代表失败重试已修复'),
|
|
16
19
|
dialTimeoutSeconds: positive().description('拨号观察 dialTimeoutSeconds(秒):默认沿已开通配置(现值20);远端TCP/SSH、控制响应头、下载CONNECT/响应头;Node请求采用阶段空闲观察,仅为可观测性,增大等更久,减小可能提前中止;无关闭值,超时说明等待事项、已等时长、下一步检查,不是修复'),
|
|
17
20
|
headerTimeoutSeconds: z.number().min(1e-9).max(9223372036.854774).description('请求头观察 headerTimeoutSeconds(秒):默认沿已开通配置(现值10);本机控制与登录代理请求头;仅为可观测性,增大保留连接更久,减小可能拒绝慢请求头;无关闭值,超时在私有日志说明等待事项、已等时长、下一步检查'),
|
|
18
21
|
enrollmentObservationSeconds: z.number().min(1e-9).max(9223372036.854774).description('首次入网观察 enrollmentObservationSeconds(秒):默认沿已开通配置(现值30);仅观察首次开通结果,不用于删除已有身份;增大等更久,减小可能更早报未知;无关闭值,超时说明等待事项、已等时长、下一步检查,不重交开通材料'),
|
|
19
22
|
httpMaxHeaderBytes: bytes().description('本机HTTP头上限 httpMaxHeaderBytes(字节):默认沿已开通配置(现值1048576);控制/登录代理请求头及控制响应头;增大增加解析内存,减小可能拒绝合法头;无关闭或无限值,不是正文上限'),
|
|
20
23
|
httpMaxResponseHeaderBytes: bytes().description('下载HTTP响应头上限 httpMaxResponseHeaderBytes(字节):默认沿已开通配置(现值1048576);主机代理及下载响应头/CONNECT头;增大增加解析内存,减小可能拒绝合法头;无关闭或无限值,不是正文上限'),
|
|
21
|
-
}).description('运行策略优先级:本插件显式值 > 开通方传入值 >
|
|
24
|
+
}).description('运行策略优先级:本插件显式值 > 开通方传入值 > 原私有配置;既有策略未设置沿用原值。新增组件恢复策略未设时分别为3次/1秒/连续在线60秒,均在本页可改;不写回私有配置。活动连接改策略须正常重载插件,不重交开通材料。固定正确性约束不可关闭:macOS控制socket路径≤103字节、端口1–65535、计时器/整数可表示范围、私有700/600权限、当前用户与单身份隔离、SHA256及加密随机凭据强度。');
|
|
22
25
|
export function register(ctx, api) {
|
|
23
26
|
ctx.provide('aiWorkbenchNetwork', api);
|
|
24
27
|
ctx.effect(() => { void api.start(); return () => api.dispose(); });
|
package/lib/network.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ export interface Executable {
|
|
|
7
7
|
platform: string;
|
|
8
8
|
arch: string;
|
|
9
9
|
}
|
|
10
|
-
export declare function createNetwork(settings: PluginSettings, executable: Executable, emit?: (state: NetworkState) => void, controlIO?: typeof control): NetworkChannels & EmployeeRuntime & {
|
|
10
|
+
export declare function createNetwork(settings: PluginSettings, executable: Executable, emit?: (state: NetworkState) => void, controlIO?: typeof control, resumeIdentityOnly?: boolean, expectedConfigSHA256?: string): NetworkChannels & EmployeeRuntime & {
|
|
11
11
|
start(): Promise<void>;
|
|
12
|
+
getRecoveryConfigSHA256?(): string | undefined;
|
|
12
13
|
};
|
package/lib/network.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
|
+
import { writeSync } from 'node:fs';
|
|
3
|
+
import { randomUUID, createHash } from 'node:crypto';
|
|
2
4
|
import { join, isAbsolute, normalize, dirname, basename, relative, sep } from 'node:path';
|
|
3
5
|
import { realpath, lstat } from 'node:fs/promises';
|
|
4
6
|
import { privateFile, verifyExecutable } from "./private.js";
|
|
@@ -38,11 +40,13 @@ function statusFrame(v) {
|
|
|
38
40
|
const receipt = receiptFrame(v.receipt);
|
|
39
41
|
return { ...receipt ? { receipt } : {}, apiVersion: 1, enrollment: v.enrollment, node: { name: v.node.name, ips: v.node.ips }, host: { name: v.host.name, online: v.host.online, lastSeenAt: v.host.lastSeenAt, observedAt: v.host.observedAt }, error };
|
|
40
42
|
}
|
|
41
|
-
export function createNetwork(settings, executable, emit = () => { }, controlIO = control) {
|
|
43
|
+
export function createNetwork(settings, executable, emit = () => { }, controlIO = control, resumeIdentityOnly = false, expectedConfigSHA256) {
|
|
42
44
|
let state = { phase: 'starting' }, cfg, child;
|
|
43
45
|
let starting, disposing, disposed = false, online = false;
|
|
44
46
|
let poll;
|
|
45
47
|
let processFailed = false, startupReceipt, terminalNeed, stoppingChild, shutdownNeed;
|
|
48
|
+
let logClosed, lifecycleNeed;
|
|
49
|
+
let configSHA256;
|
|
46
50
|
const childGone = new AbortController();
|
|
47
51
|
const lifetime = new AbortController(), listeners = new Set(), updates = new Set();
|
|
48
52
|
const leases = new Set();
|
|
@@ -68,9 +72,20 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
68
72
|
const block = (code, message, requires) => publish({ phase: 'blocked', error: needs(code, message, requires) });
|
|
69
73
|
const socket = () => join(cfg.runtimeDir, 'control.sock');
|
|
70
74
|
const alive = () => !!child?.pid && !processFailed && child.exitCode === null && child.signalCode === null;
|
|
71
|
-
const terminal = (need) => {
|
|
72
|
-
|
|
73
|
-
|
|
75
|
+
const terminal = (need) => {
|
|
76
|
+
processFailed = true;
|
|
77
|
+
terminalNeed ??= need;
|
|
78
|
+
if (lifecycleNeed) {
|
|
79
|
+
lifecycleNeed.cause ??= [terminalNeed.code, terminalNeed.message, terminalNeed.cause].filter(Boolean).join(';');
|
|
80
|
+
terminalNeed = lifecycleNeed;
|
|
81
|
+
}
|
|
82
|
+
childGone.abort();
|
|
83
|
+
if (poll)
|
|
84
|
+
clearTimeout(poll);
|
|
85
|
+
const previous = state.status, status = previous ? { ...previous, enrollment: 'error', host: { ...previous.host, online: null, observedAt: null }, error: terminalNeed } : startupReceipt ? { apiVersion: 1, enrollment: 'error', node: { name: null, ips: [] }, host: { name: null, online: null, lastSeenAt: null, observedAt: null }, error: terminalNeed, receipt: startupReceipt } : undefined;
|
|
86
|
+
if (!disposed)
|
|
87
|
+
publish({ phase: 'blocked', error: terminalNeed, ...status ? { status } : {} });
|
|
88
|
+
};
|
|
74
89
|
const stopOwned = () => stoppingChild ??= (async () => {
|
|
75
90
|
const owned = child;
|
|
76
91
|
if (!owned?.pid || owned.exitCode !== null || owned.signalCode !== null)
|
|
@@ -124,11 +139,21 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
124
139
|
let configStat;
|
|
125
140
|
try {
|
|
126
141
|
configStat = await file.stat();
|
|
127
|
-
|
|
142
|
+
const bytes = await file.readFile();
|
|
143
|
+
configSHA256 = createHash('sha256').update(bytes).digest('hex');
|
|
144
|
+
cfg = JSON.parse(bytes.toString('utf8'));
|
|
128
145
|
}
|
|
129
146
|
finally {
|
|
130
147
|
await file.close();
|
|
131
148
|
}
|
|
149
|
+
if (resumeIdentityOnly && Object.hasOwn(cfg, 'authKeyFile')) {
|
|
150
|
+
startupNeed = needs('HELPER_RECOVERY_IDENTITY_REQUIRED', '无法安全恢复既有身份,连接配置仍含首次开通材料。', '管理员核对原身份与消费记录;不会读取或重新提交旧密钥');
|
|
151
|
+
throw new Error(startupNeed.code);
|
|
152
|
+
}
|
|
153
|
+
if (expectedConfigSHA256 !== undefined && configSHA256 !== expectedConfigSHA256) {
|
|
154
|
+
startupNeed = needs('HELPER_RECOVERY_CONFIG_CHANGED', '网络配置在组件运行后发生变化,已停止自动恢复。', '管理员核对原连接配置与身份;不启动其他身份、不重交密钥');
|
|
155
|
+
throw new Error(startupNeed.code);
|
|
156
|
+
}
|
|
132
157
|
if (!validHelperPolicy(settings))
|
|
133
158
|
throw new Error('CONFIG_INVALID');
|
|
134
159
|
cfg = effectiveConfig(cfg, settings);
|
|
@@ -200,25 +225,45 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
200
225
|
environment.AW_NET_PARENT_UMASK = String(employeeUmask);
|
|
201
226
|
}
|
|
202
227
|
try {
|
|
203
|
-
child = spawn(executable.path, ['serve', '--config', settings.configFile, ...helperPolicyArgs(settings), '--controlResponseMaxBytes', String(settings.controlResponseMaxBytes)], { stdio: ['ignore', 'pipe', log.fd], env: environment });
|
|
228
|
+
child = spawn(executable.path, ['serve', '--config', settings.configFile, ...resumeIdentityOnly ? ['--resumeIdentityOnly'] : [], ...expectedConfigSHA256 ? ['--resumeConfigSHA256', expectedConfigSHA256] : [], ...helperPolicyArgs(settings), '--controlResponseMaxBytes', String(settings.controlResponseMaxBytes)], { stdio: ['ignore', 'pipe', log.fd], env: environment });
|
|
204
229
|
}
|
|
205
230
|
catch (error) {
|
|
206
231
|
await log.close();
|
|
207
232
|
throw error;
|
|
208
233
|
}
|
|
209
234
|
// Install process handlers before yielding: spawn errors can arrive on nextTick.
|
|
210
|
-
const logClosed = log.close();
|
|
211
235
|
const processChild = child;
|
|
212
|
-
|
|
213
|
-
|
|
236
|
+
const runtimeId = randomUUID(), processStarted = performance.now();
|
|
237
|
+
let journalClosing = false, journalDone;
|
|
238
|
+
logClosed = new Promise(resolve => journalDone = resolve);
|
|
239
|
+
const journalFailed = () => { lifecycleNeed ??= needs('HELPER_LIFECYCLE_LOG_FAILED', '网络组件进程诊断无法完整写入,已停止自动恢复。', '管理员检查诊断日志权限、磁盘及退出记录;不重交密钥'); };
|
|
240
|
+
const record = (operation, exitCode = null, signal = null) => {
|
|
241
|
+
try {
|
|
242
|
+
const line = Buffer.from(JSON.stringify({ event: 'HELPER_PROCESS', operation, runtimeId, at: new Date().toISOString(), pid: processChild.pid ?? null, parentPid: process.pid, expected: disposed || !!stoppingChild, exitCode, signal, elapsedMs: performance.now() - processStarted }) + '\n');
|
|
243
|
+
if (writeSync(log.fd, line) !== line.length)
|
|
244
|
+
journalFailed();
|
|
245
|
+
}
|
|
246
|
+
catch {
|
|
247
|
+
journalFailed();
|
|
248
|
+
}
|
|
249
|
+
};
|
|
250
|
+
const closeJournal = () => { if (journalClosing)
|
|
251
|
+
return; journalClosing = true; void log.close().catch(() => { journalFailed(); terminal(lifecycleNeed); }).finally(journalDone); };
|
|
252
|
+
processChild.once('spawn', () => { record('spawn'); if (lifecycleNeed)
|
|
253
|
+
terminal(lifecycleNeed); });
|
|
254
|
+
processChild.on('error', () => { record('spawn-error'); terminal(needs('HELPER_PROCESS_FAILED', '网络组件进程启动或运行失败,根因未查明', '管理员检查helper完整性和进程诊断;不重交密钥')); });
|
|
255
|
+
const exited = (exitCode, signal) => {
|
|
256
|
+
record('exit', exitCode, signal);
|
|
214
257
|
processFailed = true;
|
|
215
258
|
childGone.abort();
|
|
216
259
|
if (poll)
|
|
217
260
|
clearTimeout(poll);
|
|
218
261
|
if (handshakeComplete)
|
|
219
|
-
terminal(startupNeed ?? needs('HELPER_EXITED', '
|
|
262
|
+
terminal(startupNeed ?? { ...needs('HELPER_EXITED', '网络组件已退出,当前连接不可用。', '由网络服务按既有身份及恢复策略监督;旧操作不重放,不重交密钥'), cause: `exitCode=${exitCode ?? 'null'};signal=${signal ?? 'null'}` });
|
|
263
|
+
void closeJournal();
|
|
220
264
|
};
|
|
221
265
|
processChild.once('exit', exited);
|
|
266
|
+
processChild.once('close', () => void closeJournal());
|
|
222
267
|
const ready = new Promise((resolve, reject) => {
|
|
223
268
|
let data = Buffer.alloc(0), settled = false;
|
|
224
269
|
const started = performance.now();
|
|
@@ -270,12 +315,7 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
270
315
|
processChild.stdout.on('data', read);
|
|
271
316
|
lifetime.signal.addEventListener('abort', aborted, { once: true });
|
|
272
317
|
});
|
|
273
|
-
|
|
274
|
-
await ready;
|
|
275
|
-
}
|
|
276
|
-
finally {
|
|
277
|
-
await logClosed;
|
|
278
|
-
}
|
|
318
|
+
await ready;
|
|
279
319
|
processChild.stdout.resume();
|
|
280
320
|
if (!disposed)
|
|
281
321
|
await observe();
|
|
@@ -293,7 +333,7 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
293
333
|
}
|
|
294
334
|
})();
|
|
295
335
|
const api = {
|
|
296
|
-
start, getState: snapshot,
|
|
336
|
+
start, getState: snapshot, getRecoveryConfigSHA256: () => configSHA256,
|
|
297
337
|
onState(listener) { listeners.add(listener); return () => listeners.delete(listener); },
|
|
298
338
|
async connect(target, signal) {
|
|
299
339
|
await start();
|
|
@@ -415,7 +455,8 @@ export function createNetwork(settings, executable, emit = () => { }, controlIO
|
|
|
415
455
|
await starting;
|
|
416
456
|
await Promise.all([...leases].map(x => x.release()));
|
|
417
457
|
const forced = await stopOwned();
|
|
418
|
-
|
|
458
|
+
await logClosed;
|
|
459
|
+
publish(forced ? { phase: 'blocked', error: shutdownNeed } : lifecycleNeed ? { phase: 'blocked', error: lifecycleNeed } : { phase: 'stopped' });
|
|
419
460
|
listeners.clear();
|
|
420
461
|
updates.clear();
|
|
421
462
|
})();
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from 'node:http';
|
|
2
|
+
import { recoveryPolicyFields } from './settings.ts';
|
|
3
|
+
import type { NetworkState } from './api.ts';
|
|
4
|
+
type Key = typeof recoveryPolicyFields[number];
|
|
5
|
+
type Raw = Record<string, unknown>;
|
|
6
|
+
interface Entry {
|
|
7
|
+
options: {
|
|
8
|
+
id?: string;
|
|
9
|
+
name?: string;
|
|
10
|
+
config?: Raw;
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export interface PolicyConfigEditor {
|
|
14
|
+
entries(): Entry[];
|
|
15
|
+
configuration(): {
|
|
16
|
+
entry: Entry;
|
|
17
|
+
inherited: Raw;
|
|
18
|
+
override: Raw;
|
|
19
|
+
}[];
|
|
20
|
+
edit(entry: Entry, change: (current: Raw, inherited: Raw) => Raw): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
interface PolicyObservation {
|
|
23
|
+
network?: Pick<NetworkState, 'phase' | 'error'>;
|
|
24
|
+
connection?: {
|
|
25
|
+
instanceId: string;
|
|
26
|
+
epoch: number;
|
|
27
|
+
phase: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export declare function createPolicyEditor(editor: PolicyConfigEditor, observe?: () => PolicyObservation): {
|
|
31
|
+
describe: () => {
|
|
32
|
+
ok: false;
|
|
33
|
+
error: {
|
|
34
|
+
code: string;
|
|
35
|
+
message: string;
|
|
36
|
+
};
|
|
37
|
+
} | {
|
|
38
|
+
ok: true;
|
|
39
|
+
value: {
|
|
40
|
+
revision: string;
|
|
41
|
+
runtime: {
|
|
42
|
+
observedAt: string;
|
|
43
|
+
phase: string;
|
|
44
|
+
need: {
|
|
45
|
+
code: string;
|
|
46
|
+
message: string;
|
|
47
|
+
needs: string;
|
|
48
|
+
exit: {
|
|
49
|
+
exitCode: number | null;
|
|
50
|
+
signal: string | null;
|
|
51
|
+
} | null;
|
|
52
|
+
} | null;
|
|
53
|
+
connection: {
|
|
54
|
+
instanceId: string;
|
|
55
|
+
epoch: number;
|
|
56
|
+
phase: string;
|
|
57
|
+
} | null;
|
|
58
|
+
};
|
|
59
|
+
fields: Record<Key, {
|
|
60
|
+
value: number | null;
|
|
61
|
+
explicit: boolean;
|
|
62
|
+
defaultValue: number;
|
|
63
|
+
}>;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
save: (params: URLSearchParams) => Promise<{
|
|
67
|
+
ok: false;
|
|
68
|
+
error: {
|
|
69
|
+
code: string;
|
|
70
|
+
message: string;
|
|
71
|
+
};
|
|
72
|
+
} | {
|
|
73
|
+
ok: true;
|
|
74
|
+
value: {
|
|
75
|
+
revision: string;
|
|
76
|
+
runtime: {
|
|
77
|
+
observedAt: string;
|
|
78
|
+
phase: string;
|
|
79
|
+
need: {
|
|
80
|
+
code: string;
|
|
81
|
+
message: string;
|
|
82
|
+
needs: string;
|
|
83
|
+
exit: {
|
|
84
|
+
exitCode: number | null;
|
|
85
|
+
signal: string | null;
|
|
86
|
+
} | null;
|
|
87
|
+
} | null;
|
|
88
|
+
connection: {
|
|
89
|
+
instanceId: string;
|
|
90
|
+
epoch: number;
|
|
91
|
+
phase: string;
|
|
92
|
+
} | null;
|
|
93
|
+
};
|
|
94
|
+
fields: Record<Key, {
|
|
95
|
+
value: number | null;
|
|
96
|
+
explicit: boolean;
|
|
97
|
+
defaultValue: number;
|
|
98
|
+
}>;
|
|
99
|
+
};
|
|
100
|
+
}>;
|
|
101
|
+
};
|
|
102
|
+
interface Web {
|
|
103
|
+
register(route: {
|
|
104
|
+
kind: 'prefix';
|
|
105
|
+
path: string;
|
|
106
|
+
handler(req: IncomingMessage, res: ServerResponse): Promise<void>;
|
|
107
|
+
}): () => void;
|
|
108
|
+
}
|
|
109
|
+
interface Connection {
|
|
110
|
+
requestRejection(req: IncomingMessage): number | undefined;
|
|
111
|
+
}
|
|
112
|
+
export declare function registerPolicyRoutes(web: Web, connection: Connection, service: ReturnType<typeof createPolicyEditor>): () => void;
|
|
113
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { helperRecoveryDefaults, recoveryPolicyFields, validRecoveryPolicy } from "./settings.js";
|
|
3
|
+
const fields = (value) => Object.fromEntries(recoveryPolicyFields.filter(k => Object.hasOwn(value, k)).map(k => [k, value[k]]));
|
|
4
|
+
const revision = (current, inherited) => createHash('sha256').update(JSON.stringify([fields(current), fields(inherited)])).digest('hex');
|
|
5
|
+
const failure = (code, message) => ({ ok: false, error: { code, message } });
|
|
6
|
+
const unavailable = () => failure('RECOVERY_SETTINGS_UNAVAILABLE', '我需要官方配置编辑服务和唯一且已启用的网络组件,才能读取或保存恢复设置。');
|
|
7
|
+
const recoveryNeeds = new Set(['HELPER_RECOVERING', 'HELPER_RECOVERY_EXHAUSTED', 'HELPER_RECOVERY_DISABLED', 'HELPER_RECOVERY_FAILED', 'HELPER_RECOVERY_IDENTITY_REQUIRED', 'HELPER_RECOVERY_CONFIG_CHANGED', 'HELPER_LIFECYCLE_LOG_FAILED', 'HELPER_EXITED', 'HELPER_PROCESS_FAILED', 'HELPER_START_FAILED', 'HELPER_STARTUP_EXPIRED', 'HELPER_SHUTDOWN_FORCED', 'HELPER_HANDOFF_FAILED']);
|
|
8
|
+
function runtimeObservation(observe) {
|
|
9
|
+
const observedAt = new Date().toISOString();
|
|
10
|
+
try {
|
|
11
|
+
const { network, connection } = observe(), error = network?.error;
|
|
12
|
+
const match = typeof error?.cause === 'string' ? error.cause.match(/(?:^|;)exitCode=(null|-?[0-9]+);signal=(null|SIG[A-Z0-9]+)(?:;|$)/) : null;
|
|
13
|
+
const exit = match && (match[1] === 'null' || Number.isSafeInteger(Number(match[1]))) ? { exitCode: match[1] === 'null' ? null : Number(match[1]), signal: match[2] === 'null' ? null : match[2] } : null;
|
|
14
|
+
const need = error && recoveryNeeds.has(error.code) ? { code: error.code, message: error.message, needs: error.needs, exit } : error ? { code: /^[A-Z][A-Z0-9_]+$/.test(error.code) ? error.code : 'NETWORK_STATUS_UNKNOWN', message: '网络组件当前不可用;请在远程主机连接设置中查看对应诊断。', needs: '我需要管理员核对组件状态;不会重新提交入网密钥。', exit: null } : network ? null : { code: 'NETWORK_SERVICE_REQUIRED', message: '我需要已启用的网络组件才能读取状态。', needs: '请在官方插件管理中查看网络组件是否启用。', exit: null };
|
|
15
|
+
return { observedAt, phase: network?.phase ?? 'missing', need, connection: connection && typeof connection.instanceId === 'string' && Number.isSafeInteger(connection.epoch) && connection.epoch >= 0 ? { instanceId: connection.instanceId, epoch: connection.epoch, phase: connection.phase } : null };
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
return { observedAt, phase: 'unknown', need: { code: 'NETWORK_STATUS_UNKNOWN', message: '组件状态读取失败,根因未查明。', needs: '我需要管理员查看组件诊断;本页不猜测当前连接状态。', exit: null }, connection: null };
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export function createPolicyEditor(editor, observe = () => ({})) {
|
|
22
|
+
function target() {
|
|
23
|
+
const entries = editor.entries().filter(e => e.options.id === 'ai-workbench-network' && e.options.name === '@ai-workbench/network');
|
|
24
|
+
if (entries.length !== 1)
|
|
25
|
+
return null;
|
|
26
|
+
const entry = entries[0], layers = editor.configuration().filter(v => v.entry === entry);
|
|
27
|
+
if (layers.length !== 1)
|
|
28
|
+
return null;
|
|
29
|
+
return layers[0];
|
|
30
|
+
}
|
|
31
|
+
function describe() {
|
|
32
|
+
try {
|
|
33
|
+
const found = target();
|
|
34
|
+
if (!found)
|
|
35
|
+
return unavailable();
|
|
36
|
+
const current = found.entry.options.config ?? {};
|
|
37
|
+
if (!validRecoveryPolicy(current))
|
|
38
|
+
return failure('RECOVERY_SETTINGS_INVALID', '恢复设置包含非法值,请通过官方配置编辑流程修正。');
|
|
39
|
+
return { ok: true, value: { revision: revision(current, found.inherited), runtime: runtimeObservation(observe), fields: Object.fromEntries(recoveryPolicyFields.map(key => [key, { value: current[key] ?? null, explicit: Object.hasOwn(current, key), defaultValue: helperRecoveryDefaults[key] }])) } };
|
|
40
|
+
}
|
|
41
|
+
catch {
|
|
42
|
+
return unavailable();
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
async function save(params) {
|
|
46
|
+
const allowed = ['revision', ...recoveryPolicyFields];
|
|
47
|
+
if ([...params.keys()].some(k => !allowed.includes(k)) || allowed.some(k => params.getAll(k).length !== 1) || !/^[a-f0-9]{64}$/.test(params.get('revision') ?? ''))
|
|
48
|
+
return failure('RECOVERY_SETTINGS_INPUT_INVALID', '设置未保存:请只提交当前页面中的三项恢复设置。');
|
|
49
|
+
const values = {};
|
|
50
|
+
for (const key of recoveryPolicyFields) {
|
|
51
|
+
const raw = params.get(key);
|
|
52
|
+
if (raw === 'unset') {
|
|
53
|
+
values[key] = 'unset';
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
if (!/^(?:0|[1-9][0-9]*)(?:\.[0-9]+)?$/.test(raw))
|
|
57
|
+
return failure('RECOVERY_SETTINGS_INPUT_INVALID', '设置未保存:次数须为非负安全整数;秒数须在0.001到2147483.647之间。');
|
|
58
|
+
const value = Number(raw);
|
|
59
|
+
if (!validRecoveryPolicy({ [key]: value }))
|
|
60
|
+
return failure('RECOVERY_SETTINGS_INPUT_INVALID', '设置未保存:次数须为非负安全整数;秒数须在0.001到2147483.647之间。');
|
|
61
|
+
values[key] = value;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
const found = target();
|
|
65
|
+
if (!found)
|
|
66
|
+
return unavailable();
|
|
67
|
+
await editor.edit(found.entry, (current, inherited) => {
|
|
68
|
+
if (revision(current, inherited) !== params.get('revision'))
|
|
69
|
+
throw new PolicyConflict();
|
|
70
|
+
const next = { ...current };
|
|
71
|
+
for (const key of recoveryPolicyFields) {
|
|
72
|
+
if (values[key] === 'unset') {
|
|
73
|
+
delete next[key];
|
|
74
|
+
}
|
|
75
|
+
else
|
|
76
|
+
next[key] = values[key];
|
|
77
|
+
}
|
|
78
|
+
if (!validRecoveryPolicy(next))
|
|
79
|
+
throw new Error('invalid-policy');
|
|
80
|
+
return next;
|
|
81
|
+
});
|
|
82
|
+
return describe();
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
return error instanceof PolicyConflict ? failure('RECOVERY_SETTINGS_CONFLICT', '设置未保存:配置已被其他操作修改,请重新打开本页核对后保存。') : failure('RECOVERY_SETTINGS_SAVE_FAILED', '官方配置保存或重载失败,请检查插件管理中的错误;本页未确认修改生效。');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return { describe, save };
|
|
89
|
+
}
|
|
90
|
+
class PolicyConflict extends Error {
|
|
91
|
+
}
|
|
92
|
+
export function registerPolicyRoutes(web, connection, service) {
|
|
93
|
+
return web.register({ kind: 'prefix', path: '/ai-workbench-network/policy', async handler(req, res) {
|
|
94
|
+
res.setHeader('Cache-Control', 'no-store');
|
|
95
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
96
|
+
const reply = (status, result) => { res.statusCode = status; res.end(JSON.stringify(result)); };
|
|
97
|
+
const rejected = connection.requestRejection(req);
|
|
98
|
+
if (rejected !== undefined) {
|
|
99
|
+
reply(rejected, failure('RECOVERY_SETTINGS_FORBIDDEN', '需要当前官方客户端的有效连接。'));
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
const url = new URL(req.url ?? '/', 'http://dsh');
|
|
103
|
+
if (url.pathname !== '/ai-workbench-network/policy') {
|
|
104
|
+
reply(404, failure('RECOVERY_SETTINGS_NOT_FOUND', '没有此设置操作。'));
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
if (req.method === 'GET' && !url.search) {
|
|
108
|
+
const result = service.describe();
|
|
109
|
+
reply(result.ok ? 200 : 503, result);
|
|
110
|
+
return;
|
|
111
|
+
}
|
|
112
|
+
if (req.method === 'POST') {
|
|
113
|
+
if (req.headers['transfer-encoding'] || req.headers['content-length'] !== undefined && req.headers['content-length'] !== '0') {
|
|
114
|
+
reply(400, failure('RECOVERY_SETTINGS_INPUT_INVALID', '设置未保存:本操作不接受请求正文。'));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
const result = await service.save(url.searchParams);
|
|
118
|
+
reply(result.ok ? 200 : result.error.code === 'RECOVERY_SETTINGS_INPUT_INVALID' ? 400 : 409, result);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
reply(400, failure('RECOVERY_SETTINGS_INPUT_INVALID', '不支持此设置操作。'));
|
|
122
|
+
} });
|
|
123
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { createPolicyEditor, registerPolicyRoutes } from "./policy-management.js";
|
|
2
|
+
export const name = 'ai-workbench-network-settings';
|
|
3
|
+
// 伴生行不持有入网身份,目标 runtime 正常重载时保留保存应答路径。
|
|
4
|
+
export function apply(ctx) {
|
|
5
|
+
ctx.inject(['webServer', 'connection', 'configEditor'], host => {
|
|
6
|
+
host.effect(() => registerPolicyRoutes(host.get('webServer'), host.get('connection'), createPolicyEditor(host.get('configEditor'), () => {
|
|
7
|
+
const network = host.get('aiWorkbenchNetwork', false);
|
|
8
|
+
const connect = host.get('aiWorkbenchConnect', false);
|
|
9
|
+
return { network: network?.getState(), connection: connect?.getConnectionState?.() };
|
|
10
|
+
})));
|
|
11
|
+
});
|
|
12
|
+
}
|
package/lib/settings.d.ts
CHANGED
|
@@ -1,9 +1,21 @@
|
|
|
1
1
|
import type { PluginSettings, HelperConfig } from './api.ts';
|
|
2
2
|
export declare const helperPolicyFields: readonly ["statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes", "httpMaxResponseHeaderBytes"];
|
|
3
|
-
export declare const
|
|
4
|
-
export declare const
|
|
3
|
+
export declare const recoveryPolicyFields: readonly ["helperRecoveryMaxAttempts", "helperRecoveryDelaySeconds", "helperRecoveryStableSeconds"];
|
|
4
|
+
export declare const helperRecoveryDefaults: Readonly<{
|
|
5
|
+
helperRecoveryMaxAttempts: 3;
|
|
6
|
+
helperRecoveryDelaySeconds: 1;
|
|
7
|
+
helperRecoveryStableSeconds: 60;
|
|
8
|
+
}>;
|
|
9
|
+
export declare const pluginPolicyFields: readonly ["startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "downloadMaxRedirects", "helperRecoveryMaxAttempts", "helperRecoveryDelaySeconds", "helperRecoveryStableSeconds"];
|
|
10
|
+
export declare const settingsFields: readonly ["configFile", "helperLogFile", "startupObservationSeconds", "shutdownObservationSeconds", "controlResponseMaxBytes", "downloadMaxRedirects", "helperRecoveryMaxAttempts", "helperRecoveryDelaySeconds", "helperRecoveryStableSeconds", "statusRefreshIntervalSeconds", "dialTimeoutSeconds", "headerTimeoutSeconds", "enrollmentObservationSeconds", "httpMaxHeaderBytes", "httpMaxResponseHeaderBytes"];
|
|
5
11
|
export declare const timerSeconds: (v: unknown) => v is number;
|
|
6
12
|
export declare const positiveInteger: (v: unknown) => v is number;
|
|
13
|
+
export declare function validRecoveryPolicy(settings: PluginSettings): boolean;
|
|
14
|
+
export declare function recoveryPolicy(settings: PluginSettings): {
|
|
15
|
+
helperRecoveryMaxAttempts: number;
|
|
16
|
+
helperRecoveryDelaySeconds: number;
|
|
17
|
+
helperRecoveryStableSeconds: number;
|
|
18
|
+
};
|
|
7
19
|
export declare function validHelperPolicy(settings: PluginSettings): boolean;
|
|
8
20
|
export declare function mergedSettings(initial: PluginSettings, provided: PluginSettings): PluginSettings;
|
|
9
21
|
export declare function effectiveConfig(config: HelperConfig, settings: PluginSettings): HelperConfig;
|
package/lib/settings.js
CHANGED
|
@@ -1,8 +1,17 @@
|
|
|
1
1
|
export const helperPolicyFields = ['statusRefreshIntervalSeconds', 'dialTimeoutSeconds', 'headerTimeoutSeconds', 'enrollmentObservationSeconds', 'httpMaxHeaderBytes', 'httpMaxResponseHeaderBytes'];
|
|
2
|
-
export const
|
|
2
|
+
export const recoveryPolicyFields = ['helperRecoveryMaxAttempts', 'helperRecoveryDelaySeconds', 'helperRecoveryStableSeconds'];
|
|
3
|
+
export const helperRecoveryDefaults = Object.freeze({ helperRecoveryMaxAttempts: 3, helperRecoveryDelaySeconds: 1, helperRecoveryStableSeconds: 60 });
|
|
4
|
+
export const pluginPolicyFields = ['startupObservationSeconds', 'shutdownObservationSeconds', 'controlResponseMaxBytes', 'downloadMaxRedirects', ...recoveryPolicyFields];
|
|
3
5
|
export const settingsFields = ['configFile', 'helperLogFile', ...pluginPolicyFields, ...helperPolicyFields];
|
|
4
6
|
export const timerSeconds = (v) => typeof v === 'number' && Number.isFinite(v) && v >= 0.001 && v * 1000 <= 2147483647;
|
|
5
7
|
export const positiveInteger = (v) => Number.isSafeInteger(v) && Number(v) > 0;
|
|
8
|
+
export function validRecoveryPolicy(settings) {
|
|
9
|
+
const max = settings.helperRecoveryMaxAttempts;
|
|
10
|
+
return (max === undefined || (Number.isSafeInteger(max) && max >= 0)) && recoveryPolicyFields.slice(1).every(key => settings[key] === undefined || timerSeconds(settings[key]));
|
|
11
|
+
}
|
|
12
|
+
export function recoveryPolicy(settings) {
|
|
13
|
+
return { helperRecoveryMaxAttempts: settings.helperRecoveryMaxAttempts ?? helperRecoveryDefaults.helperRecoveryMaxAttempts, helperRecoveryDelaySeconds: settings.helperRecoveryDelaySeconds ?? helperRecoveryDefaults.helperRecoveryDelaySeconds, helperRecoveryStableSeconds: settings.helperRecoveryStableSeconds ?? helperRecoveryDefaults.helperRecoveryStableSeconds };
|
|
14
|
+
}
|
|
6
15
|
export function validHelperPolicy(settings) {
|
|
7
16
|
return helperPolicyFields.every(key => {
|
|
8
17
|
const v = settings[key];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ai-workbench/network",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=24.13.1"
|
|
@@ -19,6 +19,11 @@
|
|
|
19
19
|
"default": "./lib/cordis.js"
|
|
20
20
|
},
|
|
21
21
|
"./package.json": "./package.json",
|
|
22
|
+
"./settings": {
|
|
23
|
+
"types": "./lib/settings-host.d.ts",
|
|
24
|
+
"default": "./lib/settings-host.js"
|
|
25
|
+
},
|
|
26
|
+
"./client": "./client/index.cjs",
|
|
22
27
|
"./locale/*.json": "./locale/*.json"
|
|
23
28
|
},
|
|
24
29
|
"files": [
|
|
@@ -26,9 +31,17 @@
|
|
|
26
31
|
"NOTICE.md",
|
|
27
32
|
"cordis.patch.yml",
|
|
28
33
|
"locale",
|
|
29
|
-
"helpers"
|
|
34
|
+
"helpers",
|
|
35
|
+
"client"
|
|
30
36
|
],
|
|
31
37
|
"dsh": {
|
|
38
|
+
"client": {
|
|
39
|
+
"platform": "web",
|
|
40
|
+
"inject": [
|
|
41
|
+
"@deepseek-ai/dsh-client-ui-plugin-manager",
|
|
42
|
+
"@deepseek-ai/dsh-client-ui-slots"
|
|
43
|
+
]
|
|
44
|
+
},
|
|
32
45
|
"bundle": {
|
|
33
46
|
"patch": "./cordis.patch.yml"
|
|
34
47
|
}
|
|
@@ -46,6 +59,8 @@
|
|
|
46
59
|
"@deepseek-ai/cordis": "4.0.4",
|
|
47
60
|
"@deepseek-ai/schemastery": "3.18.4",
|
|
48
61
|
"@types/node": "24.0.0",
|
|
62
|
+
"react": "18.3.1",
|
|
63
|
+
"react-test-renderer": "18.3.1",
|
|
49
64
|
"typescript": "5.9.3"
|
|
50
65
|
},
|
|
51
66
|
"publishConfig": {
|