2026-09-13 21:38:04 +08:00
|
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
|
|
|
* verify-mem-model.mjs —— 内存模型「单一事实源」交叉校验(档案 84)
|
|
|
|
|
|
*
|
|
|
|
|
|
* 为什么需要:2026-09-13 实测发现「功能管理」那条内存条显示 388 MiB 并报「将超出上限」,
|
|
|
|
|
|
* 而平台真实配额是 672 / 384 —— 根因是**同一个事实被复制成了两份**:
|
|
|
|
|
|
* · 编排器 `src/supervisor/orchestrator.ts` 的 PLUGIN_MEM_MB / BASE / MIN / MAX(决定 MemoryMax)
|
|
|
|
|
|
* · 客户端 `poc/business-plugins/lib/client.js` 的 MEM_TABLE / MEM_BASE_MIB / …(决定界面显示)
|
|
|
|
|
|
* R1 把「写死 384」改成「按插件集合推导」时只改了前者 ⇒ 后者原地漂了。
|
|
|
|
|
|
*
|
|
|
|
|
|
* 本脚本把两处**钉死在一起**:任一常量或表项对不上,`npm run verify` 直接失败。
|
|
|
|
|
|
* 用法:node scripts/verify-mem-model.mjs 退出码 0=全绿 / 1=有失败
|
|
|
|
|
|
*/
|
|
|
|
|
|
import { readFileSync } from 'node:fs'
|
|
|
|
|
|
import { fileURLToPath } from 'node:url'
|
|
|
|
|
|
import { dirname, join } from 'node:path'
|
|
|
|
|
|
|
|
|
|
|
|
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
|
|
|
|
|
|
const read = (p) => readFileSync(join(ROOT, p), 'utf8')
|
|
|
|
|
|
|
|
|
|
|
|
let failed = 0
|
|
|
|
|
|
const ok = (name, cond, extra = '') => {
|
|
|
|
|
|
console.log((cond ? ' ✓ ' : ' ✗ ') + name + (extra ? ' ' + extra : ''))
|
|
|
|
|
|
if (!cond) failed++
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ① 编排器(权威)────────────────────────────────────────
|
|
|
|
|
|
const orch = read('src/supervisor/orchestrator.ts')
|
|
|
|
|
|
const spawner = read('src/supervisor/spawner.ts')
|
|
|
|
|
|
const dshRoute = read('src/web/routes/dsh.ts')
|
|
|
|
|
|
|
|
|
|
|
|
const num = (src, name) => {
|
|
|
|
|
|
const m = new RegExp('const ' + name + ' = (\\d+)').exec(src)
|
|
|
|
|
|
return m ? Number(m[1]) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
const ORCH = {
|
|
|
|
|
|
base: num(orch, 'BASE_MEM_MB'),
|
|
|
|
|
|
min: num(orch, 'MIN_MEM_MB'),
|
|
|
|
|
|
max: num(orch, 'MAX_MEM_MB'),
|
|
|
|
|
|
headroom: num(orch, 'HEAP_HEADROOM_MB'),
|
|
|
|
|
|
}
|
|
|
|
|
|
const orchTable = {}
|
|
|
|
|
|
{
|
|
|
|
|
|
const block = /const PLUGIN_MEM_MB: Record<string, number> = \{([\s\S]*?)\n\}/.exec(orch)
|
|
|
|
|
|
if (block) for (const m of block[1].matchAll(/'([^']+)':\s*(\d+)/g)) orchTable[m[1]] = Number(m[2])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
ok('编排器常量齐备(BASE/MIN/MAX/HEADROOM)', ORCH.base !== null && ORCH.min !== null && ORCH.max !== null && ORCH.headroom !== null,
|
|
|
|
|
|
`-> ${JSON.stringify(ORCH)}`)
|
|
|
|
|
|
ok('编排器插件成本表非空', Object.keys(orchTable).length > 0, '-> ' + JSON.stringify(orchTable))
|
|
|
|
|
|
|
|
|
|
|
|
// ── ② 客户端(副本)────────────────────────────────────────
|
|
|
|
|
|
const client = read('poc/business-plugins/lib/client.js')
|
|
|
|
|
|
const cnum = (src, name) => {
|
|
|
|
|
|
const m = new RegExp('var ' + name + ' = (\\d+)').exec(src)
|
|
|
|
|
|
return m ? Number(m[1]) : null
|
|
|
|
|
|
}
|
|
|
|
|
|
const CLIENT = {
|
|
|
|
|
|
base: cnum(client, 'MEM_BASE_MIB'),
|
|
|
|
|
|
min: cnum(client, 'MEM_MIN_MIB'),
|
|
|
|
|
|
max: cnum(client, 'MEM_MAX_MIB'),
|
|
|
|
|
|
headroom: cnum(client, 'MEM_HEAP_HEADROOM_MIB'),
|
|
|
|
|
|
}
|
|
|
|
|
|
const clientTable = {}
|
|
|
|
|
|
{
|
|
|
|
|
|
const block = /var MEM_TABLE = \{([\s\S]*?)\}/.exec(client)
|
|
|
|
|
|
if (block) for (const m of block[1].matchAll(/"([^"]+)":\s*(\d+)/g)) clientTable[m[1]] = Number(m[2])
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// ── ③ 断言:两处必须逐项一致 ────────────────────────────────
|
|
|
|
|
|
const pairs = [
|
|
|
|
|
|
['基线 BASE', ORCH.base, CLIENT.base],
|
|
|
|
|
|
['下限 MIN', ORCH.min, CLIENT.min],
|
|
|
|
|
|
['硬顶 MAX', ORCH.max, CLIENT.max],
|
|
|
|
|
|
['堆预留 HEADROOM', ORCH.headroom, CLIENT.headroom],
|
|
|
|
|
|
]
|
|
|
|
|
|
for (const [label, a, b] of pairs) ok(`常量一致 · ${label}`, a === b, `-> 编排器 ${a} / 客户端 ${b}`)
|
|
|
|
|
|
|
|
|
|
|
|
const allKeys = [...new Set([...Object.keys(orchTable), ...Object.keys(clientTable)])].sort()
|
|
|
|
|
|
const bad = allKeys.filter((k) => orchTable[k] !== clientTable[k])
|
|
|
|
|
|
ok('插件成本表逐键一致', bad.length === 0,
|
|
|
|
|
|
bad.length === 0 ? `-> ${allKeys.length} 项:${allKeys.map((k) => k + '=' + orchTable[k]).join(', ')}`
|
|
|
|
|
|
: '-> 不一致:' + bad.map((k) => `${k}(编排器 ${orchTable[k]} / 客户端 ${clientTable[k]})`).join('; '))
|
|
|
|
|
|
|
|
|
|
|
|
// ── ④ 断言:clamp 算式同款(防「常量对了、规则又各写一套」)────
|
|
|
|
|
|
ok('编排器 clamp 规则在位', /Math\.min\(Math\.max\(mb, MIN_MEM_MB\), MAX_MEM_MB\)/.test(orch))
|
|
|
|
|
|
ok('客户端 clamp 规则与编排器同构', /Math\.min\(Math\.max\(raw, MEM_MIN_MIB\), MEM_MAX_MIB\)/.test(client))
|
|
|
|
|
|
ok('客户端 heap 推导与编排器同构', /Math\.max\(128, Math\.min\(256, memMb - MEM_HEAP_HEADROOM_MIB\)\)/.test(client))
|
|
|
|
|
|
ok('编排器 heap 推导算法未变', /Math\.max\(128, Math\.min\(256, memMb - HEAP_HEADROOM_MB\)\)/.test(orch))
|
|
|
|
|
|
|
|
|
|
|
|
// ── ⑤ 断言:status 真的把真值透出来了(否则前端的「读真值」是空接线)──
|
|
|
|
|
|
ok('Spawner 接口声明 quotaInfo?', /quotaInfo\?\(userId: string\): \{ memMb: number; heapMb: number \} \| null/.test(spawner))
|
|
|
|
|
|
ok('编排器实现 quotaInfo', /quotaInfo\(userId: string\): \{ memMb: number; heapMb: number \} \| null \{/.test(orch))
|
2026-09-14 00:00:15 +08:00
|
|
|
|
// 档案 87 顺手修:档案 86 把 status 主体抽成 `statusForUser(app, user)` 之后,这里从
|
|
|
|
|
|
// `request.user!.id` 变成了 `user.id` ⇒ 老断言正则失配、`npm run verify` 一直红着。
|
|
|
|
|
|
// 断言意图不变("status 真的把真值透出来了"),改成不绑定形参名。
|
|
|
|
|
|
ok('/api/dsh/status 返回 quota', /quota:\s*app\.supervisor\.quotaInfo\?\.\([^)]*\)\s*\?\?\s*null/.test(dshRoute))
|
2026-09-13 21:38:04 +08:00
|
|
|
|
ok('客户端会去读 /api/dsh/status', /\/api\/dsh\/status/.test(client) && /setQuotaInfo\(/.test(client))
|
|
|
|
|
|
|
|
|
|
|
|
console.log(failed === 0 ? '\n结论:全绿 ✅' : '\n结论:有 ' + failed + ' 项失败 ❌')
|
|
|
|
|
|
process.exit(failed === 0 ? 0 : 1)
|