feat(config): 配置管道治本——单一 schema + 暴露面收敛 + 一键重启 + 三元组 + 根因错误 + 保存即生效(A–G,含 #80) - #81
Merged
Merged
Conversation
守护(mebular serve)只在启动时读取一次 config.json,运行期没有热加载;
设置页保存后磁盘已变,但当前实例仍按上次启动的快照工作,用户误以为
「配置无法生效」。真实案例:config 22:12 保存 network/join=true,进程
21:30 启动,settings 仍为 false/false。
- admin.mjs:buildSettings 新增 pendingRestart,按 curated 白名单(对齐
serve.mjs CONFIG_PATCH_SPECS)对比当前磁盘 config.json 与启动快照,元素
形如 { path, file, running };空数组=未设置归一为 null;受环境变量优先
的路径跳过并列入 pendingRestartEnvOverrides(不报错)。新增 restart
元信息(serviceHeartbeat / serviceManaged)供 UI 选择重启命令。
- console.js:设置弹窗内持久「待重启」提示(中文标签 + 已配置/实际 +
可复制重启命令 + 根因说明 + 环境变量优先说明);设置入口徽标与「运行
状态」区标注「待重启 N 项」;保留保存 toast。
- verify-console.mjs(E1):新增 pendingRestart 断言(含刚改键、元素
形状、重启后清空);顺带让只读降级用例改用临时端口,避免与本机真实
实例抢 7331。
- verify-console-ui.mjs:页面内 fetch+CSRF 改 sync.antiEntropy.intervalMs,
断言出现「待重启」及该项,改回后确认清空。
证据:npm test / npm run lint / check:cleanliness / check:docs /
verify-console / verify-console-ui 全绿。
…le-ui 冲突) 冲突根因:PR #80 基于 06211ad,而 main 随后合入 PR #79(fix(console): 设置/关于本机不再随 3s 轮询重建)——两边都在 packages/console/scripts/verify-console-ui.mjs 的同一位置**新增** 检查块,正文冲突;console.js 本身自动合并成功。 解法(并集,语义两全): - 保留 PR #79 的「轮询重建回归」检查(DOM 节点引用跨 3s 不变 / 草稿保持 / 撤销复位) - 保留 PR #80 的「UI 待重启」检查(写 sync.antiEntropy.intervalMs → 出现「待重启」+变更项; 入口徽标;改回后清空) - **顺序**:轮询回归块在前、待重启块在后 —— 前者依赖「3s 内 DOM 稳定」,后者会写配置触发 重建,放在后面避免打断前者(并集语义即两侧原意) 语义兼容性核查(自动合并后逐项确认): - `settingsSignature()`/about 签名均含 `state.settings` → `settings.pendingRestart` 变化会触发重建, PR #79 的防重建**不会**吞掉 PR #80 的「待重启」横幅/徽标 - E1(verify-console.mjs,含 PR #80 的 8 条 pendingRestart 断言)本地全绿
…to feat/config-pipeline
A 单一真源:新增 packages/mcp/src/config-schema.mjs(纯数据)——每字段
{path, kind, spec, default, requiresRestart, exposure, ui, read, effective, reason};
由它驱动 ① serve 写校验 ② pendingRestart 判定(= #80 的 24 项,语义不弱化)
③ 三元组的「实际值/未生效原因」④ 控制台编辑器元数据(settings.configSchema,控制台不再重复声明)
⑤ verify:config 矩阵。防漂移断言:schema.EDITABLE/STATUS_ONLY == IA 渲染面/只读面(多/少即红)。
B 暴露面收敛(自动的不保留在 GUI):relayServers → status-only(只读「自动 relay 池」);
relayUnlimited → internal(不渲染);高级面 16→14、可编辑 20(常用 6+高级 14);
C5–C7 新项(lan.*/relayService/broadcast/nat/endpointStore/autoConnect…)→ internal(只入 RUNBOOK §13);
敏感 internal(storagePath/deviceId/encryption/tokensFile…)显式 writable:false(H4 不回归)。
C 一键重启:packages/service 增 restartPlanFor/restartService(launchd kickstart -k /
systemctl --user restart / schtasks End+Run;未注册只给手动指引)+ `mebular service restart`;
GUI POST /admin/api/restart(memory.admin+CSRF+confirm;先回 202 再分离式触发;DRY_RUN 可注入);
待重启横幅接「一键重启」按钮。
D 三元组:已配置 X / 实际 Y / **未生效原因**(服务端计算:待重启·环境变量优先·join 端口被占·
mDNS/LAN 不可达·桥未提供·缺证书·缺可选依赖·热生效·已生效);覆盖 editable 20 + status-only 11。
E 启动失败根因化:createJoinServer 抛错 → 「joinService.port <p> 被占(bind=…):改端口(如 0)或释放占用」
+ 落 join.error.json(不是笼统「serve 起不来」)。
F verify:config(45 断言):schema 写校验/暴露面/自锁/待重启/自动重启/防抖/热路径/回滚/
restart 接口 401·403·409·202/join 根因。红→绿 7 例(4 A + 3 G)各自必红,附断言原文(见 PR)。
G 保存即生效(用户拍板:保存成功 = 已生效或已回滚+明确报错):
- 含 requiresRestart 项 → {restarting:true} 自动重启(防抖 800ms 合并);全即时(MEBULAR_CONFIG_HOT_PATHS)→ 就地生效
- 生效校验:新实例写 serve-ready + 用运行时逐字段比对 → 绿「已生效」/红「未按预期生效」
- 回滚兜底:分离监督进程(apply-supervisor)等新实例健康,不健康→还原 .bak+重启→红「已回滚(根因)」
(pending.rollingBack 标记防回滚实例误写「已生效」);前台 nohup→手动命令+.bak 路径
- 自锁防护:auth/host/tls → 未确认返回 needsConfirmation(不写盘不重启)
断言只增不弱化:#80 的 8 条 pendingRestart 断言原样保留;A 轮 E1 断言原样保留。
零 core:git diff origin/main..HEAD -- src 为空。
根因(CI Console UI 冒烟 5/12 失败):上一轮把 CONFIG_EFFECTIVE 块换成服务端三元组时, `[起:CONFIG_EFFECTIVE 注释, 止:旧 runtimeCompareLine]` 的整段替换**误删了夹在中间的 5 个 helper**(fieldInitial / fieldToConfig / normalizedFieldValue / collectConfigChanges / formatRuntimeValue)——浏览器执行 renderCfgField 时抛 `fieldInitial is not defined`, 设置弹窗整个渲染不出来(E1 只做源码断言,不执行 JS,故未拦住;本地无 Chrome 也无法冒烟)。 现以 Node + DOM 桩 harness 实跑 renderSettings/renderAbout 四条路径回归(见下)。 - 恢复 5 个 helper(fieldInitial 改用服务端三元组「实际」值作初值) - valueWithEffective 补「未生效原因」第三列(About 只读行;此前替换未命中、reason 未生效) - schema ui 补 group 标签(反熵与快照/对端与签发者/语义召回/设备接入(高级)/MCP 接入(高级)), 高级面恢复分组渲染(随 CONFIG_EDITOR 移除时丢失) 复验:harness renderSettings(common/advanced/diagnostics)+renderAbout 全 ✓ · verify:console(E1)✓ · verify:config 45/45 ✓ · npm test 849/849 · build/lint 0
…r address' 抖动) 上一次 CI 的 Console UI 失败已不是产品问题:Chrome headless 启动即报 'Could not parse server address: Unknown address type…'(CDP/dbus 抖动,历史已知 flake)。 launchChrome 包一层 launchChromeRetry(杀掉重建 + 退避,最多 3 次)。 复验:verify-console-ui 本机无 Chrome SKIP(脚本语法/导入 ✓);CI 上重跑验证。
…钟挂起) 事实链:PR #80 的原生分支(56c2587,runs 298/299,无本轮 A–G 代码)与本轮分支的 windows job 都在「Console E1」步骤挂满 30 分钟被 timeout 杀掉(ubuntu E1 全绿、 main 的 windows E1 ~30 秒)→ 挂点是 verify-console.mjs 里的**无超时 await**, 与 A–G 无关(#80 的 pendingRestart 重启用例引入)。 - stop():signalCode 也算已退出;8s 限时 + SIGKILL 兜底(此前无限等 'exit') - execFileSync(mebular console / openssl version / openssl req)加 timeout(限时抛错) - Windows 步骤改 Tee-Object 流式输出:若仍挂起,步骤日志直接显示卡在哪一行 复验:verify:console 本地全绿(含 #80 8 条 + G 收尾);CI windows 步骤见流式日志。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
S1 概要
配置管道「治本 + 保存即生效」一轮(A–G,17 文件 +1260/−234,零 core:
git diff origin/main..HEAD -- src为空)。#80 处理:本轮分支已
merge origin/feat/console-pending-restart(保留其全部提交与断言,未重写历史)。#80 的 8 条 pendingRestart 断言原样保留、只增不弱化;本 PR 合并后可直接关闭 #80(改合本 PR 即覆盖其内容)。A 单一真源(治本)
packages/mcp/src/config-schema.mjs(纯数据)每字段{path, kind, spec, default, requiresRestart, exposure, ui, read, effective, reason},驱动:①POST /admin/api/config写校验 ②pendingRestart判定(= #80 的 24 项,逐项对应)③ 三元组「实际值/未生效原因」④ 控制台编辑器元数据(/admin/api/settings.configSchema,控制台不再重复声明字段)⑤verify:config矩阵。防漂移断言(E1):
schema.EDITABLE_PATHS == IA 渲染面且schema.STATUS_ONLY_PATHS == IA 只读面(多/少即红)。B 暴露面收敛(用户拍板:自动的不保留在 GUI)
network.enablednetwork.libp2p.listensync.namespacesjoinService.enabledmcp.http.portmcp.http.authsync.antiEntropy.enabledsync.antiEntropy.intervalMssync.antiEntropy.jitterRatiosync.snapshotThresholdsync.peerWhitelistsync.policyIssuerssemantic.enabledsemantic.minScorejoinService.bindjoinService.portmcp.http.hostmcp.http.tlsmcp.http.tlsKeymcp.http.tlsCertnetwork.libp2p.relayServerssync.autoSyncsync.pushOnWritestatus.lan.discoverystatus.relay.rolestatus.relay.bridgestatus.nat.holepunchstatus.net.broadcaststatus.join.endpointstatus.tlsstatus.invite.grantTtlnetwork.libp2p.relayUnlimitednetwork.libp2p.relayServernetwork.libp2p.relayPolicynetwork.lan.enablednetwork.lan.autoDialnetwork.lan.defaultFactorynetwork.relayServicenetwork.broadcast.modenetwork.broadcast.ttlMsnetwork.nat.autonatnetwork.nat.dcutrnetwork.endpointStorenetwork.autoConnectnetwork.peerssync.pushOnWriteThrottleMssync.peerNamespacePolicysync.syncTimeoutsync.syncStatePathsemantic.modelsemantic.cacheDirstoragePathstorageAdapterdeviceIddeviceNameidentity.modeencryption.levelencryption.passphraseEnvmcp.http.tokensFilenetwork.libp2p.relayServers:editable → status-only(只读「自动 relay 池」= config seeds ∪ 令牌 hints ∪ 地址簿学习)network.libp2p.relayUnlimited:editable → internal(GUI 不渲染;保留历史可写能力writable:true)lan.*/relayService/broadcast.*/nat.*/endpointStore…)→ internal(只入 RUNBOOK §13)writable:false(H4「敏感字段不可写」不回归)C 一键重启(修正 F-UI-1:
mebular service restart之前不存在)packages/service增restartPlanFor/restartService:launchdkickstart -k gui/<uid>/<label>· systemdsystemctl --user restart mebular-<kind>.service· win32schtasks /End+/Run;未注册只给手动指引(绝不盲发);命令可注入mebular service restart接上;设置卡 footer 文案与实际命令一致POST /admin/api/restart(memory.admin + CSRF +{confirm:true}):先返回 202 再分离式触发;前台运行 → 409 + 手动指引(命令 + .bak 路径);MEBULAR_RESTART_DRY_RUN=1可注入D 三元组(补 #80 的双值)
已配置 X / 实际 Y / **未生效原因**(服务端计算,前端只渲染):待重启 · 环境变量优先 · join 端口被占 · LAN/回环不可达 · 桥未提供 · 缺证书 · 缺可选依赖 · 热生效 · 已生效。覆盖 editable 20 + status-only 11。E 启动失败根因化
createJoinServer抛错 → 启动错误即「joinService.port <p> 被占(bind=…):改端口(如 0)或释放占用」+ 落join.error.json`(不再笼统「serve 起不来」)。F/G
verify:config(45 断言全绿)+ 保存即生效requiresRestart项 →{restarting:true}自动重启(防抖 800ms 合并多次保存);全即时项(MEBULAR_CONFIG_HOT_PATHS)→{effectiveImmediately:true}就地生效serve-ready.json+ 用自身运行时逐字段比对 → 绿「已生效(字段清单)」/红「未按预期生效:<字段>(已配置 X / 实际 Y)」apply-supervisor.mjs)等新实例健康(serve-ready新 pid + 端口连通;MEBULAR_APPLY_TIMEOUT_MS可调),不健康 → 还原config.json.bak+ 再重启 →status:'rolled-back'(含根因);pending.rollingBack标记防回滚实例误写「已生效」;前台 nohup → 手动命令 + .bak 路径mcp.http.auth/host/tls→ 未带{confirm:true}返回 409needsConfirmation(不写盘、不重启)红→绿实证(≥4 A + ≥3 G,各自必红)
✗ S4 实际生效:intervalMs=保存值 且原因=已生效(actual=123456)、✗ S6 热路径不进待重启横幅(原因=已声明为热生效)(reason=null)✕ runServiceCli restart:已注册→0 且输出命令;usage 含 restart✗ B 暴露面收敛:relayServers 退出编辑面…(advanced=15 残留=network.libp2p.relayUnlimited)、✗ IA 不丢项…(editorPaths=21…)✗ schema↔IA 防漂移:editable 集合逐项一致(各 20)(schema=20 ia=21 差异=network.libp2p.relayUnlimited)✗ S4 保存需重启项 → 200 + restarting:true + restart.mode=service(status=200 mode=foreground)、✗ S4 lastApply=applied 且逐字段生效校验全通过(verify=[])✗ S7 已回滚:config.json 恢复为备份(joinService.enabled=false)(enabled=true)、✗ S7 回滚后旧配置实例恢复健康(serve-ready = 新 pid)(pid=undefined)✗ S4 lastApply=applied 且逐字段生效校验全通过(verify=[…:null,…])我的复验命令
手动复验「造启动失败再回滚」:
断言只增不弱化
依赖/平台说明
.bak仍在(诊断页给出手动回滚命令)——已记 RUNBOOK §13