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:
1 parent
022b1f770c
commit
cbaf3ad653
7 files changed
+401
-23
No files matched your search
@@ -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() {
|
||||
|
||||
Reference in new issue
Block a user