Files
dsh_shenxian/scripts/verify-dsh-install.mjs
admin 1d72e8f9c8 chore(web): 降级留痕 + 收尾 —— 静默失效改为可观测;picker 升 0.1.5 消漂移(档案 88 / T07)
- 【留痕】model-catalog 读不到目录:console.warn(每进程一次)+ 新增 catalogDiagnostics(),并把结果透出到 GET /api/me/model-providers 的 catalog 字段;plugin-compat 的 platformPkgCount()=0 同样告警一次 ⇒「功能没做」与「做了但读不到目录」从此**可区分**(这正是那个 P1 长期潜伏的原因)
- 【前端】插件 0.3.13:厂家目录不可读 / 为空时**如实提示**(含目录路径),不再静默给空列表
- 【消漂移】poc/workspace-scoped-picker 升 0.1.5 并重打包铺发 —— 两实例已装 0.1.5 且含按序探测的新代码(此前只改了源码、线上还是旧候选)
- 【文档】README 补 DSHS_PACKAGE_DIR / DSHS_COMPAT_ROOT / DSHS_PI_AI_DATA_DIR(逃生口此前**未在任何地方暴露**)
- 【回归】verify-dsh-install 扩到 6 组(含「读不到 ≠ 目录为空」的区分);verify-platform-admin-section 加 2 条断言(词典 284/284)
- 真环境回归:picker 在 /usr/local 仍 import OK(没把本来能用的改坏)|providers 39(端点回 38)|platformPkgCount 223
2026-09-14 04:52:35 +08:00

