Files
dsh_shenxian/scripts/verify-models-dict.mjs
T

132 lines
6.3 KiB
JavaScript
Raw Normal View History

#!/usr/bin/env node
/**
* verify-models-dict.mjs —— 「模型设置」页(`poc/business-plugins/lib/client.js` 的 ms.* 分区)
* 的**词典完整性 + 版式防线**交叉校验(档案 91)。
*
* 为什么需要(两个都真踩过):
* ① **漏键是静默的**:2026-09-14 重写该页时把 `ms.needUrl` / `ms.needModels` 从词典里删掉、
* 却仍被 `submitCustom()` 引用 —— 页面只会在校验失败时把**键名原样**显示给用户,
* 构建与语法检查**都不会报错**。zh/en 也容易只改一边。
* ② **换行是版式问题、不是逻辑问题**:用户截图报「内置 DeepSeek 换行 + 两个按钮换行」,
* 根因是那 4 列表格用了**自动列宽**。改用官方卡片行后,只要 `rowName` / `rowActions`
* 的 `white-space:nowrap` 与 `rowActions` 的 `margin-left:auto` 任一被删,缺陷就会复发
* —— 而肉眼很难在构建期发现。这里把它钉成断言。
*
* 用法:node scripts/verify-models-dict.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 src = read('poc/business-plugins/lib/client.js')
// ── ① 取出 zh / en 两个词典对象字面量(按花括号配平截取,不用 eval 全文件)──
/** 从 `var <name> = {` 起按花括号配平截出对象字面量文本。 */
function grabDict(name) {
const at = src.indexOf('var ' + name + ' = {')
if (at < 0) return null
const start = src.indexOf('{', at)
let depth = 0
for (let i = start; i < src.length; i++) {
const c = src[i]
if (c === '{') depth++
else if (c === '}') {
depth--
if (depth === 0) return src.slice(start, i + 1)
}
}
return null
}
const zhText = grabDict('zh')
const enText = grabDict('en')
ok('取到 zh 词典对象', zhText !== null)
ok('取到 en 词典对象', enText !== null)
if (zhText === null || enText === null) process.exit(1)
/** 只取**顶层**键:按缩进深度 1 的行首 `"k":` 判定,避免把嵌套对象里的键也算进来。 */
function keysOf(text) {
const keys = []
const dups = []
const seen = new Set()
for (const m of text.matchAll(/^\t{3}"([^"]+)":/gm)) {
if (seen.has(m[1])) dups.push(m[1])
seen.add(m[1])
keys.push(m[1])
}
return { keys, dups }
}
const zh = keysOf(zhText)
const en = keysOf(enText)
ok('zh 无重复键(重复会让后者静默胜出)', zh.dups.length === 0, zh.dups.length ? '-> ' + zh.dups.join(', ') : '')
ok('en 无重复键', en.dups.length === 0, en.dups.length ? '-> ' + en.dups.join(', ') : '')
const zhSet = new Set(zh.keys)
const enSet = new Set(en.keys)
const onlyZh = zh.keys.filter((k) => !enSet.has(k))
const onlyEn = en.keys.filter((k) => !zhSet.has(k))
ok('zh / en 键数一致', zh.keys.length === en.keys.length, `-> zh ${zh.keys.length} / en ${en.keys.length}`)
ok('zh ⊆ en', onlyZh.length === 0, onlyZh.length ? '-> 仅 zh 有: ' + onlyZh.join(', ') : '')
ok('en ⊆ zh', onlyEn.length === 0, onlyEn.length ? '-> 仅 en 有: ' + onlyEn.join(', ') : '')
// ── ② 代码里引用的键必须都声明了(本轮真漏过)──
// ⚠️ 2026-09-14 扩到**全词典**(原来只查 `ms.*`):`pa.*` 那批一样会漏,
// 而漏键在页面上只表现为"把键名原样显示给用户",构建与语法检查都不报错。
const used = new Set()
for (const m of src.matchAll(/\bt\("([A-Za-z0-9_.]+)"\)/g)) used.add(m[1])
// MS_ERR 的值是把后端错误码映射成的词典键
{
const block = /var MS_ERR = \{([\s\S]*?)\n\t{3}\};/.exec(src)
if (block) for (const m of block[1].matchAll(/"(ms\.[A-Za-z0-9_.]+)"/g)) used.add(m[1])
}
ok('词典引用点非空', used.size > 100, `-> 引用键 ${used.size} 个`)
const missing = [...used].filter((k) => !zhSet.has(k))
ok('被引用的词典键全部已声明', missing.length === 0, missing.length ? '-> 缺失: ' + missing.join(', ') : '')
// ── ③ 版式防线:卡片行的「不换行」三件套(少任何一个,用户报的换行就会复发)──
const cssHas = (selector, decl) => {
const re = new RegExp('\\.' + selector + '\\{[^}]*' + decl + '[^}]*\\}')
return re.test(src)
}
ok('.ms-rowName 有 white-space:nowrap', cssHas('ms-rowName', 'white-space:nowrap'))
ok('.ms-rowActions 有 white-space:nowrap', cssHas('ms-rowActions', 'white-space:nowrap'))
ok('.ms-rowActions 有 margin-left:auto(动作右对齐,官方 rowActions 同)', cssHas('ms-rowActions', 'margin-left:auto'))
ok('.ms-rowHead 是 flex 且 align-items:center', cssHas('ms-rowHead', 'align-items:center'))
ok('.ms-addActions 可换行(两个虚线按钮窄屏时上下堆叠)', cssHas('ms-addActions', 'flex-wrap:wrap'))
ok('.ms-addBtn 有 flex:1 1 0 + min-width:180px(官方 addButton 同)', cssHas('ms-addBtn', 'min-width:180px'))
// ── ④ 新的新增流程确实在(两个虚线入口 + 已无 4 列表格)──
ok('默认态有「添加提供方」入口', src.includes('ms.addProvider'))
ok('默认态有「添加自定义提供方」入口', src.includes('ms.addCustom'))
// 描述字符串很长,最容易混进未转义的 `"` 把 JSON 写坏(2026-09-14 实际踩过一次)
{
let pkgsOk = true
for (const p of ['package.json', 'poc/business-plugins/package.json']) {
try {
JSON.parse(read(p))
} catch {
pkgsOk = false
}
}
ok('package.json 均为合法 JSON', pkgsOk)
}
ok('「我已添加的提供方」已从表格改为 ul.ms-rows', /className: "ms-rows"/.test(src))
{
// 「模型设置」分区内不应再出现表格(其它页面仍可用 pa-tbl)
const from = src.indexOf('function ModelSettingsSection()')
const to = src.indexOf('ctx.slots.inject("settings.section"')
const body = from >= 0 && to > from ? src.slice(from, to) : ''
ok('「模型设置」分区内无 pa-tbl 残留', body.includes('pa-tbl') === false)
}
console.log(failed === 0 ? '\n全部通过(模型设置:词典一致性 + 版式防线)' : `\n${failed} 项失败`)
process.exit(failed === 0 ? 0 : 1)