Files
dsh_shenxian/scripts/verify-models-dict.mjs
T
admin 29c57a8207 feat(web): 模型设置页复刻官方交互 + 官方推荐插件按 dsh 版本过滤(档案 91/92/93)
档案 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 上一致。
2026-09-14 21:13:32 +08:00

133 lines
6.3 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-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)