132 lines
6.3 KiB
JavaScript
132 lines
6.3 KiB
JavaScript
#!/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)
|