1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
+ ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
7.9 KiB
7.9 KiB
84-实例内存配额口径统一(消灭「388 MiB」误报)
2026-09-13 落地 | 触发:用户问「实例内存大小是不是调整过了,为什么功能设置中还是显示 388 多」 定性:同一事实被复制成两份、只改了一份 —— 属于本项目反复出现的「两处副本必然漂」类缺陷。
一、现象与实测(先取证,再动手)
「dsh 设置 → 功能管理」顶部那条内存条显示 勾选后 388 MiB 并报 ⚠ 将超出上限。
实测真实配额(systemctl show <scope> -p MemoryMax,字节换算):
| 实例 | 装载的重型插件 | 真实 MemoryMax |
验算 |
|---|---|---|---|
| guest(uid 100002) | dsh-univer-office |
704643072 = 672 MiB |
160 + 512 ✓ |
| admin(uid 114801) | dsh-plugin-mcn-suite |
402653184 = 384 MiB |
clamp(160+128)=384(踩下限)✓ |
V8 堆两边都 = min(256, 配额−96) = 256 MB。⇒ 配额确实早就改过了,且是真生效的。
二、根因:两套模型从未对齐
| 项 | 平台侧(决定真实 MemoryMax) |
插件前端(决定界面显示) |
|---|---|---|
| 位置 | src/supervisor/orchestrator.ts |
poc/business-plugins/lib/client.js |
| 基线 | BASE_MEM_MB = 160 |
MEM_BASE_MIB = **285** |
dsh-univer-office |
512 | 64 |
dsh-plugin-mcn-suite |
128 | 39 |
| 合成规则 | clamp(160+Σ, **384**, 1024) |
285 + Σ,并把 384 当硬上限判超限 |
| 「上限」 | 384 只是下限,硬顶是 1024 | MEM_LIMIT_MIB = 384(档案 58 时代的写死值副本) |
- 388 的来源:候选池恰好只有这 2 个插件(实调
/api/plugins/business确认),前端表对两者都有值 ⇒ 全勾 =285+64+39 = 388。而平台侧同一情形 =160+512+128 = 800。 - 误报机制:
388 > 384触发「将超出上限」红色警告;但 384 在平台侧早已只是下限 ⇒ 纯误报。 - 为什么插件只能自估:
/api/dsh/status不返回任何内存字段(只有 running / instance / watchdog / breaker / url)⇒ 插件无从得知真值。 - 漂移时点:档案 81 R1 把「写死 384」改成「按插件集合推导」时,只改了编排器;插件那份副本原地未动,此后所有改动(univer 384→512、mcn-suite 128)都没同步。
三、改法(两步,一次做掉)
① 平台侧:把已算好的真值透出来(零新增计算)
| 文件 | 改动 |
|---|---|
src/supervisor/spawner.ts |
Spawner 接口新增可选方法 quotaInfo?(userId): { memMb; heapMb } | null(照 breakerInfo? 的模式) |
src/supervisor/orchestrator.ts |
实现 quotaInfo() —— 调同一个 instanceMemMb() / heapMbFor(),读不到返回 null |
src/web/routes/dsh.ts |
/api/dsh/status 增加 quota: app.supervisor.quotaInfo?.(request.user!.id) ?? null |
⇒ 因为走的是 spawn 时用的同一函数,不可能再与实际 MemoryMax 对不上。
② 插件侧:改读真值 + 对齐规则 + 修正超限判据
- 常量与规则逐项对齐编排器(
MEM_BASE_MIB 160/MEM_MIN_MIB 384/MEM_MAX_MIB 1024/MEM_HEAP_HEADROOM_MIB 96/MEM_TABLE{univer:512, mcn:128}),并新增quotaOf()/rawMiB()/heapMiBFor()与编排器同构。 load()并行取/api/dsh/status(失败静默,只影响内存条),把quota.memMb/heapMb作为事实行直接显示:实际配额 384 MiB · V8 堆 256 MiB;读不到才退回估算并如实标注。- 超限判据改为「原始需求 > 硬顶 1024」 —— 旧版拿 clamp 后的值判,既误报、又在语义上不可能触发(clamp 后永远 ≤ MAX)。
- 未进成本表的插件不再显示
≈ 0 MiB徽章(平台口径「未识别按 0 计」,显示 0 只会制造噪音)。 - 文案:
上限→硬顶、将超出上限→顶到硬顶,overWarn明确写出「1024 MiB 硬顶」。 - 版本 0.3.6。
③ 钉死(关键):新增 scripts/verify-mem-model.mjs
根因是「同一事实两处副本」,所以修法必须包含「让它不可能再漂」。
脚本解析编排器与插件的常量/成本表/clamp 算式/接线点,逐项交叉断言,并接进 npm run verify:
常量不一致、成本表任一键不一致、clamp 算式不同构、quotaInfo 接口/实现/路由任一环缺失、插件不再读 status —— 任一命中即构建失败。
反向验证(已做):把插件的 MEM_BASE_MIB 改回旧值 285 ⇒ 脚本报
✗ 常量一致 · 基线 BASE -> 编排器 160 / 客户端 285、退出码 1;还原后全绿。
(说明这道闸门真的会拦,而不是摆设。)
四、验证记录
| 层 | 证据 |
|---|---|
| 编译 | npm run build(tsc)零报错 |
| 单测 | npm run verify 全绿 —— 含新增的 15 项模型交叉断言 |
| 平台实装 | 服务器 /opt/dshs/lib/supervisor/orchestrator.js、lib/web/routes/dsh.js md5 与本机编译产物一致(差异已核:orchestrator 正好 19 行 = 我的改动;dsh.js 3 行 + 1 行他人旧的注释) |
| 接口 | GET /api/dsh/status → quota: {"memMb":384,"heapMb":256},与实测 MemoryMax=402653184 吻合 |
| 插件实装 | admin/guest 两 profile 均 0.3.6;lib/client.js md5 86bc8a46… 与本地产物一致;旧符号 MEM_LIMIT_MIB 0 处 |
| 端到端 | 冷启动取下发 bundle(rev=a8eb3ecf1edb,4.30 MB):MEM_BASE_MIB=160、MEM_MAX_MIB=1024、univer 成本 512、读 status 6 处、旧符号 0 处 |
修复后界面应有的样子(admin 为例):当前 384 MiB → 勾选后 800 MiB + 实际配额 384 MiB · V8 堆 256 MiB,
状态 余量充足(800 < 1024)—— 388 与红色误报都不再出现。
五、事故/踩坑记录
- 并发撞车(本次最该记住的一条):
poc/business-plugins/lib/client.js在我作业期间被并行会话连续改了两轮(0.3.4「系统管理改为门户 6 功能页」、0.3.5「插件管理列表高度」,0.3.5 已部署)。因为每次Edit前都重读了文件,我的改动是叠在它们之上、没有覆盖;但版本号我以为是 0.3.3、实际已是 0.3.5 ⇒ 我第一把npm pack打出了「内容是别人的 0.3.5 + 我的修改、版本号却写 0.3.5」的同名假产物(已删除),改判为 0.3.6。 ⇒ 纪律:动手前先git status+ 查最新 tgz 版本号;版本号取「当前值 +1」,不要用记忆里的值。 - 未提交区已被多方污染:该仓
git status同时有他人未提交的 0.3.4/0.3.5(已上线但未 commit)。 ⇒ 本次只提交我自己的文件,poc/business-plugins/lib/client.js与其package.json保持未提交、留给他人的批次一起提交(避免把别人在途改动「顺手上锁」)。 server 侧 dsh.js比仓库旧一行注释(/desktop.html→/portal.html)—— 说明服务器编译产物整体略滞后于仓库;本次顺手带上,无行为影响。
六、回滚
- 平台:
/opt/dshs/.bak-20260913-memmodel/{orchestrator.js,dsh.js}拷回原位 →systemctl restart dshs.service - 插件:
/opt/dsh/artifacts/保留历代 tgz(0.3.5 仍可取)→node /opt/dshs/scripts/ensure-biz-plugins.cjs --all --restart指定旧版 - 校验脚本:删掉
package.jsonverify 链末尾的&& node scripts/verify-mem-model.mjs即可(脚本本身独立,不影响运行)
七、后续(未做,非阻塞)
- 若成本表再加插件:只改编排器 + 跑 verify 会失败 ⇒ 逼你同步插件侧(这就是设计意图)。若将来希望彻底「只维护一处」,可考虑由
/api/dsh/status或新增只读接口下发成本表本身,插件不再内置副本。 business-plugins前端那条内存条现在展示的是配额(cgroup 上限)而非实时占用;若要显示真实 RSS,需要平台侧新增采样接口(当前未做,文案不声称是实时值)。