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
+400 -22

No files matched your search

+1 -1
View File
@@ -22,7 +22,7 @@
"prepare": "npm run build",
"dev": "node lib/cli.js",
"typecheck": "tsc -p tsconfig.json --noEmit",
"verify": "npm run build && node --test test/db.test.mjs test/k8s-spawner.test.mjs test/leader.test.mjs test/local-user-fs.test.mjs test/crash-policy.test.mjs && node scripts/verify-inject.cjs lib/supervisor/proxy.js && node scripts/verify-static.mjs && node scripts/verify-platform-admin-section.mjs && node scripts/verify-mem-model.mjs && node scripts/verify-model-landing.mjs",
"verify": "npm run build && node --test test/db.test.mjs test/k8s-spawner.test.mjs test/leader.test.mjs test/local-user-fs.test.mjs test/crash-policy.test.mjs && node scripts/verify-inject.cjs lib/supervisor/proxy.js && node scripts/verify-static.mjs && node scripts/verify-platform-admin-section.mjs && node scripts/verify-mem-model.mjs && node scripts/verify-model-landing.mjs && node scripts/verify-dsh-install.mjs",
"test": "npm run build && node --test test/db.test.mjs test/k8s-spawner.test.mjs test/leader.test.mjs test/local-user-fs.test.mjs test/crash-policy.test.mjs && node scripts/verify-inject.cjs lib/supervisor/proxy.js",
"smoke": "node scripts/smoke.mjs",
"smoke:admin": "node scripts/smoke-admin.mjs",
+11 -2
View File
@@ -33,8 +33,17 @@ HOME=<userRoot>/ws pnpm add file:<userRoot>/ws/workspace-scoped-picker-0.1.0.tgz
## 依赖解析
唯一外部物是官方 seam 基类,用**绝对路径动态 import** 取得(不复制/不改官方包):
`/usr/local/lib/node_modules/@deepseek-ai/dsh/node_modules/@deepseek-ai/dsh-host-directory-picker/lib/index.js`,
可用 `DSH_SEAM_DIRECTORY_PICKER` 覆盖。
路径**按序探测**而成 —— env(`DSH_SEAM_DIRECTORY_PICKER` 显式指定,或 `DSH_PACKAGE_DIR` /
`DSHS_PACKAGE_DIR` / `DSH_COMPAT_ROOT` / `DSHS_COMPAT_ROOT` 给包根)→ 由平台配置的 dsh
可执行文件解软链反推包根(`DSHS_DSH_BIN` / `DSH_USERS_PLATFORM_DSH_BIN`)→ 常见全局根
(`/usr/local/lib/node_modules`、`/usr/lib/node_modules`)各取「嵌套 / 扁平」两种布局 →
最后兜底问一次 `npm root -g`。
⚠️ 2026-09-14 修:此前是**写死** `/usr/local/lib/node_modules/...` 两个字面量;在
`npm root -g` 落到 `/usr/lib/node_modules` 的机器(发行版包管理器装的 Node)上两个候选
**都不存在** ⇒ 本模块 import 即抛错 ⇒ 实例内目录选择器不可用(测试服已实测复现)。
平台侧同思路的另一份实现在 `src/web/dsh-install.ts`(本插件跑在实例进程内、拿不到平台代码,
故自带一份;**改一侧记得同步另一侧**)。
## 自检
+78 -4
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() {
+122
View File
@@ -0,0 +1,122 @@
#!/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 全绿')
+171
View File
@@ -0,0 +1,171 @@
/**
* 「平台内置 dsh」安装位置解析 —— **全平台唯一入口**(档案 87 补做 · 2026-09-14)。
*
* 为什么必须有这一层:**`npm i -g` 的落点随发行版 / npm prefix 而变** —— 实测
* · Debian 系常见 `/usr/local/lib/node_modules`(npm 默认 `prefix=/usr/local`)
* · 以发行版包管理器装的 Node(OpenCloudOS / CentOS / 宝塔 等)常见 `/usr/lib/node_modules`
* (本平台测试服 `test106` 就是这种:`/usr/bin/dsh` → `/usr/lib/node_modules/@deepseek-ai/dsh/lib/bin.js`)
* 而定位失败**不会报错**:调用方只会静默降级(厂家目录读成空、平台包计数为 0),
* 表现成「这个功能没做」,排查成本极高(实测:`/api/me/model-providers` 返回 `{"providers":[]}`)。
* ⇒ 不能写死单一路径,必须**按序探测**。
*
* 解析顺序(命中即停):
* 1. env `DSH_PACKAGE_DIR` / `DSH_COMPAT_ROOT`(+ 本仓前缀别名 `DSHS_PACKAGE_DIR` /
* `DSHS_COMPAT_ROOT`)—— 显式覆盖,也是测试注入点,**无条件生效**;
* 2. 平台配置的 dsh 可执行文件(`DSHS_DSH_BIN`,同时接受导出侧改名后的
* `DSH_USERS_PLATFORM_DSH_BIN`)→ `realpath` 后向上找含 `name=@deepseek-ai/dsh`
* 的 package.json。**最可靠的一路**:它就是实例真正在跑的那个包;
* 3. 常见全局根逐一探测(`/usr/local/lib/node_modules`、`/usr/lib/node_modules`);
* 4. `npm root -g`(慢,只在前面全落空时跑一次,5s 超时);
* 5. 全落空 ⇒ 返回历史默认值(**保持旧行为**,由上层继续按「目录不可读」降级)。
*
* 判定用 `package.json` 的 `name` 而非「目录存在」,避免撞上同名空目录。结果缓存。
*
* ⚠️ `poc/workspace-scoped-picker/lib/index.js` 里有一份**同思路的独立实现**
* (它跑在**实例进程**内、拿不到本模块 —— 实例侧不加载平台代码),改这里时别忘了那一处。
*
* ⚠️ 本模块**只依赖 node 内建**(无内部 import)⇒ 可单独编译、单独在目标机上验证。
*
* @module dshs/web/dsh-install
*/
import { existsSync, readFileSync, realpathSync, statSync } from 'node:fs'
import { execFileSync } from 'node:child_process'
import { dirname, join, resolve } from 'node:path'
/** 平台内建包的包名(`package.json.name`)。 */
const PKG_NAME = '@deepseek-ai/dsh'
/** 历史默认值(也是最后兜底);仅在探测全落空时使用。 */
const LEGACY_DEFAULT_ROOT = '/usr/local/lib/node_modules/' + PKG_NAME
/** 常见的全局 `node_modules` 落点(候选,非假设)。 */
const COMMON_ROOTS = ['/usr/local/lib/node_modules', '/usr/lib/node_modules']
/** 第一个非空的 env 值。 */
function firstEnv(names: readonly string[]): string | null {
for (const n of names) {
const v = process.env[n]
if (v !== undefined && v !== '') return v
}
return null
}
let cachedRoot: string | null = null
/** 该目录是不是 dsh 包根(看 `package.json.name`,避免撞上同名空目录)。 */
function isDshPackageDir(dir: string): boolean {
try {
if (!statSync(join(dir, 'package.json')).isFile()) return false
const pkg = JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { name?: unknown }
return pkg.name === PKG_NAME
} catch {
return false
}
}
/** 从某个路径向上找包根(最多 6 层,够到 `<root>/lib/bin.js` 这种深度)。 */
function findPackageRootUpwards(from: string): string | null {
let cur = resolve(from)
for (let i = 0; i < 6; i += 1) {
if (isDshPackageDir(cur)) return cur
const up = dirname(cur)
if (up === cur) break
cur = up
}
return null
}
/** 由平台配置的 dsh 可执行文件反推包根(解软链)。 */
function rootFromConfiguredBin(): string | null {
const bin = firstEnv(['DSHS_DSH_BIN', 'DSH_USERS_PLATFORM_DSH_BIN'])
if (bin === null) return null
try {
return findPackageRootUpwards(realpathSync(bin))
} catch {
return null
}
}
/** 由 `npm root -g` 反推包根(兜底;失败 / 超时一律当作没有)。 */
function rootFromNpm(): string | null {
try {
const root = execFileSync('npm', ['root', '-g'], {
encoding: 'utf8',
timeout: 5000,
stdio: ['ignore', 'pipe', 'ignore'],
}).trim()
if (root === '') return null
const candidate = join(root, PKG_NAME)
return isDshPackageDir(candidate) ? candidate : null
} catch {
return null
}
}
/**
* 平台内置 dsh 的包根目录。
* @returns 绝对路径;**不保证存在**(全落空时返回历史默认值,由调用方按「目录不可读」降级)。
*/
export function dshPackageRoot(): string {
if (cachedRoot !== null) return cachedRoot
const envRoot = firstEnv(['DSH_PACKAGE_DIR', 'DSHS_PACKAGE_DIR', 'DSH_COMPAT_ROOT', 'DSHS_COMPAT_ROOT'])
if (envRoot !== null) {
cachedRoot = envRoot
return envRoot
}
const probes: Array<string | null> = [
rootFromConfiguredBin(),
...COMMON_ROOTS.map((r) => join(r, PKG_NAME)),
rootFromNpm(),
]
for (const p of probes) {
if (p !== null && isDshPackageDir(p)) {
cachedRoot = p
return p
}
}
cachedRoot = LEGACY_DEFAULT_ROOT
return LEGACY_DEFAULT_ROOT
}
/** 平台内建包所在 scope 目录(`<root>/node_modules/@deepseek-ai`)。 */
export function dshScopeDir(): string {
return join(dshPackageRoot(), 'node_modules', '@deepseek-ai')
}
/**
* `@earendil-works/pi-ai` 的**厂家目录**数据位置。
* @returns 绝对路径;**不保证存在**(不存在时返回首选候选,由调用方降级为空目录)。
*/
export function piAiDataDir(): string {
const envDir = firstEnv(['PI_AI_DATA_DIR', 'DSHS_PI_AI_DATA_DIR'])
if (envDir !== null) return envDir
const root = dshPackageRoot()
const first = join(root, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'data')
// 另一种合法布局:pi-ai 被提升到 `dsh-llm-pi-ai` 自己的 node_modules 下。
const nested = join(
root,
'node_modules',
'@deepseek-ai',
'dsh-llm-pi-ai',
'node_modules',
'@earendil-works',
'pi-ai',
'dist',
'providers',
'data',
)
if (existsSync(first)) return first
if (existsSync(nested)) return nested
return first
}
/** 清解析缓存(改了 env 之后重新解析;测试用)。 */
export function resetInstallPathsCache(): void {
cachedRoot = null
}
+10 -8
View File
@@ -25,16 +25,18 @@
import { readdir, readFile } from 'node:fs/promises'
import { join } from 'node:path'
import { piAiDataDir } from './dsh-install.js'
/** dsh 包安装目录(可用 env 覆盖,便于测试与其它部署形态)。 */
const DSH_PACKAGE_DIR = process.env.DSH_PACKAGE_DIR ?? '/usr/local/lib/node_modules/@deepseek-ai/dsh'
/** 目录数据目录。 */
/**
* 厂家目录数据目录。
*
* ⚠️ 2026-09-14 修:原先写死 `/usr/local/lib/node_modules/...`,在 `npm root -g` 落到
* `/usr/lib/node_modules` 的机器上会**静默返回空目录**(测试服实测
* `GET /api/me/model-providers` → `{"providers":[]}`,而真实位置有 39 个厂家文件)。
* 改由 `dsh-install.ts` 按序探测(env → dsh 可执行文件解软链 → 常见全局根 → `npm root -g`)。
*/
export function catalogDir(): string {
return (
process.env.PI_AI_DATA_DIR ??
join(DSH_PACKAGE_DIR, 'node_modules', '@earendil-works', 'pi-ai', 'dist', 'providers', 'data')
)
return piAiDataDir()
}
/** 目录里一个厂家。 */
+7 -7
View File
@@ -30,11 +30,11 @@
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'
import { join, extname } from 'node:path'
import { pathToFileURL } from 'node:url'
// 平台内置包的落点**不能写死**:`npm root -g` 随发行版变(`/usr/local/lib` vs `/usr/lib`),
// 写死只会在另一种布局上静默退化成「平台包目录不可读」而不报错(测试服实测 = 0)。
// 统一走 `dsh-install.ts` 的按序探测。
import { dshScopeDir } from './dsh-install.js'
/** 平台内置包根目录(本部署事实;env 可覆盖,便于测试)。 */
const PLATFORM_DSH_ROOT =
process.env.DSH_COMPAT_ROOT ?? '/usr/local/lib/node_modules/@deepseek-ai/dsh'
const SCOPE_DIR = join(PLATFORM_DSH_ROOT, 'node_modules', '@deepseek-ai')
const DS_SCOPE = '@deepseek-ai/'
export interface CompatFinding {
@@ -69,13 +69,13 @@ function shortName(pkg: string): string {
}
function platformPkgDir(pkg: string): string {
return join(SCOPE_DIR, shortName(pkg))
return join(dshScopeDir(), shortName(pkg))
}
/** 平台内置 @deepseek-ai 包的数量(目录级,低成本)。 */
export function platformPkgCount(): number {
try {
return readdirSync(SCOPE_DIR).filter((n) => !n.startsWith('.')).length
return readdirSync(dshScopeDir()).filter((n) => !n.startsWith('.')).length
} catch {
return 0
}
@@ -269,7 +269,7 @@ export async function checkPluginCompat(root: string): Promise<CompatResult> {
}
try {
if (out.platformPkgs === 0) {
out.note = `平台包目录不可读:${SCOPE_DIR}`
out.note = `平台包目录不可读:${dshScopeDir()}`
return out
}
const pkgPath = findPackageJson(root)