根因: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 个厂家文件**
123 lines
5.5 KiB
JavaScript
123 lines
5.5 KiB
JavaScript
#!/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 全绿')
|