Files
dsh_shenxian/scripts/verify-dsh-install.mjs
T
admin cbaf3ad653 fix(web): 内置 dsh 安装路径改为按序探测 —— 修厂家目录/平台包目录在 /usr/lib 布局下静默失效
根因:npm root -g 的落点随发行版变(Debian 系 /usr/local/lib;发行版包管理器装的 Node 常见 /usr/lib),而三处把它写死 ⇒ 定位失败**不报错**、只静默降级:
- src/web/model-catalog.ts:30  → 厂家目录读成空 ⇒ GET /api/me/model-providers 返回 {"providers":[]}
- src/web/plugin-compat.ts:36  → platformPkgCount() = 0 ⇒ 预检退化成「平台包目录不可读」
- poc/workspace-scoped-picker/lib/index.js:34-35(**交接单漏掉的第 3 处**)→ 本模块 import 即抛错 ⇒ 实例内目录选择器不可用

改动:
- 新增 src/web/dsh-install.ts(全平台唯一入口;只依赖 node 内建,可单独在目标机验证):env → 平台配置的 dsh 可执行文件解软链反推包根(最可靠)→ 常见全局根 → npm root -g → 历史默认值
  ⚠️ env 名两侧都认:源仓 DSHS_DSH_BIN / 导出侧 DSH_USERS_PLATFORM_DSH_BIN(交接件只用了后者,照抄会让该分支在源仓侧永不生效)
- 三处调用点改走该模块;插件侧跑在实例进程内、拿不到平台代码,自带同思路一份(两处需同步改)
- 新增 scripts/verify-dsh-install.mjs(15 项断言)并接入 npm run verify

验证:本机 tsc 零错误|verify 全套绿|单测 16/0;服务器回归 platformPkgCount=223、厂家目录 39;**测试服 test106(/usr/lib 布局 = 原故障机)在「无 env / DSHS_DSH_BIN / DSH_USERS_PLATFORM_DSH_BIN」三种姿势下均解析到 /usr/lib 且 39 个厂家文件**
2026-09-14 04:38:58 +08:00

123 lines
5.5 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-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())
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 全绿')