149 lines
6.9 KiB
JavaScript
Raw Permalink 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-dsh-install.mjs —— 「平台内置 dsh 安装位置解析」回归(2026-09-14 · 修 P1)
*
* 为什么必须有:定位失败**不报错** —— 厂家目录读成空 ⇒ 前端「新增模型条目」只剩内置
* DeepSeek;平台包计数为 0 ⇒ 兼容性预检退化成「平台包目录不可读」。两者都只在**另一种
* 安装布局**(`npm root -g` = `/usr/lib/node_modules`)上出现,本机 `/usr/local` 布局
* **永远测不出来** ⇒ 只有把解析规则钉死成断言,才能防回归。
*
* 用法:node scripts/verify-dsh-install.mjs 退出码 0=全绿 / 1=有失败
*/
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { fileURLToPath, pathToFileURL } from 'node:url'
import { dirname, join } from 'node:path'
const ROOT = join(dirname(fileURLToPath(import.meta.url)), '..')
const M = await import(pathToFileURL(join(ROOT, 'lib/web/dsh-install.js')).href)
const { dshPackageRoot, dshScopeDir, piAiDataDir, resetInstallPathsCache } = M
let failed = 0
const ok = (name, cond, extra = '') => {
console.log((cond ? ' ✓ ' : ' ✗ ') + name + (extra ? ' ' + extra : ''))
if (!cond) failed++
}
/** 清掉所有相关 env,保证用例互不干扰。 */
const ENVS = [
'DSH_PACKAGE_DIR',
'DSHS_PACKAGE_DIR',
'DSH_COMPAT_ROOT',
'DSHS_COMPAT_ROOT',
'DSHS_DSH_BIN',
'DSH_USERS_PLATFORM_DSH_BIN',
'PI_AI_DATA_DIR',
'DSHS_PI_AI_DATA_DIR',
]
const clearEnv = () => {
for (const k of ENVS) delete process.env[k]
resetInstallPathsCache()
}
console.log('verify-dsh-install(P1:安装位置解析)\n')
// ── 1. env 覆盖(无条件优先,含本仓前缀别名与导出侧旧名)────────────────────────
console.log('[1] env 显式覆盖')
for (const k of ['DSH_PACKAGE_DIR', 'DSHS_PACKAGE_DIR', 'DSH_COMPAT_ROOT', 'DSHS_COMPAT_ROOT']) {
clearEnv()
process.env[k] = '/tmp/fake-root-' + k
ok(`${k} 生效`, dshPackageRoot() === '/tmp/fake-root-' + k, dshPackageRoot())
}
// ── 2. 由「配置的 dsh 可执行文件」向上解出包根(**最可靠的一路**)────────────────
console.log('\n[2] 由 dsh 可执行文件解出包根(模拟 /usr/lib 布局)')
const T = mkdtempSync(join(tmpdir(), 'dsh-install-'))
const pkgRoot = join(T, 'node_modules', '@deepseek-ai', 'dsh')
mkdirSync(join(pkgRoot, 'lib'), { recursive: true })
writeFileSync(join(pkgRoot, 'package.json'), JSON.stringify({ name: '@deepseek-ai/dsh', version: '0.0.0-test' }))
const binPath = join(pkgRoot, 'lib', 'bin.js')
writeFileSync(binPath, '// stub\n')
for (const k of ['DSHS_DSH_BIN', 'DSH_USERS_PLATFORM_DSH_BIN']) {
clearEnv()
process.env[k] = binPath
ok(`${k} → 解出包根`, dshPackageRoot() === pkgRoot, dshPackageRoot().replace(T, '<T>'))
}
// 反例:bin 指向一个**不是 dsh 包**的目录 ⇒ 不许误判
clearEnv()
const otherRoot = join(T, 'node_modules', '@other', 'thing')
mkdirSync(join(otherRoot, 'lib'), { recursive: true })
writeFileSync(join(otherRoot, 'package.json'), JSON.stringify({ name: '@other/thing' }))
const otherBin = join(otherRoot, 'lib', 'bin.js')
writeFileSync(otherBin, '// stub\n')
process.env.DSHS_DSH_BIN = otherBin
ok('包名不匹配时不误认(回落到默认值)', !dshPackageRoot().startsWith(otherRoot), dshPackageRoot().replace(T, '<T>'))
// ── 3. 派生量 ─────────────────────────────────────────────────────────────────
console.log('\n[3] 派生:scope 目录 / pi-ai 数据目录')
clearEnv()
process.env.DSH_PACKAGE_DIR = '/opt/whatever/dsh'
ok(
'dshScopeDir = <root>/node_modules/@deepseek-ai',
dshScopeDir() === join('/opt/whatever/dsh', 'node_modules', '@deepseek-ai'),
dshScopeDir(),
)
ok(
'piAiDataDir 落在 <root>/node_modules/@earendil-works/pi-ai/dist/providers/data',
piAiDataDir() === join('/opt/whatever/dsh', 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'data'),
piAiDataDir(),
)
for (const k of ['PI_AI_DATA_DIR', 'DSHS_PI_AI_DATA_DIR']) {
clearEnv()
process.env.DSH_PACKAGE_DIR = '/opt/whatever/dsh'
process.env[k] = '/tmp/data-' + k
ok(`${k} 覆盖 piAiDataDir`, piAiDataDir() === '/tmp/data-' + k, piAiDataDir())
}
// ── 4. 缓存语义(env 改了不 reset 就用旧值;reset 后重新解析)────────────────────
console.log('\n[4] 缓存语义')
clearEnv()
process.env.DSH_PACKAGE_DIR = '/first'
const first = dshPackageRoot()
process.env.DSH_PACKAGE_DIR = '/second'
ok('缓存生效(不 reset 仍是旧值)', dshPackageRoot() === first && first === '/first', dshPackageRoot())
resetInstallPathsCache()
ok('reset 后重新解析', dshPackageRoot() === '/second', dshPackageRoot())
// ── 5. 本机实况:必须返回可用的绝对路径(不抛错)───────────────────────────────
console.log('\n[5] 本机实况')
clearEnv()
const real = dshPackageRoot()
ok('返回非空绝对路径', typeof real === 'string' && real.length > 0 && real.startsWith('/'), real)
ok('dshScopeDir 形状正确', dshScopeDir().endsWith(join('node_modules', '@deepseek-ai')), dshScopeDir())
// ── 6. 目录可读性诊断:**「读不到」必须与「目录为空」可区分**(2026-09-14 留痕)─────
console.log('\n[6] 目录可读性诊断(静默降级 → 可观测)')
const cat = await import(pathToFileURL(join(ROOT, 'lib/web/model-catalog.js')).href)
const warnings = []
const realWarn = console.warn
console.warn = (...a) => warnings.push(a.join(' '))
clearEnv()
process.env.PI_AI_DATA_DIR = join(T, 'no-such-data-dir') // 不存在 ⇒ 读不到
process.env.DSH_PACKAGE_DIR = '/nonexistent'
await cat.listCatalogProviders(true)
const d1 = cat.catalogDiagnostics()
ok('读不到时 readable=false', d1 !== null && d1.readable === false, JSON.stringify(d1))
ok('读不到时 count=0', d1 !== null && d1.count === 0)
ok('读不到时**告警过一次**(不再静默)', warnings.some((w) => w.includes('厂家目录不可读')), warnings[0]?.slice(0, 60) ?? '(无)')
clearEnv()
const emptyDir = join(T, 'empty-data')
mkdirSync(emptyDir, { recursive: true })
process.env.PI_AI_DATA_DIR = emptyDir // 存在但没有任何 <id>.json ⇒ "目录为空"
await cat.listCatalogProviders(true)
const d2 = cat.catalogDiagnostics()
ok('目录可读但为空时 readable=true(与"读不到"可区分)', d2 !== null && d2.readable === true && d2.count === 0, JSON.stringify(d2))
ok('已告警过就不再刷日志', warnings.filter((w) => w.includes('厂家目录不可读')).length === 1, String(warnings.length))
console.warn = realWarn
rmSync(T, { recursive: true, force: true })
console.log('')
if (failed > 0) {
console.log(`✗ verify-dsh-install 失败 ${failed} 项`)
process.exit(1)
}
console.log('✓ verify-dsh-install 全绿')