# amo v3 批次构建报告(NEXT-ROUND-NOTES 第 12/15/16/17/20/21/22/23 条) - 基线:`aa2ee62`(开工时核对一致);本批 10 个 item-scoped 提交,HEAD 见文末清单。 - 施工口径:第 0 条 Go 单一真源——只改 `panel-go/`、`agent-go/`、共用前端 `public/`、安装脚本 `scripts/`;Node 实现零改动(`git diff --name-only aa2ee62..HEAD` 可核)。数据文件既有字段语义未改,新字段全部 `omitempty` 向后兼容。 - 验收环境:沙箱 linux/amd64(AMD EPYC 9D64)、Go 1.24.6、自编 sing-box v1.13.21 沙箱现成二进制。**未部署、未推 Gitea**;真机(路由器 A53)数字待部署后复测。 ## 逐项结果 ### 1. 第 12 条 核心版本显空修复 — ✅ 完成(`bb974e2`) - `agent-go/health.go`:探针 `singBoxVersion()` 在二进制版本为空/`unknown` 时兜底读安装记录;记录文件 mtime 纳入探针缓存键,补写记录能被及时感知。新增 `installedSingBoxVersion()`:去空白与可选 `v` 前缀、正则校验版本号形状,垃圾内容忽略。 - `agent-go/main.go`:config 新增 `SingBoxVersionFile`,默认 `/etc/nexusgate/sing-box/version`,env `NG_SINGBOX_VERSION_FILE` 可覆盖。 - `scripts/agent-singbox.sh`:新增 `write_version_record()`(写去 `v` 前缀的 `1.13.21`),在已安装早退、预编译安装、源码编译安装三条成功路径全部调用。 - agent 版本号保持 `0.6.8`(面板 AnyTLS 版本门禁按数字解析,铁律未动)。 - 测试:新增 `TestSingBoxVersionFallsBackToInstallRecord`(unknown→读记录、带戳二进制优先、垃圾记录忽略、v 前缀剥离)通过;脚本 `bash -n` 通过。现网四台补写记录属部署动作,不在本批。 ### 2. 第 15 条 凭据页签删绑定区块 — ✅ 完成(`8189a51`) - `public/app.js` `renderCredentialsPane`:删除按绑定逐行列表(节点名/状态/协议/Reality 公钥+复制),只留订阅令牌+共用凭据三件套。后端 `customerCredentialsView` 载荷、keyStore、订阅、下发数据未动。 - 第 19 条已被第 21 条取代作废:未按旧口径往共用凭据区加「用户名」行(socks 用户名自此在节点上)。 ### 3. 第 16 条 REALITY 目标扫描器 — ✅ 完成(`0ab31ae`) - 后端 `panel-go/realityscan.go`+接口 `POST /api/reality/scan`(RequireAdmin 之后):入参 `{input}` 或 `{targets:[...]}`。 - 输入解析:留空→内置候选池 14 个(与目标预设同源的 tesla/amd 等+ microsoft/google/nvidia/samsung/intel/cloudflare/bing/github/yahoo/mozilla,均为 443);支持域名/IP/host:port/IPv4 CIDR(展开上限 32 主机);显式输入超 32 目标报 400。 - 探测:真实 TLS 握手,并发上限 8、单探针超时 4 秒;返回 target/host/port/ok/reason/tlsVersion/alpn/keyExchange/keyExchangeOk/certName/latencyMs,顺序与输入一致。密钥交换判定:Go 1.24 的 `tls.ConnectionState` 无 CurveID 字段,故改为限定组握手实测——依次只提供 X25519MLKEM768、只提供 X25519、组合兜底,任一成功即记支持。探测带 `InsecureSkipVerify`(只判可达性与密钥交换能力,证书 SAN 仍回报展示;理由已注释在代码内)。 - 前端:预设旁「查找目标」按钮;扫描器为独立叠加层(DOM 元素,不走 openModal,不冲未保存的节点表单状态),带主控视角说明文案(「从主控所在网络发起探测……节点所在网络可能不同」);每行「使用」回填 Reality SNI/目标端口、预设切 custom、关层。样式 `.scan-table` 在 `public/styles.css`。 - 测试:`realityscan_test.go` 通过(池/输入/CIDR 解析、本地自签 TLS 服务器 loopback 实测 OK/TLS 1.3/密钥交换非空、死端口给 reason);冒烟实测见下。 ### 4. 第 17 条 节点页 Reality 公私钥 — ✅ 完成(`a9d9867`+测试修复 `4199a43`) - 新只读接口 `GET /api/nodes/:id/reality-keys`:取 keyStore 中该节点所属**服务器**的托管密钥对;**绝不调用生成逻辑**,无条目返回 null;非 Reality 节点 400、节点不存在 404。 - 前端:节点编辑弹窗(Reality 协议时)加展示区,公钥/私钥各一行带复制;无密钥对显示「尚未生成(首次部署时由主控生成)」。只显示,无「换一对」。 - 测试:`realitykeys_test.go` 通过;冒烟实测接口返回完整公私钥对、非 Reality 节点 400。 ### 5. 第 20+21 条 先建后绑+SOCKS5 固定账号 — ✅ 完成(`2fafd85`) - 先建后绑:`orchestrator.go` 删除 `ValidateNodeDeploy` 的零客户 400 校验,全部协议一致放开(socks 例外按第 21 条连带修订取消);删除 `RebuildNode` 零客户自动移除分支与对账路径的零客户 skip——绑客户归零只重建、不下架。 - 固定账号:`types.go` Node 新增 `socksUser`/`socksPassword`(omitempty;老文件可读、老二进制读新文件忽略新字段不崩)。`protocols.go` `EnsureSocksCredentials()` 生成:用户名 `ngs_`+10 位 hex——**刻意避开 `ng:` 前缀**,因 agent 侧 sing-box 日志解析用 `^ng:(.+)$` 把 user 计数映射为客户级 usage 样本,固定账号带该前缀会被误记到某客户头上;密码为 base64 32 字节随机(与 RelayPassword 同级)。 - 入站 users 恒为固定账号一组,与绑定数无关(0/1/2 客户均有测试覆盖)。 - 订阅:`subscriptions.go` `NodeClientURI` 对所有绑定客户携带节点固定账密;链接只在客户已绑定时出现,绑定只控制出不出现。 - 编辑表单:socks 协议时显示两字段(可改、带复制,含未保存值复制);表单提交空值=保留旧值(与 exitCredentials 合并语义一致);用户名校验拒绝 `ng:` 开头与空白;协议切离 socks 时清空两字段。新建留空保存后自动生成。 - 存量迁移:`store.go` `MigrateSocksNodeCredentials()` 在 Store.Init 与既有凭据收敛迁移合并落盘,幂等只填空缺。 - 记账:固定账号流量在 agent 侧不匹配任何客户 user 样本,走 inbound 样本记节点级 MeteredTraffic;入口侧按客户 UUID 分账逻辑未动。 - 测试:新增 `panel-go/socksfixed_test.go` 全过;订阅金标准夹具的 socks 节点改存固定账密对(取旧派生值使全部格式产物逐字不变,见金标准节)。 ### 6. 第 22 条 性能专项 — ✅ 代码完成,真机复测待部署(`05366d9`+`6d97d4e`+基准扩容 `0f74955`) 后端: - `types.go` `Clone()` 改反射深拷贝(指针/接口/切片/映射/结构体递归、[]byte 逐字拷贝、未导出字段经浅拷贝保留);原 JSON 往返保留为 `cloneViaJSON()` 供等价对照。`clone_equivalence_test.go` 在真实种子库与合成数据(含 json.Number 嵌套、keyStore、Extra)上证明两种克隆序列化逐字节相等且深独立、nil 安全。 - `handlers.go` `CurrentAdmin` 改走 `store.View` 只读命中单条 User 记录,不再为找用户整库 Snapshot——每个 admin GET 的全量克隆由至少两次降为一次(handler 自身的一次 Snapshot 保留,未逐个改 View,以保同一请求内读一致性)。事务语义、数据文件格式、并发模型不变。 前端(`public/app.js`): - 基础数据集页间缓存(servers/customers/nodes/protocols,TTL 30 秒);`api()` 内任何非 GET 成功统一失效缓存——全站写路径都过 `api()`,无漏点。 - `load()` 改渐进渲染:先以已有数据立即渲染当前页,各数据集到达后逐个应用并重渲染;overview/system/jobs 恒新鲜拉取;仅当本次全部请求皆失败才弹错误提示(保持旧失败面)。 量化证据(沙箱 amd64,合成种子 **20 服务器 / 40 客户 / 40 节点 / 160 绑定**,改动前后同种子同测法;旧版取 `aa2ee62` worktree 构建,基准文件同源移植): | 指标 | 改动前 | 改动后 | 倍数 | |---|---|---|---| | Clone 一次 | 13.14 ms(JSON 往返) | 0.54 ms(反射) | ≈24× | | GET /api/overview | 31.74 ms | 0.48 ms | ≈66× | | GET /api/servers | 33.65 ms | 1.29 ms | ≈26× | | GET /api/customers | 31.17 ms | 2.29 ms | ≈14× | | GET /api/nodes | 31.31 ms | 5.08 ms | ≈6× | | GET /api/jobs | 24.63 ms | 0.64 ms | ≈39× | 注:改动后 nodes 接口剩余耗时主要是 PublicNode/订阅 URI 等业务序列化,不再是克隆。以上是 amd64 沙箱数值;路由器真机(A53+整机 load 约 3)验收待部署后用 devtools 同数据量复测,预期回到几十 ms 级的结论以真机数字为准。 空闲稳态 CPU 对照(用户追加项;规模按最终口径 **20 台 agent**): - 测法:同一种子(20 服务器/40 客户/40 节点,含 20 个带指纹的 agent 凭据),新旧二进制各起一份、无任何界面请求,驱动脚本按 agent-go 现网节奏模拟 20 台 agent——poll 每 8 秒、heartbeat 每 30 秒、usage 每 60 秒(空 samples),错峰启动;预热 15 秒后取 60.06 秒窗口,用 `/proc//stat` 的 utime+stime 差折算 %CPU(100 ticks=1 秒 CPU)。两版驱动错误数均为 0。 - 结果:**改动前 38.41% → 改动后 32.31%**。**未达到个位数目标,如实报告。** - 剩余成本主项(实测归因):agent 鉴权 `VerifySecret` 的 scrypt 校验。指纹快路径只负责定位 agent 记录,定位后仍须对 KeyHash 做一次 scrypt 验证(不可跳过,否则指纹即凭据);沙箱实测单次 VerifySecret ≈ 87.9 ms。稳态请求率 = poll 2.5/s+heartbeat 0.67/s+usage 0.33/s ≈ 3.5/s,scrypt 一项即 ≈ 3.5×87.9ms ≈ 30.7% CPU,与改动后 32.31% 几乎完全吻合——即克隆成本已被基本消除(旧版多出的约 6 个百分点即 JSON 全量克隆+usage 事务克隆落盘),稳态 CPU 现由**密码学校验**主导,与第 22 条的克隆问题是两个独立瓶颈。若后续要继续压稳态 CPU,方向是 agent 鉴权改造(如指纹命中后改用廉价 HMAC 校验或会话化令牌),属新立项,本批未动鉴权语义。 - 事务路径说明:`Transaction` 仍做一次全量克隆+落盘(写路径语义要求),稳态下仅 usage 每分钟 20 次触发,成本已被 scrypt 淹没,不是主项;60 秒 housekeeping 的事务+Snapshot 在两版 RSS 采样中均表现为分钟级瞬时尖峰。 RSS 对照(同种子、无界面请求): - 空闲(带现网参数 `GOMEMLIMIT=28MiB GOGC=50`,启动 20 秒后连取 3 次):改动前 10,876 KB 稳定;改动后 10,672–10,840 KB。**无回涨**,满足第 22 条「RSS 不回涨」口径。 - 稳态负载下(未加 GOMEMLIMIT 的沙箱默认,60 秒窗口每 10 秒采样):改动前 47→197 MB 持续爬升(JSON 克隆分配压力),改动后多数样本约 46 MB、housekeeping 分钟边界出现一次约 150 MB 瞬时尖峰后回落。 ### 7. 第 23 条 绑定变更合并下发(本批第 8 项)— ✅ 完成(`80140f3`) - 窗口语义:**固定窗口 10 秒**——从该节点第一次待发变更起算,后续变更不延长(用户补充口径);窗口内多次绑定类变更只在到期时以最终状态下发一次 `apply_resource`。 - 实现:Node 新增落盘字段 `rebuildPending`/`rebuildPendingAt`/`rebuildPendingExit`(omitempty);`Orchestrator.FlushPendingRebuilds(now)` 由主进程 1 秒 ticker 调用。**待发状态落盘**(二选一中取此方案):面板在窗口内重启,首个 tick 即补发,不会卡死。 - 打标路径:assign/unassign、AlignCustomerNodes、Suspend/ResumeCustomerResources、凭据 regenerate(原逐节点立即重建改打标)。 - 保持即时并清标:手动 `POST /api/nodes/:id/deploy`(约束①)、RepairNode、节点 PATCH 编辑、对账路径;RemoveNode 清标(约束②)——flusher 对 PendingDelete/removing/draft 节点的标直接丢弃,绝不复活;flush 失败写 node.Error+审计 `rebuild_flush_failed` 并继续其他节点。 - 测试:`rebuild_coalesce_test.go` 五项全过——连加 5 客户窗口内 0 次 apply、到期只 flush 1 次且资源含齐 5 人;固定窗口不滑动;窗口内删节点 flush 0 次且无 apply;手动部署立即 1 次并消费待发标;Store 重载后标仍在且可 flush。 ## 横向验收 - **全量测试**:`panel-go` 与 `agent-go` 各 `go test ./...` 全绿(含第 18 条 `listen_hotfix_test`、本批新增 socksfixed/rebuild_coalesce/realitykeys/realityscan/clone_equivalence/singboxversion 等)。 - **金标准**:`golden/golden.mjs verify` 仅有 1 处 diff——`lists.customers.body.customers[0].subscriptionClientCount 3 → 0`。已判定为**种子时间漂移、非本批回归**:用 `aa2ee62` 旧二进制复跑得到逐字相同的 diff(种子内订阅客户端的活跃时间窗已随墙钟过期,过期后恒为 0)。其余 31 个 section 逐字一致(含订阅全格式、部署任务、用量投影)。因此**未 recapture 基线**:该 diff 与本批行为变更无关,贸然 recapture 会把环境漂移写进基线。 - **真实 sing-box check**:冒烟中用改动后面板实际生成的两份节点资源(SOCKS5 固定账号节点+VLESS Reality 节点),按 agent-go `combinedSingBoxConfig` 同规则组装合并配置、keyMaterial 替换 `${REALITY_PRIVATE:...}` 占位后,跑沙箱自编 sing-box v1.13.21 `check` —— **通过**(入站:socks 听 0.0.0.0、users 恰 1 组;vless 听 0.0.0.0、Reality 私钥就位)。 - **沙箱真启冒烟**(新二进制、空库引导):登录成功;overview/servers/customers/nodes/jobs/system/protocols/session 全部 200;建服务器→令牌注册 agent 成功;零客户 socks 节点建单成功且固定账密自动生成(`ngs_` 前缀)、部署不被拦(202)、agent 侧 job 资源 inbounds users 恰固定一组;零客户 VLESS Reality 部署不被拦、资源完整;绑定客户后订阅 raw 链接为 `socks5://<固定用户名>:<固定密码>@…`(密码 URL 编码);reality-keys 接口对 Reality 节点返回完整公私钥对、非 Reality 节点 400;扫描接口对 www.microsoft.com 返回结构完整的结果行(沙箱直连出网被代理拦截,ok=false 且 reason 为握手失败——见缺口),对本地 TLS 1.3 服务器返回 ok=true、tlsVersion=TLS 1.3、keyExchange=X25519、keyExchangeOk=true、certName 与延迟齐全。冒烟全项 PASS。 ## 发布包 `~/workspace/NexusGate-release/amo-v3/`(CGO_ENABLED=0、-trimpath;面板构建前经 build.sh 刷新 embed_public): | 文件 | SHA256 | |---|---| | nexusgate-panel-linux-amd64 | `5698b7f5861078e8244c4d558d5191408b32590fd3b699446f1b564de9d6fcd9` | | nexusgate-panel-linux-arm64 | `57e7b5cd43fd0582bb0aa917a36734483fac3f46d6e9d61ce141d46473a4ff4b` | | nexusgate-agent-linux-amd64 | `b0aedfdcafa5cde7051f6cce6eb82eea709628f5e060688828ab8d107ed9df92` | | nexusgate-agent-linux-arm64 | `e8173e6712769184e87efd6c796129c12f3daf0c0127bfc629d758f412d8afa6` | 同目录附 `SHA256SUMS` 清单。 ## 未决缺口(如实) 1. **稳态 CPU 未达个位数**:32.31%(旧 38.41%),主因是 agent 鉴权 scrypt 而非克隆,详见第 22 条节内归因;需另立项改鉴权,本批未动。 2. **扫描器真实域名成功路径未在沙箱验到**:沙箱直连 TCP 出网被运行环境代理拦截(curl 走代理正常、直连 TLS 全部 WRONG_VERSION 类失败,python 直连复测同结果),故 www.microsoft.com 只验到结构完整的失败行;成功路径已由本地 TLS 1.3 服务器经面板接口实测通过,真实域名待部署后在路由器网络复测。 3. **真机数字待复测**:接口耗时、切页体感、RSS 均为沙箱 amd64 数值;路由器(arm64 A53)部署后需按第 22 条验收口径用 devtools 复测。 4. **取舍记录**:`PublicNode` 未剥离 socksUser/socksPassword(管理员接口内,编辑表单回显需要);扫描器探测用 InsecureSkipVerify(只判可达性/密钥交换,理由见第 16 条)。 5. **未部署、未推 Gitea**:`aa2ee62` 热修与本批提交均只在本地;现网四台 sing-box 版本记录补写、节点重新部署等部署动作不在本批范围。 ## 提交清单(aa2ee62 之后) | 提交 | 内容 | |---|---| | `bb974e2` | 第 12 条 agent 核心版本兜底 | | `2fafd85` | 第 20+21 条 先建后绑+SOCKS5 固定账号 | | `80140f3` | 第 23 条 绑定合并下发(固定窗口) | | `8189a51` | 第 15 条 凭据页签删绑定区块 | | `a9d9867`+`4199a43` | 第 17 条 节点页 Reality 公私钥(+测试辅助改名) | | `0ab31ae` | 第 16 条 Reality 目标扫描器 | | `05366d9`+`6d97d4e` | 第 22 条 后端克隆消除+前端缓存/渐进渲染 | | `0f74955` | 第 22 条 基准种子扩容到 20 台规模 | | (收尾提交) | NEXT-ROUND-NOTES.md 原文(未改一字)+本报告 |