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 上一致。
This commit is contained in:
admin committed 2026-09-14 21:13:32 +08:00
1 parent 1d72e8f9c8
commit 29c57a8207
11 files changed
+1474 -209

No files matched your search

+132
View File
@@ -0,0 +1,132 @@
#!/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)