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 个厂家文件**
This commit is contained in:
admin committed 2026-09-14 04:38:58 +08:00
1 parent 022b1f770c
commit cbaf3ad653
7 files changed
+401 -23

No files matched your search

+79 -5
View File
@@ -22,6 +22,8 @@
* @module @dsh-local/workspace-scoped-picker
*/
import { existsSync, readFileSync, realpathSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { mkdir, opendir, realpath, stat } from 'node:fs/promises'
import { basename, dirname, isAbsolute, join, resolve, sep } from 'node:path'
import { pathToFileURL } from 'node:url'
@@ -29,11 +31,83 @@ import { pathToFileURL } from 'node:url'
/** 单个列举层级的行数上限(与官方后端同为 GitHub 风格 1000)。 */
const MAX_ENTRIES = 1000
/** 官方 seam 基类的候选绝对路径(按序尝试首个可导入者)。 */
const DEFAULT_SEAM_CANDIDATES = [
'/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-host-directory-picker/lib/index.js',
'/usr/local/lib/node_modules/@deepseek-ai/dsh-host-directory-picker/lib/index.js',
]
/** 平台内置 dsh 的包名(用来确认"这个目录确实是 dsh 包根")。 */
const PKG_NAME = '@deepseek-ai/dsh'
/** 要解析的官方包:目录选择器的 seam 基类。 */
const SEAM_PKG = 'dsh-host-directory-picker'
/**
* 平台内置 dsh 的**包根候选**(按序探测,命中即用)。
*
* ⚠️ 2026-09-14 修:原先把两个 seam 候选写成 `/usr/local/lib/node_modules/...` 的**绝对字面量**
* —— 而 `npm i -g` 的落点随发行版 / npm prefix 而变(以发行版包管理器装的 Node 常见
* `/usr/lib/node_modules`;本平台测试服 `test106` 就是这种)。写死 ⇒ 两个候选**都不存在**
* ⇒ 本模块 `import` 即抛错 ⇒ **实例内目录选择器不可用**(已实测复现)。
*
* 平台侧 `src/web/dsh-install.ts` 是**同思路的另一份实现** —— 本插件跑在**实例进程**内、
* 拿不到平台代码,所以必须自带一份。改一侧时记得同步另一侧。
*/
function dshPackageRootCandidates() {
const out = []
for (const k of ['DSH_PACKAGE_DIR', 'DSHS_PACKAGE_DIR', 'DSH_COMPAT_ROOT', 'DSHS_COMPAT_ROOT']) {
const v = process.env[k]
if (typeof v === 'string' && v !== '') out.push(v)
}
// 由平台配置的 dsh 可执行文件解软链反推包根(最可靠的一路;实例内一般拿不到该变量,故非必中)
for (const k of ['DSHS_DSH_BIN', 'DSH_USERS_PLATFORM_DSH_BIN']) {
const bin = process.env[k]
if (typeof bin !== 'string' || bin === '') continue
try {
let cur = resolve(realpathSync(bin))
for (let i = 0; i < 6; i += 1) {
const pj = join(cur, 'package.json')
if (existsSync(pj)) {
const pkg = JSON.parse(readFileSync(pj, 'utf8'))
if (pkg !== null && typeof pkg === 'object' && pkg.name === PKG_NAME) {
out.push(cur)
break
}
}
const up = dirname(cur)
if (up === cur) break
cur = up
}
} catch {
/* 忽略:继续用下面的常见全局根 */
}
}
for (const r of ['/usr/local/lib/node_modules', '/usr/lib/node_modules']) out.push(join(r, PKG_NAME))
return [...new Set(out)]
}
/**
* 官方 seam 基类的候选绝对路径(按序尝试首个可导入者)。
* 每个包根各出两种布局:dsh 自己的 `node_modules` 下(嵌套)与全局 scope 下(扁平)。
*/
const DEFAULT_SEAM_CANDIDATES = dshPackageRootCandidates().flatMap((root) => {
const globalRoot = dirname(dirname(root)) // <globalRoot>/@deepseek-ai/dsh → <globalRoot>
return [
join(root, 'node_modules', '@deepseek-ai', SEAM_PKG, 'lib', 'index.js'),
join(globalRoot, '@deepseek-ai', SEAM_PKG, 'lib', 'index.js'),
]
})
// 常见根全落空时,再问一次 npm(慢,故只在真落空时跑;失败/超时一律当作没有)。
if (!DEFAULT_SEAM_CANDIDATES.some((c) => existsSync(c))) {
try {
const npmRoot = execFileSync('npm', ['root', '-g'], {
encoding: 'utf8',
timeout: 5000,
stdio: ['ignore', 'pipe', 'ignore'],
}).trim()
if (npmRoot !== '') {
DEFAULT_SEAM_CANDIDATES.push(join(npmRoot, '@deepseek-ai', PKG_NAME, 'node_modules', '@deepseek-ai', SEAM_PKG, 'lib', 'index.js'))
DEFAULT_SEAM_CANDIDATES.push(join(npmRoot, '@deepseek-ai', SEAM_PKG, 'lib', 'index.js'))
}
} catch {
/* 忽略 */
}
}
/** 解析并导入官方 seam 基类。 */
async function loadSeam() {