档案 91 ——「模型设置」页折行 + 复刻官方交互(插件 0.3.15) · 折行根因不是"窄":原「我已添加的厂家」是 4 列表格(列宽 auto)⇒ 名称/状态/两按钮三处折行同源; 改官方 dsh-client-ui-settings-models 的**卡片行**(rowHead + rowActions margin-left:auto,两侧 nowrap) · 「新增」改官方**两步式**:默认两个虚线按钮 → 点开才出卡片,主字段只剩「API 密钥」 (官方原文:页面从不询问环境变量名);次要字段收进 <details>「自定义设置」 · CSS 数值逐值照抄官方 CSS 模块;顺修「由 admin 配置」「密钥可留空」两处错文案,补删除二次确认 档案 92 —— 官方推荐插件列表按 dsh 版本过滤(插件 0.3.16) · 官方目录不带 dsh 版本字段 ⇒ 逐包查 npm 声明与平台真实版本比,分类 match/none/newer/older/unknown · 默认隐藏「仅兼容更旧版本」,「需更高版本」橙色标注;勾选框为逃生口;说明行给隐藏计数(不静默消失) · ⚠️ 判 satisfies 必须带 includePrerelease(平台是 prerelease,默认语义会把在跑的 dshmarket 之类判成更旧) 档案 93 —— 兼容判定收口(服务器 CI 由红转绿) · 「单测长期红」真相 = 服务器源码落后于 git(本机 48/0 全绿)⇒ 同步即修,非代码问题 · 伞包 @deepseek-ai/dsh 收口到 dsh-install.platformPackageVersion()(它不在自己的 node_modules 里, 原先恒判 null ⇒ 声明在伞包上的"要求更高版本"全被放行);闸门与列表共用同一份版本表 · 闸门判据 A 加 prerelease 兜底:实测抽样 300 里 prerelease-artifact 117 条(≈39% 假阳性, 含在跑的 dshmarket)⇒ 容忍下能满足则只计数(prereleaseOnly)不阻断 校验 · 新增 scripts/verify-dsh-compat.mjs(21 条回归锚点)与 verify-models-dict.mjs / verify-models-render.cjs · verify-platform-admin-section.mjs:断言从旧 UI(表格/下拉「自定义厂家…」)改成断言新 UI (含模拟点击两个新增入口 + 选中目录厂家) · 两个仓库外的动作:服务器 npm run build + ci.sh(CI OK)+ systemctl restart dshs;插件 0.3.16 已铺发 ⚠️ 本提交未包含 src/supervisor/orchestrator.ts —— 那是别人在途的内存配额改动 (BASE 160→448 / MIN 384→512 / MAX 1024→1536 / mcn 128→256),其对应的客户端常量尚未同步, 本机 verify-mem-model 因此有 4 项红;提交后仓库树两边均为旧值,该断言在 HEAD 上一致。
133 lines
6.3 KiB
JavaScript
133 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)
|