Files
dsh_shenxian/scripts/verify-mem-model.mjs
T
admin 918f1d3a51 feat(models): 平台自建「模型设置」—— 用户自配厂家 / 条目各自开关 / 共享模型开关(档案 87)
- 官方「设置 → 模型」页在平台环境**必然报错**(判据在**浏览器页面**的 loopback 判定;官方 README 原文 Non-loopback pages get no durable settings)⇒ 该分区对全角色(含 admin)隐藏,用户自配改走平台自建页(插件 0.3.11)
- DB 迁移 V6:credential_vault.route/base_url/api/models + users.shared_model_enabled;并**重定义 getEnabledCredentialKeyRef**(互斥删除后原实现无 ORDER BY ⇒ 「任取一条」)
- 新落地层 src/web/model-landing.ts:spawn 时把「已启用条目」写进实例 .credentials.yaml 与 settings.yaml 的 llm-pi-ai.providers.<route>;字段名与官方包实测对齐(apiKeyEnv / baseURL / api,**不是** protocol);只碰自己写过的 + 一次性交接
- 接口 /api/me/keys、/api/me/keys/:id/toggle、/api/me/models/shared;前端新增「设置 → 模型设置」分区(settings.section id=model-settings / order 100 / 全角色)
- 顺手修两处:ensure-role-profile-patch.cjs 的 --force 整文件覆盖会抹掉 admin 的 disable-hmr 与 workspace-scoped-picker 两个平台块(改为 stripManagedBlock 只替换自己那段);verify-mem-model.mjs 因档案 86 重构而长期失败的陈旧断言

⚠️ 本提交同时包含**档案 86(admin 跨用户实例管理 + 两处改名)**的代码改动 —— 该部分已上线并端到端验证;其 import/register 与本次改动同处 src/web/server.ts、poc/business-plugins/lib/client.js 等文件,按**文件粒度无法拆分**,且不带它会让仓库 tsc 直接失败(缺 src/web/routes/admin-user-ops.ts)。
2026-09-14 00:00:15 +08:00

102 lines
5.4 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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))
// 档案 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))
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)