@ai-workbench/network 0.4.8 → 0.4.9
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 +4 -3
- package/README.md +32 -20
- package/client/index.cjs +15 -33
- package/helpers/darwin-arm64/NOTICE.md +4 -3
- package/helpers/darwin-arm64/bin/aw-net +0 -0
- package/helpers/darwin-arm64/helper-manifest.json +1 -1
- package/helpers/darwin-arm64/licenses-manifest.json +4 -4
- package/helpers/darwin-arm64/package.json +1 -1
- package/helpers/darwin-x64/NOTICE.md +4 -3
- package/helpers/darwin-x64/bin/aw-net +0 -0
- package/helpers/darwin-x64/helper-manifest.json +1 -1
- package/helpers/darwin-x64/licenses-manifest.json +4 -4
- package/helpers/darwin-x64/package.json +1 -1
- package/lib/activation.js +66 -22
- package/lib/api.d.ts +0 -17
- package/lib/control.d.ts +0 -1
- package/lib/control.js +0 -32
- package/lib/employee-endpoint.js +5 -5
- package/lib/index.js +8 -10
- package/lib/network.d.ts +4 -2
- package/lib/network.js +67 -102
- package/lib/policy-management.d.ts +0 -37
- package/lib/policy-management.js +7 -59
- package/lib/private.d.ts +1 -1
- package/lib/private.js +6 -2
- package/lib/settings-host.js +1 -1
- package/lib/settings.d.ts +3 -3
- package/lib/settings.js +3 -3
- package/package.json +1 -1
- package/lib/download.d.ts +0 -2
- package/lib/download.js +0 -128
package/NOTICE.md
CHANGED
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# 依赖与来源(N 第 1 轮)
|
|
2
2
|
|
|
3
|
-
N2 helper 已接入固定 tsnet 并可通过 Makefile
|
|
3
|
+
N2 helper 已接入固定 tsnet 并可通过 Makefile 构建;未部署、未发包。历史认证HTTP/CONNECT代理及Electron44组件诊断已归档,0.4.9已删除该产品能力;DSH插件0.3.0及新消费者Cordis组件诊断已交。真实证据见 N 回执。
|
|
4
4
|
|
|
5
5
|
| 组件 | 固定版本/来源提交 | 协议 | 用途及当前证据 |
|
|
6
6
|
|---|---|---|---|
|
|
7
7
|
| tailscale.com/tsnet | v1.104.1 / `9a522a9786c97eb7910c01ccb7bd66557b04c910` | BSD-3-Clause | [上游源码](https://github.com/tailscale/tailscale/tree/v1.104.1/tsnet),实际 helper 依赖;完整闭包固定于 go.mod/go.sum |
|
|
8
|
-
| tailscaled HTTP proxy | 同上,`cmd/tailscaled/proxy.go` | BSD-3-Clause | [CONNECT 处理源码](https://github.com/tailscale/tailscale/blob/v1.104.1/cmd/tailscaled/proxy.go),只读参考;本实现使用net/http.ReverseProxy及SSH direct-tcpip,未复制该文件 |
|
|
9
8
|
| DeepSeek Harness | 0.2.0-rc.2 / `639ed015397290b3745d163aafe02ffee4aa3f84` | MIT(已复核固定源码package.json),不复制核心代码 | [官方上游](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.2.0-rc.2),只读参考;实现只在本仓,浅克隆验证完成后精确清理 |
|
|
10
9
|
| Go | 构建固定1.27.3;目标tsnet要求至少1.27.1 | BSD-3-Clause | `go mod download tailscale.com@v1.104.1` 自动选择 1.27.3。已用自动选择的1.27.3通过普通/race/vet/build |
|
|
11
10
|
|
|
@@ -15,7 +14,7 @@ N2 helper 已接入固定 tsnet 并可通过 Makefile 构建;未部署、未
|
|
|
15
14
|
|
|
16
15
|
N3 SSH使用 golang.org/x/crypto/ssh v0.57.0(BSD-3-Clause;实际锁定版本见go.mod);只使用none认证与可信known_hosts。Electron44.0.0官方darwin-arm64 ZIP为诊断工具,MIT及其随附第三方许可;SHA256与引擎版本见 `docs/evidence/n3-2026-10-20/electron-artifact.json`。独立进程/内存session,没有复制进发布包。
|
|
17
16
|
|
|
18
|
-
N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MIT,来源为固定DSH提交的vendor目录及官方npm包。TS5.9.3/Node24类型是构建依赖;实际闭包固定package-lock.json
|
|
17
|
+
N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MIT,来源为固定DSH提交的vendor目录及官方npm包。TS5.9.3/Node24类型是构建依赖;实际闭包固定package-lock.json,新消费者闭包另存证据。0.4.9删除主机下载与登录代理实现,不再提供相关能力。源码/API升级需要在自有仓库适配,不提上游PR/issue或等待上游修改。
|
|
19
18
|
|
|
20
19
|
0.4.3分发说明:唯一发布单元为network主包;helpers/darwin-arm64与helpers/darwin-x64的package.json是private内嵌身份元数据,不是独立发布依赖。每目标的helper-manifest.json固定二进制SHA,THIRD_PARTY_NOTICES.txt保留该目标Go实际编译依赖的许可原文,licenses-manifest.json列出模块、编译包、许可来源文件与SHA。构建来源为本仓cmd/aw-net及internal、固定go.mod/go.sum,Go1.27.3、CGO_ENABLED=0、ts_omit_logtail。
|
|
21
20
|
|
|
@@ -29,3 +28,5 @@ D20(0.4.4)实际端点复用:x/crypto/ssh v0.57.0(BSD-3-Clause),crea
|
|
|
29
28
|
0.4.8 登记连接观测:沿原 register 拨号上下文保留独立端点观察器,共享 SSH channel 关联同一条 TCP。白名单新增可空 RTT 初始化、SRTT/RTTVar/RTO 和恢复状态;端点锁→RTT锁沿原 getTCPInfo 顺序,保留标准 LockUser/UnlockUser(后者可调度原已排队处理,不宣称绝对零调度影响)。没有更改发送器算法、连接期限、重试、探测、取消或身份规则。新增私有 socket 只读 GET 与既有生命周期事件日志;不记录载荷、命令、环境或凭据。没有新增运行时模块或设置,来源/补丁 SHA 随交付更新;不依赖上游修改。
|
|
30
29
|
|
|
31
30
|
同轮 tailscale.com v1.104.1(BSD-3-Clause)固定源码复制至 third_party/tailscale,只新增 magicsock/observation_aw.go 的静态字段复制出口及其测试,不改上游原文件。它避免 UpdateStatus 的 WireGuard-only 特殊分支选择/延长直连信任;原模块/档案 SHA、逐文件来源和新增补丁见该目录 UPSTREAM.json/OBSERVATION.patch。两目标构建引用此本仓模块,原许可照常进入实际编译闭包。
|
|
31
|
+
|
|
32
|
+
0.4.9 修复:控制通道不可用且已有可恢复身份时沿原监督恢复网络组件;员工端点历史关闭失败仅作诊断,不再阻止下一轮重新准备。恢复提示与原设置说明同步;新增设置及运行依赖均为零,不改连接超时或应用重试;按owner追加删除诊断失败闸门/主机下载/登录代理,开通未知先查持久身份、无身份时原密钥仅补救一次,记录预算沿原attempted-v1持久保存。
|
package/README.md
CHANGED
|
@@ -1,53 +1,57 @@
|
|
|
1
|
-
|
|
1
|
+
安装:本轮0.4.9由编排交付固定tgz;在官方 DSH「插件 → 添加插件」填写固定包绝对路径。工人不执行 npm publish。
|
|
2
|
+
|
|
3
|
+
0.4.9 修复断线后下一轮无法重新开始:控制通道不可用时,已有可恢复身份沿原监督流程先停止旧实例再恢复;旧端点关闭失败只记该次诊断,不跨轮阻止同一或替换后的组件重新准备。已排恢复即隔离旧回调,避免迟到在线/退出覆盖监督状态。没有新增设置或业务超时/重试;按owner追加决定删除诊断闸门、主机下载与登录代理,开通未知按下段补救一次。
|
|
2
4
|
|
|
3
5
|
# 远程主机网络
|
|
4
6
|
|
|
5
|
-
|
|
7
|
+
开通中断结果未知时,先检查本地持久身份,有则直接使用;无则用原一次性密钥补救一次。原attempted-v1持久记录最多两次N主动交AuthKey,交第二次前原子写入,不因DSH重启清零;旧journal仍绑定原配置与材料源。第二次仍未知时保留原组件的watch/刷新,直到真实在线并确认同一NodeID;未在线的成功状态查询不能清掉本次失败。明确密钥拒绝等终态也不能被迟到的网络错误复活。只有控制面明确auth-key已使用/作废,且本地无可用身份时,才提示「开通链接已失效,请向管理员领取新的。」
|
|
8
|
+
|
|
9
|
+
主机出口下载和登录代理已按owner删除;登录走员工本机网络,组件下载沿L的官方地址。downloadMaxRedirects/httpMaxResponseHeaderBytes不再是公开设置;旧activate值仅丢弃。私有JSON的proxyListen/httpMaxResponseHeaderBytes仅保留旧journal摘要兼容,不运行、不作运行取值校验或CLI覆盖,新文件不补回零值。
|
|
6
10
|
|
|
7
|
-
0.4.
|
|
11
|
+
0.4.9 当前为待交付固定版本,由编排统一发布公开 npm;工人不执行 publish。发布前可由官方添加插件入口安装本轮固定主包 tgz 的绝对路径,无需再解析平台依赖。
|
|
8
12
|
|
|
9
|
-
|
|
13
|
+
已有身份由稳定插件服务监督自有 helper。已确认可复用身份的 helper 意外退出或控制通道不可用后,先撤回旧通道和员工执行器、发布 lost,等待旧实例清理完成,再走原私有路径、SHA 校验、ready 和身份确认路径启动新实例。恢复成功发布 restored,由 L 更新连接 epoch;不重放 SSH、模型输入、命令或写文件请求。正常卸载不重启;身份/配置/权限等真实异常保留本次失败。开通未知按owner规则补救,日志异常不阻止连接。已有身份恢复内部启用resumeIdentityOnly,由Go在实际密钥入口拒绝无身份时读取开通材料;旧配置有key引用而已有持久身份时直接用身份、不读或删除未绑定key。这不是员工参数。正常identity实例保存实际读取配置的SHA256,恢复在插件/helper两端对同一读取快照核比;配置变化明确HELPER_RECOVERY_CONFIG_CHANGED,不改指另一身份目录。首次key清理交接后以新identity配置建立摘要。私有stateDir仍由N维护,本轮不新增跨实例NodeID协议,不声称防住整套受保护身份文件被外部替换。
|
|
14
|
+
|
|
15
|
+
既有三个官方插件管理恢复设置保持不变(基础策略删除两项后为八项):
|
|
10
16
|
|
|
11
17
|
| 名称 | 未设置时 | 效果 |
|
|
12
18
|
|---|---:|---|
|
|
13
|
-
| helperRecoveryMaxAttempts | 3 次 | 0
|
|
19
|
+
| helperRecoveryMaxAttempts | 3 次 | 0 关闭;连续恢复耗尽后明确停止,显示原因及员工关闭再启用后的下一轮操作步骤。增大允许更多连续恢复,减小更早停止。 |
|
|
14
20
|
| helperRecoveryDelaySeconds | 1 秒 | 每次恢复前等待;增大会延后恢复并减少频繁拉起,减小增加启动开销。 |
|
|
15
21
|
| helperRecoveryStableSeconds | 60 秒 | 连续 online 达到此时长才清零失败计数;增大更严格,减小可能把短暂在线当成稳定。 |
|
|
16
22
|
|
|
17
|
-
|
|
23
|
+
本插件显式设置优先于开通方设置;只在缺省时采用上述三项默认,不写回原配置。改活动连接策略须正常重载插件;恢复耗尽后可在网络恢复时关闭再启用网络组件,再发送下一轮,不要求员工重新开通。恢复间隔与稳定时长范围 0.001..2147483.647 秒,次数为 0..Number.MAX_SAFE_INTEGER 的整数;这些是计时器/整数的可表示范围。每次启动、退出和启动进程错误以 HELPER_PROCESS 固定 JSON 追加到原私有 helperLogFile,含时间、PID、实例 ID、退出码、实际信号、预期标记和耗时,不记录命令行、环境、密钥或请求正文。捕获 TERM 后自行 exit 0 的实际信号为 null,不猜作 SIGTERM。真实旧实例清理失败给出本次Need;诊断open/write/close错误或关闭未完成都不阻止恢复/停止,只固定一行本地告警。没有安全日志时stderr用ignore,不进入ready stdout。
|
|
18
24
|
|
|
19
|
-
插件通过官方 bundle/Cordis 接口提供远程主机网络、设备身份、SSH
|
|
25
|
+
插件通过官方 bundle/Cordis 接口提供远程主机网络、设备身份、SSH 通道和员工回连能力。不替换 DSH 服务、布局或默认设置,不增加工作区/登录界面。L 负责可信连接资料导入和登录入口,员工不手填主机、用户名、路径或密钥。没有连接资料时明确说明需要导入,不把插件 Running 当作网络已连接。
|
|
20
26
|
|
|
21
27
|
主包从0.4.3起完整携带`helpers/darwin-arm64`和`helpers/darwin-x64`两架构Go助手,不再声明平台optionalDependencies或下载平台包。运行时只选择本机架构的内嵌文件,严格校验主包与内嵌metadata名称/版本/private/os/cpu、manifest平台/SHA格式,并在启动前验证实际二进制SHA256;缺失、损坏或平台不支持时用中文明确说明需要完整主包并保留原始cause,不使用PATH、旧平台包、另一架构或安装时下载。安装无postinstall/prepare等脚本。两目标分别附实际编译闭包的许可证原文、模块/文件来源与摘要,构建遇缺失声明明确失败。只提供macOS arm64/x64;x64构建/字节检查不等于Intel真机运行验收。
|
|
22
28
|
|
|
23
29
|
旧0.4.1/0.4.2安装保留的锁/依赖树仍可能触发D16,直接add新主包不是迁移方案;owner已取消产品迁移;测试机的一次remove旧N→add新N整理由编排处理,保留原身份/配置,不把add当清理旧图的替代。本仓不移动员工profile/状态、不要求员工手删锁;公开`/api`、`/cordis`及wire apiVersion=1不变。
|
|
24
30
|
|
|
25
|
-
一个默认 profile 只启用一组 N bundle(网络 runtime 与同包设置伴生行)。独立安装与 L 聚合安装不要重复挂载 N。N
|
|
31
|
+
一个默认 profile 只启用一组 N bundle(网络 runtime 与同包设置伴生行)。独立安装与 L 聚合安装不要重复挂载 N。N 的网络身份按系统用户隔离、持久保存;未知结果按本轮规则优先查持久身份,无身份才用原材料补救一次。公开 `aiWorkbenchNetwork.activate(settings, signal)`、`getActivationState/onActivationState` 和通道 API;公开类型入口 `/api`,Cordis 类型入口 `/cordis`。`active` 表示身份恢复与消费交接完成,不等于主机 SSH 或 AI 登录已通过。原六项安装设置仍由可信开通提供;下述策略可在N自己的官方Config独立设置,不要求员工手填路径/身份,不新增数值默认。
|
|
26
32
|
|
|
27
33
|
## 本插件设置
|
|
28
34
|
|
|
29
|
-
|
|
35
|
+
当前wire apiVersion仍1;以下保留8项基础数值策略(3项插件策略、5项helper策略),另有上节3项既有组件恢复策略。N官方Config导出这些具名项,不使用新面板或L设置代替。**不改变数值默认**:N原来没有数值默认,本版仍不注入;未设沿用原开通值,已有自定义值不被覆盖。表中数字为当前L装配值,不是给所有旧配置强改的新默认。
|
|
30
36
|
|
|
31
37
|
| N Config具名项 | 未设时的来源 / 当前L装配值 | 范围与改值后果 |
|
|
32
38
|
|---|---|---|
|
|
33
39
|
| startupObservationSeconds | 开通方显式提供 / 35秒 | helper ready观察;增大等更久,减小可能提前未知;无关闭值 |
|
|
34
40
|
| shutdownObservationSeconds | 开通方显式提供 / 10秒 | 等自有helper正常退出;增大清理更久,减小可能强制结束并报告;无关闭值 |
|
|
35
|
-
| controlResponseMaxBytes | 开通方显式提供 / 65536B | 控制JSON/ready
|
|
36
|
-
| downloadMaxRedirects | 开通方显式提供 / 3 | HTTPS逐跳重定向;增大多跳,减小可能拒下载;0关闭跟随 |
|
|
41
|
+
| controlResponseMaxBytes | 开通方显式提供 / 65536B | 控制JSON/ready及员工端点桥首帧;增大增内存,减小拒合法帧;无无限值 |
|
|
37
42
|
| statusRefreshIntervalSeconds | 原私有配置 / 2秒 | helper刷新与插件状态轮询;增大提示迟,减小开销高;无关闭值 |
|
|
38
|
-
| dialTimeoutSeconds | 原私有配置 / 20秒 | TCP/SSH
|
|
39
|
-
| headerTimeoutSeconds | 原私有配置 / 10秒 |
|
|
43
|
+
| dialTimeoutSeconds | 原私有配置 / 20秒 | TCP/SSH、控制头;增大等更久,减小可能提前中止;无关闭值 |
|
|
44
|
+
| headerTimeoutSeconds | 原私有配置 / 10秒 | 本机控制读请求头;增大占连接久,减小拒慢头;无关闭值 |
|
|
40
45
|
| enrollmentObservationSeconds | 原私有配置 / 30秒 | 仅首次key入网结果;增大久等,减小更早未知;无关闭值,不用于销毁已有身份 |
|
|
41
|
-
| httpMaxHeaderBytes | 原私有配置 / 1048576B |
|
|
42
|
-
| httpMaxResponseHeaderBytes | 原私有配置 / 1048576B | 主机代理、下载响应头及CONNECT头;增大增内存,减小拒合法头;无无限值 |
|
|
46
|
+
| httpMaxHeaderBytes | 原私有配置 / 1048576B | 本机控制请求头与控制响应头;增大增内存,减小拒合法头;无无限值 |
|
|
43
47
|
|
|
44
|
-
优先级:**N官方Config中显式策略 > L.activate的显式同名策略 > 原私有helper
|
|
48
|
+
优先级:**N官方Config中显式策略 > L.activate的显式同名策略 > 原私有helper配置**。旧三项插件策略仍须N或L提供,缺项明确SETTINGS_REQUIRED;五个helper策略可不提供,原值保持。无路径/身份的N策略配置只注册未开通服务,等待L按原合同激活,不自动开通。已激活后同实例改变有效策略仍ACTIVATION_CONFLICT,需官方插件正常重载,不热替换身份/启动同身份第二实例。
|
|
45
49
|
|
|
46
|
-
|
|
50
|
+
五个helper覆盖通过`serve --statusRefreshIntervalSeconds <数值>`等同名参数白名单传入,只含非秘密数值;L私有文件不写回。helper严格按**原始配置**校验交接inode/摘要与入网journal,运行实例另用有效策略,不把新数值混入原身份摘要。只有原合同的已确认key清理会移除authKeyFile及精确源key;策略不扩大清理权限。N无需改L即可兼容现有开通,但L展示应说明本插件显式覆盖优先,跨模块请求N-L-VISIBLE-LIMITS-001。
|
|
47
51
|
|
|
48
|
-
所有超时只为可观测性,触发后提供等待事项、实际已等秒数、具名期限与下一步检查,根因仍未知,不重试/换源/换出口。Node
|
|
52
|
+
所有超时只为可观测性,触发后提供等待事项、实际已等秒数、具名期限与下一步检查,根因仍未知,不重试/换源/换出口。Node控制的req.setTimeout沿用原**阶段空闲**计时;远端拨号/SSH和请求头沿用原阶段deadline。到响应头/握手结束即移除该阶段期限,正文和SSH流仍由显式取消/释放/进程退出管理,不增加流总时限。headerTimeout在HTTP handler之前由实际Read超时处记入脱敏私有日志,不记录请求内容/授权参数;主动取消后台读不误报。首次开通到期保留当前失败并按下述一次补救继续原观察;已有身份不套首次窗口。
|
|
49
53
|
|
|
50
|
-
固定正确性约束不是可任意调大的运行策略:Unix socket≤103字节、端口1..65535、计时器/整数可表示范围、HTTP实现溢出保护、700/600权限、当前UID/单身份、SHA256
|
|
54
|
+
固定正确性约束不是可任意调大的运行策略:Unix socket≤103字节、端口1..65535、计时器/整数可表示范围、HTTP实现溢出保护、700/600权限、当前UID/单身份、SHA256和SSH主机信任均不可关闭,官方Config总说明及盘点表明确列出。本节旧0.4.2未新增重试;0.4.6新增的三项helper恢复策略见上,不是SSH或模型业务重试。会话寿命未新增上限。D14旧manifest空catch已在0.4.0修复,当前packaged-helper继续返回原始cause,本轮加坏主manifest回归。
|
|
51
55
|
|
|
52
56
|
开发: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 副本由构建生成,不提交。
|
|
53
57
|
|
|
@@ -62,6 +66,14 @@ N从官方后端自行取得OS shell和原umask,经同UID子进程继承交付
|
|
|
62
66
|
|
|
63
67
|
本包提供N端点与管理能力;两品牌原生工具改道、每RPC撤回、自动工具闭包、真实GUI/TCC/首次员工/leo及Intel实机须各自验收,不能以组件/开发桥验证代替。断线或缺命令完成确认保留UNKNOWN,不自动重连或重放文件/命令。mini原支持节点仍保留;禁止另起同身份N或把它用作D20实验。
|
|
64
68
|
|
|
65
|
-
管理入口:官方「插件 → 远程主机网络 → ai-workbench-network
|
|
69
|
+
管理入口:官方「插件 → 远程主机网络 → ai-workbench-network」组件详情页只读显示三项恢复参数当前Config值、未指定状态及默认/范围/后果。表单输入、保存按钮、POST保存/自定义重载及revision/CAS已删除;值仍可经官方 Config 修改。同包settings伴生行只读公开配置和当前状态,缺服务明确说明,核心联网不依赖本页。未在N指定的值显示由开通方提供及默认值,不把默认数值冒称为实际运行值。HanaMesh管理入口须另验。
|
|
66
70
|
|
|
67
71
|
员工具体原因投影:L既有断线提示会泛化Need,N自己的同一官方设置页增加“读取时状态”,显示恢复/耗尽的固定Need code/message/needs;不投原始cause/Error,只从可信退出记录格式提取实际exitCode/signal,缺证据明确未知。页面标采样UTC、重新打开可取最新,不冒充实时监控。折叠技术详情只含L公开instanceId/epoch/phase,供只读复核,无控制或重放操作。
|
|
72
|
+
|
|
73
|
+
缓存身份恢复时,若原journal已绑定且原配置仍含authKeyFile,当前组件虽只使用持久身份、未向tsnet再次交AuthKey,消费回执仍标session:key,直到原配置/材料清理完成后走既有串行identityOnly交接。新身份组件读取清理后配置的SHA,后续恢复核对该SHA;未绑定材料的身份组件不据此清理或交接。
|
|
74
|
+
|
|
75
|
+
旧probe-packed-enrollment/probe-network-faults探针已退役:其未知结果锁存、固定二进制路径及同PID恢复断言不适用0.4.9。历史证据保留;本轮以受控Node/Go回归提供工程证明,不运行真实入网或主机中断实验。
|
|
76
|
+
|
|
77
|
+
待交接的缓存身份遇CONTROL时保留当前失败和原watch/刷新,未在线快照不能清错;活组件不被恢复次数耗尽隔离。该组件已退出时按既有恢复策略仅用身份重建、核原SHA。若清理已完成但启动随后失败,则已有合法清理回执仍触发原身份交接,重新读取清理后的SHA,不把H0误用于H1。身份拒绝/权限错误仍不被迟到CONTROL复活。
|
|
78
|
+
|
|
79
|
+
若原配置H0已去key写成H1,而complete收据尚未到达就退出,恢复只在可信后端内存核对原读取快照:原authKeyFile必须非空、当前必须完全无该属性,其余JSON字段须递归相同;仅额外容许Go既有omitempty省略空proxyListen、零httpMaxResponseHeaderBytes、空host.knownHostsFile,不能改值。只改空白但保留key、把key设空串/null、改变路径/身份/主机/信任/策略均拒绝。该精确去key变化允许以实际H1的SHA传给Go;快照来自与H0同次原字节读取、在运行策略覆盖前保存,getter深复制,不进argv/HTTP/renderer/日志。Go仍核当前读取字节SHA(原核对位置在node锁前),锁内构造继续验证同一配置、有journal时的原/sanitized摘要及已确认NodeID;无journal只复用有效身份,不读取/清理未绑定材料。不声称锁内重读raw SHA或证明每次删除均由N完成。
|
package/client/index.cjs
CHANGED
|
@@ -1,54 +1,36 @@
|
|
|
1
1
|
window.__ModuleLoader__.load({id:'@ai-workbench/network',factory:(require)=>{
|
|
2
2
|
const React=require('react'),h=React.createElement;
|
|
3
3
|
const definitions=[
|
|
4
|
-
['helperRecoveryMaxAttempts','自动恢复次数','默认3次,0
|
|
4
|
+
['helperRecoveryMaxAttempts','自动恢复次数','默认3次,0关闭自动恢复。次数为非负安全整数。增大允许更多连续恢复;减小更早停止。耗尽后停止并显示原因;网络恢复后可关闭再启用网络组件,正常重载后发送下一轮。'],
|
|
5
5
|
['helperRecoveryDelaySeconds','恢复间隔(秒)','默认1秒,范围0.001至2147483.647。增大减少频繁启动但恢复更慢;减小增加连续启动开销。无关闭值,关闭恢复请将次数设为0。'],
|
|
6
6
|
['helperRecoveryStableSeconds','计数清零所需连续在线时间(秒)','默认60秒,范围0.001至2147483.647。增大更严格识别反复崩溃;减小可能把短暂在线当成稳定。无关闭值,刚收到就绪不会清零。'],
|
|
7
7
|
];
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
const response=await fetch('/ai-workbench-network/policy'+(params?'?'+params:''),{method:params?'POST':'GET',credentials:'same-origin',signal});
|
|
8
|
+
async function request(signal){
|
|
9
|
+
const response=await fetch('/ai-workbench-network/policy',{method:'GET',credentials:'same-origin',signal});
|
|
11
10
|
const result=await response.json();
|
|
12
|
-
if(!response.ok||!result.ok)throw new Error(
|
|
11
|
+
if(!response.ok||!result.ok)throw new Error('恢复设置读取失败。');
|
|
13
12
|
return result.value;
|
|
14
13
|
}
|
|
15
14
|
function RecoveryPolicyForm(){
|
|
16
|
-
const [snapshot,setSnapshot]=React.useState(null),[
|
|
17
|
-
const
|
|
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
|
-
}
|
|
15
|
+
const [snapshot,setSnapshot]=React.useState(null),[error,setError]=React.useState('');
|
|
16
|
+
React.useEffect(()=>{const controller=new AbortController();request(controller.signal).then(value=>{if(!controller.signal.aborted)setSnapshot(value);},()=>{if(!controller.signal.aborted)setError('我需要官方客户端的网络设置服务才能显示本页。请在插件管理查看组件错误。');});return ()=>controller.abort();},[]);
|
|
33
17
|
return h('section',{'aria-label':'网络组件自动恢复设置',style:{maxWidth:'780px',padding:'16px'}},
|
|
34
18
|
h('h3',null,'网络组件自动恢复'),
|
|
35
19
|
snapshot?.runtime?h('div',{'aria-label':'读取时组件状态'},
|
|
36
20
|
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.
|
|
38
|
-
snapshot.runtime.connection?h('details',null,h('summary',null,'连接技术详情'),h('pre',null,JSON.stringify(snapshot.runtime.connection,null,2))):null
|
|
21
|
+
snapshot.runtime.need?h('div',{role:'alert'},h('p',null,snapshot.runtime.need.message),h('p',null,snapshot.runtime.need.needs)):null,
|
|
22
|
+
snapshot.runtime.need||snapshot.runtime.connection?h('details',null,h('summary',null,'连接技术详情'),snapshot.runtime.need?h('p',null,snapshot.runtime.need.code+';'+(snapshot.runtime.need.exit?'退出码:'+(snapshot.runtime.need.exit.exitCode??'无(信号退出)')+';退出信号:'+(snapshot.runtime.need.exit.signal??'无(组件自行退出)'):'退出码及信号:未知')):null,snapshot.runtime.connection?h('pre',null,JSON.stringify(snapshot.runtime.connection,null,2)):null):null
|
|
39
23
|
):null,
|
|
40
|
-
h('p',null,'
|
|
41
|
-
h('p',null,'
|
|
42
|
-
snapshot
|
|
24
|
+
h('p',null,'自有网络组件退出或控制通道中断时,按原身份恢复连接,不重交密钥。上一轮不会自动重发。'),
|
|
25
|
+
h('p',null,'本页只读。请在官方 Config 修改这三项;修改后按官方流程正常重载,恢复连接后发起新操作,旧请求不会重放。'),
|
|
26
|
+
snapshot?definitions.map(([key,title,description])=>h('fieldset',{key,style:{margin:'12px 0',padding:'10px',border:'1px solid #8886',borderRadius:'6px'}},
|
|
43
27
|
h('legend',null,title),h('code',null,key),h('p',{style:{margin:'8px 0',lineHeight:1.5}},description),
|
|
44
|
-
h('
|
|
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}})})
|
|
28
|
+
h('p',null,'当前值:'+(snapshot.fields[key].explicit?String(snapshot.fields[key].value):'未在 N 官方 Config 指定(由开通方提供;开通方未设时默认'+snapshot.fields[key].defaultValue+')'))
|
|
46
29
|
)):h('p',null,error?'设置不可用':'正在读取恢复设置…'),
|
|
47
|
-
error?h('p',{role:'alert',style:{color:'#df6659'}},error):null
|
|
48
|
-
|
|
49
|
-
snapshot?h('button',{type:'button',disabled:busy,onClick:save,style:controlStyle},busy?'正在保存并正常重载…':'保存并正常重载网络组件'):null);
|
|
30
|
+
error?h('p',{role:'alert',style:{color:'#df6659'}},error):null);
|
|
31
|
+
|
|
50
32
|
}
|
|
51
33
|
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'?'
|
|
34
|
+
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
35
|
}};
|
|
54
36
|
}});
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# 依赖与来源(N 第 1 轮)
|
|
2
2
|
|
|
3
|
-
N2 helper 已接入固定 tsnet 并可通过 Makefile
|
|
3
|
+
N2 helper 已接入固定 tsnet 并可通过 Makefile 构建;未部署、未发包。历史认证HTTP/CONNECT代理及Electron44组件诊断已归档,0.4.9已删除该产品能力;DSH插件0.3.0及新消费者Cordis组件诊断已交。真实证据见 N 回执。
|
|
4
4
|
|
|
5
5
|
| 组件 | 固定版本/来源提交 | 协议 | 用途及当前证据 |
|
|
6
6
|
|---|---|---|---|
|
|
7
7
|
| tailscale.com/tsnet | v1.104.1 / `9a522a9786c97eb7910c01ccb7bd66557b04c910` | BSD-3-Clause | [上游源码](https://github.com/tailscale/tailscale/tree/v1.104.1/tsnet),实际 helper 依赖;完整闭包固定于 go.mod/go.sum |
|
|
8
|
-
| tailscaled HTTP proxy | 同上,`cmd/tailscaled/proxy.go` | BSD-3-Clause | [CONNECT 处理源码](https://github.com/tailscale/tailscale/blob/v1.104.1/cmd/tailscaled/proxy.go),只读参考;本实现使用net/http.ReverseProxy及SSH direct-tcpip,未复制该文件 |
|
|
9
8
|
| DeepSeek Harness | 0.2.0-rc.2 / `639ed015397290b3745d163aafe02ffee4aa3f84` | MIT(已复核固定源码package.json),不复制核心代码 | [官方上游](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.2.0-rc.2),只读参考;实现只在本仓,浅克隆验证完成后精确清理 |
|
|
10
9
|
| Go | 构建固定1.27.3;目标tsnet要求至少1.27.1 | BSD-3-Clause | `go mod download tailscale.com@v1.104.1` 自动选择 1.27.3。已用自动选择的1.27.3通过普通/race/vet/build |
|
|
11
10
|
|
|
@@ -15,7 +14,7 @@ N2 helper 已接入固定 tsnet 并可通过 Makefile 构建;未部署、未
|
|
|
15
14
|
|
|
16
15
|
N3 SSH使用 golang.org/x/crypto/ssh v0.57.0(BSD-3-Clause;实际锁定版本见go.mod);只使用none认证与可信known_hosts。Electron44.0.0官方darwin-arm64 ZIP为诊断工具,MIT及其随附第三方许可;SHA256与引擎版本见 `docs/evidence/n3-2026-10-20/electron-artifact.json`。独立进程/内存session,没有复制进发布包。
|
|
17
16
|
|
|
18
|
-
N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MIT,来源为固定DSH提交的vendor目录及官方npm包。TS5.9.3/Node24类型是构建依赖;实际闭包固定package-lock.json
|
|
17
|
+
N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MIT,来源为固定DSH提交的vendor目录及官方npm包。TS5.9.3/Node24类型是构建依赖;实际闭包固定package-lock.json,新消费者闭包另存证据。0.4.9删除主机下载与登录代理实现,不再提供相关能力。源码/API升级需要在自有仓库适配,不提上游PR/issue或等待上游修改。
|
|
19
18
|
|
|
20
19
|
0.4.3分发说明:唯一发布单元为network主包;helpers/darwin-arm64与helpers/darwin-x64的package.json是private内嵌身份元数据,不是独立发布依赖。每目标的helper-manifest.json固定二进制SHA,THIRD_PARTY_NOTICES.txt保留该目标Go实际编译依赖的许可原文,licenses-manifest.json列出模块、编译包、许可来源文件与SHA。构建来源为本仓cmd/aw-net及internal、固定go.mod/go.sum,Go1.27.3、CGO_ENABLED=0、ts_omit_logtail。
|
|
21
20
|
|
|
@@ -29,3 +28,5 @@ D20(0.4.4)实际端点复用:x/crypto/ssh v0.57.0(BSD-3-Clause),crea
|
|
|
29
28
|
0.4.8 登记连接观测:沿原 register 拨号上下文保留独立端点观察器,共享 SSH channel 关联同一条 TCP。白名单新增可空 RTT 初始化、SRTT/RTTVar/RTO 和恢复状态;端点锁→RTT锁沿原 getTCPInfo 顺序,保留标准 LockUser/UnlockUser(后者可调度原已排队处理,不宣称绝对零调度影响)。没有更改发送器算法、连接期限、重试、探测、取消或身份规则。新增私有 socket 只读 GET 与既有生命周期事件日志;不记录载荷、命令、环境或凭据。没有新增运行时模块或设置,来源/补丁 SHA 随交付更新;不依赖上游修改。
|
|
30
29
|
|
|
31
30
|
同轮 tailscale.com v1.104.1(BSD-3-Clause)固定源码复制至 third_party/tailscale,只新增 magicsock/observation_aw.go 的静态字段复制出口及其测试,不改上游原文件。它避免 UpdateStatus 的 WireGuard-only 特殊分支选择/延长直连信任;原模块/档案 SHA、逐文件来源和新增补丁见该目录 UPSTREAM.json/OBSERVATION.patch。两目标构建引用此本仓模块,原许可照常进入实际编译闭包。
|
|
31
|
+
|
|
32
|
+
0.4.9 修复:控制通道不可用且已有可恢复身份时沿原监督恢复网络组件;员工端点历史关闭失败仅作诊断,不再阻止下一轮重新准备。恢复提示与原设置说明同步;新增设置及运行依赖均为零,不改连接超时或应用重试;按owner追加删除诊断失败闸门/主机下载/登录代理,开通未知先查持久身份、无身份时原密钥仅补救一次,记录预算沿原attempted-v1持久保存。
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"platform":"darwin","arch":"arm64","sha256":"
|
|
1
|
+
{"platform":"darwin","arch":"arm64","sha256":"23c7d763071c47d5c4c9dc82569a972455f8a534675adf248acecc49976307be"}
|
|
@@ -1070,10 +1070,6 @@
|
|
|
1070
1070
|
"module": "Go go1.27.3",
|
|
1071
1071
|
"package": "src/net/http"
|
|
1072
1072
|
},
|
|
1073
|
-
{
|
|
1074
|
-
"module": "Go go1.27.3",
|
|
1075
|
-
"package": "src/net/http/httputil"
|
|
1076
|
-
},
|
|
1077
1073
|
{
|
|
1078
1074
|
"module": "Go go1.27.3",
|
|
1079
1075
|
"package": "src/hash/maphash"
|
|
@@ -2030,6 +2026,10 @@
|
|
|
2030
2026
|
"module": "github.com/pires/go-proxyproto@v0.15.0",
|
|
2031
2027
|
"package": ""
|
|
2032
2028
|
},
|
|
2029
|
+
{
|
|
2030
|
+
"module": "Go go1.27.3",
|
|
2031
|
+
"package": "src/net/http/httputil"
|
|
2032
|
+
},
|
|
2033
2033
|
{
|
|
2034
2034
|
"module": "tailscale.com@v1.104.1",
|
|
2035
2035
|
"package": "appc"
|
|
@@ -1,11 +1,10 @@
|
|
|
1
1
|
# 依赖与来源(N 第 1 轮)
|
|
2
2
|
|
|
3
|
-
N2 helper 已接入固定 tsnet 并可通过 Makefile
|
|
3
|
+
N2 helper 已接入固定 tsnet 并可通过 Makefile 构建;未部署、未发包。历史认证HTTP/CONNECT代理及Electron44组件诊断已归档,0.4.9已删除该产品能力;DSH插件0.3.0及新消费者Cordis组件诊断已交。真实证据见 N 回执。
|
|
4
4
|
|
|
5
5
|
| 组件 | 固定版本/来源提交 | 协议 | 用途及当前证据 |
|
|
6
6
|
|---|---|---|---|
|
|
7
7
|
| tailscale.com/tsnet | v1.104.1 / `9a522a9786c97eb7910c01ccb7bd66557b04c910` | BSD-3-Clause | [上游源码](https://github.com/tailscale/tailscale/tree/v1.104.1/tsnet),实际 helper 依赖;完整闭包固定于 go.mod/go.sum |
|
|
8
|
-
| tailscaled HTTP proxy | 同上,`cmd/tailscaled/proxy.go` | BSD-3-Clause | [CONNECT 处理源码](https://github.com/tailscale/tailscale/blob/v1.104.1/cmd/tailscaled/proxy.go),只读参考;本实现使用net/http.ReverseProxy及SSH direct-tcpip,未复制该文件 |
|
|
9
8
|
| DeepSeek Harness | 0.2.0-rc.2 / `639ed015397290b3745d163aafe02ffee4aa3f84` | MIT(已复核固定源码package.json),不复制核心代码 | [官方上游](https://github.com/deepseek-ai/deepseek-harness/tree/dsh-v0.2.0-rc.2),只读参考;实现只在本仓,浅克隆验证完成后精确清理 |
|
|
10
9
|
| Go | 构建固定1.27.3;目标tsnet要求至少1.27.1 | BSD-3-Clause | `go mod download tailscale.com@v1.104.1` 自动选择 1.27.3。已用自动选择的1.27.3通过普通/race/vet/build |
|
|
11
10
|
|
|
@@ -15,7 +14,7 @@ N2 helper 已接入固定 tsnet 并可通过 Makefile 构建;未部署、未
|
|
|
15
14
|
|
|
16
15
|
N3 SSH使用 golang.org/x/crypto/ssh v0.57.0(BSD-3-Clause;实际锁定版本见go.mod);只使用none认证与可信known_hosts。Electron44.0.0官方darwin-arm64 ZIP为诊断工具,MIT及其随附第三方许可;SHA256与引擎版本见 `docs/evidence/n3-2026-10-20/electron-artifact.json`。独立进程/内存session,没有复制进发布包。
|
|
17
16
|
|
|
18
|
-
N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MIT,来源为固定DSH提交的vendor目录及官方npm包。TS5.9.3/Node24类型是构建依赖;实际闭包固定package-lock.json
|
|
17
|
+
N4插件peer:@deepseek-ai/cordis4.0.4、@deepseek-ai/schemastery3.18.4,均MIT,来源为固定DSH提交的vendor目录及官方npm包。TS5.9.3/Node24类型是构建依赖;实际闭包固定package-lock.json,新消费者闭包另存证据。0.4.9删除主机下载与登录代理实现,不再提供相关能力。源码/API升级需要在自有仓库适配,不提上游PR/issue或等待上游修改。
|
|
19
18
|
|
|
20
19
|
0.4.3分发说明:唯一发布单元为network主包;helpers/darwin-arm64与helpers/darwin-x64的package.json是private内嵌身份元数据,不是独立发布依赖。每目标的helper-manifest.json固定二进制SHA,THIRD_PARTY_NOTICES.txt保留该目标Go实际编译依赖的许可原文,licenses-manifest.json列出模块、编译包、许可来源文件与SHA。构建来源为本仓cmd/aw-net及internal、固定go.mod/go.sum,Go1.27.3、CGO_ENABLED=0、ts_omit_logtail。
|
|
21
20
|
|
|
@@ -29,3 +28,5 @@ D20(0.4.4)实际端点复用:x/crypto/ssh v0.57.0(BSD-3-Clause),crea
|
|
|
29
28
|
0.4.8 登记连接观测:沿原 register 拨号上下文保留独立端点观察器,共享 SSH channel 关联同一条 TCP。白名单新增可空 RTT 初始化、SRTT/RTTVar/RTO 和恢复状态;端点锁→RTT锁沿原 getTCPInfo 顺序,保留标准 LockUser/UnlockUser(后者可调度原已排队处理,不宣称绝对零调度影响)。没有更改发送器算法、连接期限、重试、探测、取消或身份规则。新增私有 socket 只读 GET 与既有生命周期事件日志;不记录载荷、命令、环境或凭据。没有新增运行时模块或设置,来源/补丁 SHA 随交付更新;不依赖上游修改。
|
|
30
29
|
|
|
31
30
|
同轮 tailscale.com v1.104.1(BSD-3-Clause)固定源码复制至 third_party/tailscale,只新增 magicsock/observation_aw.go 的静态字段复制出口及其测试,不改上游原文件。它避免 UpdateStatus 的 WireGuard-only 特殊分支选择/延长直连信任;原模块/档案 SHA、逐文件来源和新增补丁见该目录 UPSTREAM.json/OBSERVATION.patch。两目标构建引用此本仓模块,原许可照常进入实际编译闭包。
|
|
31
|
+
|
|
32
|
+
0.4.9 修复:控制通道不可用且已有可恢复身份时沿原监督恢复网络组件;员工端点历史关闭失败仅作诊断,不再阻止下一轮重新准备。恢复提示与原设置说明同步;新增设置及运行依赖均为零,不改连接超时或应用重试;按owner追加删除诊断失败闸门/主机下载/登录代理,开通未知先查持久身份、无身份时原密钥仅补救一次,记录预算沿原attempted-v1持久保存。
|
|
Binary file
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"platform":"darwin","arch":"x64","sha256":"
|
|
1
|
+
{"platform":"darwin","arch":"x64","sha256":"9b88281eac844fbce9f2a09fc0bd9a1b7a4f06e24d7740d9f74b294075661f2a"}
|
|
@@ -1066,10 +1066,6 @@
|
|
|
1066
1066
|
"module": "Go go1.27.3",
|
|
1067
1067
|
"package": "src/net/http"
|
|
1068
1068
|
},
|
|
1069
|
-
{
|
|
1070
|
-
"module": "Go go1.27.3",
|
|
1071
|
-
"package": "src/net/http/httputil"
|
|
1072
|
-
},
|
|
1073
1069
|
{
|
|
1074
1070
|
"module": "Go go1.27.3",
|
|
1075
1071
|
"package": "src/hash/maphash"
|
|
@@ -2038,6 +2034,10 @@
|
|
|
2038
2034
|
"module": "github.com/pires/go-proxyproto@v0.15.0",
|
|
2039
2035
|
"package": ""
|
|
2040
2036
|
},
|
|
2037
|
+
{
|
|
2038
|
+
"module": "Go go1.27.3",
|
|
2039
|
+
"package": "src/net/http/httputil"
|
|
2040
|
+
},
|
|
2041
2041
|
{
|
|
2042
2042
|
"module": "tailscale.com@v1.104.1",
|
|
2043
2043
|
"package": "appc"
|
package/lib/activation.js
CHANGED
|
@@ -5,14 +5,18 @@ import { fail, ok } from "./api.js";
|
|
|
5
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
|
-
if (!s || typeof s !== 'object'
|
|
8
|
+
if (!s || typeof s !== 'object')
|
|
9
9
|
return;
|
|
10
|
-
|
|
10
|
+
// 旧开通方曾传入的退役设置只在边界丢弃,不影响原身份配置或提供旧能力。
|
|
11
|
+
s = Object.fromEntries(Object.entries(s).filter(([key]) => !['downloadMaxRedirects', 'httpMaxResponseHeaderBytes'].includes(key)));
|
|
12
|
+
if (Object.keys(s).some(k => !fields.includes(k)))
|
|
13
|
+
return;
|
|
14
|
+
if (typeof s.configFile !== 'string' || !isAbsolute(s.configFile))
|
|
11
15
|
return;
|
|
12
16
|
for (const v of [s.startupObservationSeconds, s.shutdownObservationSeconds])
|
|
13
17
|
if (typeof v !== 'number' || !Number.isFinite(v) || v < 0.001 || v * 1000 > 2147483647)
|
|
14
18
|
return;
|
|
15
|
-
if (!Number.isSafeInteger(s.controlResponseMaxBytes) || s.controlResponseMaxBytes <= 0
|
|
19
|
+
if (!Number.isSafeInteger(s.controlResponseMaxBytes) || s.controlResponseMaxBytes <= 0)
|
|
16
20
|
return;
|
|
17
21
|
if (!validHelperPolicy(s) || !validRecoveryPolicy(s))
|
|
18
22
|
return;
|
|
@@ -24,8 +28,8 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
24
28
|
let activation = { instanceId: randomUUID(), revision: 0, phase: 'unconfigured', receipt: null, error: network.error };
|
|
25
29
|
let runtime, selected, disposed = false, disposing, handoff, generation = 0, handedOff = false, consumerOnline = false;
|
|
26
30
|
let recoveryTimer, stableTimer, recovering;
|
|
27
|
-
let recoveryAttempts = 0, recoverAgain = false, handledFailure, lastFailure, policy = recoveryPolicy(initial);
|
|
28
|
-
const
|
|
31
|
+
let recoveryAttempts = 0, recoverAgain = false, enrollmentRechecked = false, handledFailure, lastFailure, policy = recoveryPolicy(initial);
|
|
32
|
+
const recoverableFailures = new Set(['HELPER_EXITED', 'HELPER_PROCESS_FAILED', 'HELPER_START_FAILED', 'HELPER_STARTUP_EXPIRED', 'CONTROL_UNAVAILABLE']);
|
|
29
33
|
const listeners = new Set(), edges = new Set(), updates = new Set();
|
|
30
34
|
const snapshot = () => structuredClone(activation);
|
|
31
35
|
const employee = createEmployeeEndpointController({ getConnect, getRuntime: () => activation.phase === 'active' ? runtime : undefined, getState: () => network, connect: (target, signal) => api.connect(target, signal) });
|
|
@@ -49,11 +53,14 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
49
53
|
if (disposed)
|
|
50
54
|
return;
|
|
51
55
|
const receipt = next.status?.receipt ?? activation.receipt;
|
|
52
|
-
const
|
|
53
|
-
|
|
56
|
+
const current = receipt;
|
|
57
|
+
if (canRecheckEnrollment(next, current) && !enrollmentRechecked && runtime && !handoff && !recovering && !recoveryTimer) {
|
|
58
|
+
recheckEnrollment();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
54
61
|
const operational = !next.error || ['NETWORK_INTERRUPTED', 'HOST_OFFLINE', 'HOST_NOT_VISIBLE'].includes(next.error.code);
|
|
55
62
|
const clean = current?.identity === 'resumable' && (current.cleanup === 'complete' || current.cleanup === 'not-required');
|
|
56
|
-
if (clean && current?.session === 'key' && operational) {
|
|
63
|
+
if (clean && current?.session === 'key' && (operational || !!next.error && recoverableFailures.has(next.error.code))) {
|
|
57
64
|
if (handedOff) {
|
|
58
65
|
observed({ phase: 'blocked', error: { code: 'CONTROL_PROTOCOL_INVALID', message: '身份恢复组件错误地保留密钥。', needs: '管理员核对组件版本', actor: 'network' } });
|
|
59
66
|
return;
|
|
@@ -109,16 +116,48 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
109
116
|
clearTimeout(stableTimer);
|
|
110
117
|
stableTimer = undefined;
|
|
111
118
|
}
|
|
112
|
-
const
|
|
113
|
-
|
|
119
|
+
const pendingIdentity = current?.identity === 'resumable' && current.session === 'key' && current.cleanup === 'pending' && (runtime?.isRunning?.() === false || !!next.error && next.error.code !== 'CONTROL_UNAVAILABLE');
|
|
120
|
+
const resumable = pendingIdentity || current?.identity === 'resumable' && (current.cleanup === 'complete' || current.cleanup === 'not-required') && (current.session === 'identity' || handedOff);
|
|
121
|
+
if (next.error && recoverableFailures.has(next.error.code) && resumable && runtime && handledFailure !== runtime) {
|
|
114
122
|
handledFailure = runtime;
|
|
115
123
|
lastFailure = next.error;
|
|
124
|
+
generation++;
|
|
116
125
|
if (recovering || recoveryTimer)
|
|
117
126
|
recoverAgain = true;
|
|
118
127
|
else
|
|
119
128
|
scheduleRecovery(next.error);
|
|
120
129
|
}
|
|
121
130
|
}
|
|
131
|
+
function canRecheckEnrollment(next, receipt = activation.receipt) {
|
|
132
|
+
return !!next.error && recoverableFailures.has(next.error.code) && receipt?.session === 'key' && receipt.identity === 'unconfirmed' && receipt.cleanup === 'pending' && ['attempted', 'unknown'].includes(receipt.key);
|
|
133
|
+
}
|
|
134
|
+
function recheckEnrollment() {
|
|
135
|
+
const owned = runtime, detail = network.error;
|
|
136
|
+
enrollmentRechecked = true;
|
|
137
|
+
handledFailure = owned;
|
|
138
|
+
generation++;
|
|
139
|
+
// 一次开通补救独立于已有身份恢复次数;Go先查身份并持久限制原密钥交接次数。
|
|
140
|
+
recovering = Promise.resolve().then(async () => {
|
|
141
|
+
await owned.dispose();
|
|
142
|
+
if (disposed)
|
|
143
|
+
return;
|
|
144
|
+
const error = owned.getState().error;
|
|
145
|
+
if (error) {
|
|
146
|
+
observed({ phase: 'blocked', error });
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
install(JSON.parse(selected), false, owned.getRecoveryConfigSHA256?.(), owned.getRecoveryConfigSnapshot?.());
|
|
150
|
+
await launch();
|
|
151
|
+
}).catch(() => recoveryState({ code: 'HELPER_RECOVERY_FAILED', message: '网络连接恢复未完成,自动恢复已停止。', needs: '需要旧连接结束、网络重新就绪。请确认网络恢复后,在插件设置中关闭再启用网络组件,然后发送下一轮;上一轮不会自动重发。', actor: 'network', cause: detail?.code })).finally(() => {
|
|
152
|
+
recovering = undefined;
|
|
153
|
+
if (!disposed && recoverAgain) {
|
|
154
|
+
recoverAgain = false;
|
|
155
|
+
scheduleRecovery(lastFailure);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
const status = network.status ? { ...network.status, enrollment: 'starting', error: null, host: { ...network.status.host, online: null } } : undefined;
|
|
159
|
+
observed({ phase: 'starting', ...status ? { status } : {} });
|
|
160
|
+
}
|
|
122
161
|
function recoveryState(error) {
|
|
123
162
|
const status = network.status ? { ...network.status, enrollment: 'error', host: { ...network.status.host, online: null, observedAt: null }, error } : undefined;
|
|
124
163
|
observed({ phase: error.code === 'HELPER_RECOVERING' ? 'starting' : 'blocked', ...status ? { status } : {}, error });
|
|
@@ -126,19 +165,18 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
126
165
|
function scheduleRecovery(cause) {
|
|
127
166
|
if (disposed)
|
|
128
167
|
return;
|
|
129
|
-
const detail = [cause.code, cause.message, cause.cause].filter(Boolean).join(';');
|
|
168
|
+
const detail = [cause.code, cause.message, cause.cause, `helperRecoveryMaxAttempts=${policy.helperRecoveryMaxAttempts};helperRecoveryDelaySeconds=${policy.helperRecoveryDelaySeconds};helperRecoveryStableSeconds=${policy.helperRecoveryStableSeconds}`].filter(Boolean).join(';');
|
|
130
169
|
if (recoveryAttempts >= policy.helperRecoveryMaxAttempts) {
|
|
131
|
-
recoveryState({ code: policy.helperRecoveryMaxAttempts === 0 ? 'HELPER_RECOVERY_DISABLED' : 'HELPER_RECOVERY_EXHAUSTED', message: policy.helperRecoveryMaxAttempts === 0 ? '
|
|
170
|
+
recoveryState({ code: policy.helperRecoveryMaxAttempts === 0 ? 'HELPER_RECOVERY_DISABLED' : 'HELPER_RECOVERY_EXHAUSTED', message: policy.helperRecoveryMaxAttempts === 0 ? '网络连接已中断,自动恢复已关闭。' : `网络连接连续恢复${recoveryAttempts}次仍未稳定,自动恢复已停止。`, needs: policy.helperRecoveryMaxAttempts === 0 ? '请确认网络恢复后,在插件设置中开启自动恢复,或关闭再启用网络组件,然后发送下一轮;上一轮不会自动重发。' : '请等待网络恢复后,在插件设置中关闭再启用网络组件,然后发送下一轮;上一轮不会自动重发。', actor: 'network', cause: detail });
|
|
132
171
|
return;
|
|
133
172
|
}
|
|
134
173
|
recoveryAttempts++;
|
|
135
|
-
recoveryState({ code: 'HELPER_RECOVERING', message:
|
|
174
|
+
recoveryState({ code: 'HELPER_RECOVERING', message: `网络连接已中断,正在第${recoveryAttempts}/${policy.helperRecoveryMaxAttempts}次恢复。`, needs: `系统将在${policy.helperRecoveryDelaySeconds}秒后按原身份重新连接。请等待网络就绪后发送下一轮;上一轮不会自动重发。`, actor: 'network', cause: detail });
|
|
136
175
|
if (disposed)
|
|
137
176
|
return;
|
|
138
177
|
recoveryTimer = setTimeout(() => {
|
|
139
178
|
recoveryTimer = undefined;
|
|
140
179
|
const owned = runtime;
|
|
141
|
-
generation++;
|
|
142
180
|
recovering = Promise.resolve().then(async () => {
|
|
143
181
|
await owned.dispose();
|
|
144
182
|
if (disposed)
|
|
@@ -148,9 +186,9 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
148
186
|
recoveryState({ ...error, cause: [error.cause, detail].filter(Boolean).join(';') });
|
|
149
187
|
return;
|
|
150
188
|
}
|
|
151
|
-
install(JSON.parse(selected), true, owned.getRecoveryConfigSHA256?.());
|
|
189
|
+
install(JSON.parse(selected), true, owned.getRecoveryConfigSHA256?.(), owned.getRecoveryConfigSnapshot?.());
|
|
152
190
|
await launch();
|
|
153
|
-
}).catch(() => recoveryState({ code: 'HELPER_RECOVERY_FAILED', message: '
|
|
191
|
+
}).catch(() => recoveryState({ code: 'HELPER_RECOVERY_FAILED', message: '网络连接恢复未完成,自动恢复已停止。', needs: '需要旧连接结束、网络重新就绪。请确认网络恢复后,在插件设置中关闭再启用网络组件,然后发送下一轮;上一轮不会自动重发。', actor: 'network', cause: detail })).finally(() => {
|
|
154
192
|
recovering = undefined;
|
|
155
193
|
if (!disposed && recoverAgain) {
|
|
156
194
|
recoverAgain = false;
|
|
@@ -160,10 +198,10 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
160
198
|
}, policy.helperRecoveryDelaySeconds * 1000);
|
|
161
199
|
recoveryTimer.unref?.();
|
|
162
200
|
}
|
|
163
|
-
function install(config, resumeIdentityOnly = false, expectedConfigSHA256) {
|
|
201
|
+
function install(config, resumeIdentityOnly = false, expectedConfigSHA256, expectedConfigSnapshot) {
|
|
164
202
|
const ownGeneration = ++generation;
|
|
165
203
|
runtime = factory(config, executable, next => { if (ownGeneration === generation)
|
|
166
|
-
observed(next); }, undefined, resumeIdentityOnly, expectedConfigSHA256);
|
|
204
|
+
observed(next); }, undefined, resumeIdentityOnly, expectedConfigSHA256, expectedConfigSnapshot);
|
|
167
205
|
}
|
|
168
206
|
function launch() {
|
|
169
207
|
const owned = runtime, ownGeneration = generation;
|
|
@@ -192,6 +230,13 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
192
230
|
const key = JSON.stringify(config);
|
|
193
231
|
if (selected !== undefined && selected !== key)
|
|
194
232
|
return fail('ACTIVATION_CONFLICT', '已有连接激活,不能替换身份。', '管理员核对当前开通上下文');
|
|
233
|
+
if (runtime && activation.phase === 'blocked' && canRecheckEnrollment(network) && recovering) {
|
|
234
|
+
const owned = recovering;
|
|
235
|
+
await new Promise(resolve => { const done = () => { signal.removeEventListener('abort', done); resolve(); }; signal.addEventListener('abort', done, { once: true }); void owned.then(done, done); if (signal.aborted)
|
|
236
|
+
done(); });
|
|
237
|
+
if (disposed || signal.aborted)
|
|
238
|
+
return fail('ACTIVATION_CANCELLED', '连接激活等待已取消。', '查询当前激活结果;不要重复提交密钥');
|
|
239
|
+
}
|
|
195
240
|
if (!runtime) {
|
|
196
241
|
selected = key;
|
|
197
242
|
policy = recoveryPolicy(config);
|
|
@@ -200,6 +245,9 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
200
245
|
if (!disposed)
|
|
201
246
|
launch();
|
|
202
247
|
}
|
|
248
|
+
else if (activation.phase === 'blocked' && canRecheckEnrollment(network) && runtime.isRunning?.() !== true && !recovering && !handoff && !recoveryTimer) {
|
|
249
|
+
recheckEnrollment();
|
|
250
|
+
}
|
|
203
251
|
await new Promise(resolve => {
|
|
204
252
|
const done = () => { if (!disposed && !signal.aborted && activation.phase === 'activating')
|
|
205
253
|
return; updates.delete(done); signal.removeEventListener('abort', done); resolve(); };
|
|
@@ -213,10 +261,6 @@ export function createActivationService(initial, executable, emit = () => { }, f
|
|
|
213
261
|
},
|
|
214
262
|
async connect(target, signal) { if (disposed)
|
|
215
263
|
return fail('DISPOSED', '网络服务已停止。', '重新加载插件'); return activation.phase === 'active' && runtime ? runtime.connect(target, signal) : { ok: false, error: network.error ?? { code: 'NETWORK_NOT_READY', message: '连接尚未就绪。', needs: '等待N身份交接完成', actor: 'network' } }; },
|
|
216
|
-
async openLoginProxy(signal) { if (disposed)
|
|
217
|
-
return fail('DISPOSED', '网络服务已停止。', '重新加载插件'); return activation.phase === 'active' && runtime ? runtime.openLoginProxy(signal) : { ok: false, error: network.error ?? { code: 'NETWORK_NOT_READY', message: '连接尚未就绪。', needs: '等待N身份交接完成', actor: 'network' } }; },
|
|
218
|
-
async download(source, signal) { if (disposed)
|
|
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' } }; },
|
|
220
264
|
dispose() { if (disposing)
|
|
221
265
|
return disposing; disposed = true; if (recoveryTimer)
|
|
222
266
|
clearTimeout(recoveryTimer); if (stableTimer)
|