Files
dsh_shenxian/dsh-server-docs/04-调整方案/88-内置dsh安装路径探测-修静默失效P1.md
T
admin 5ad755116e chore(docs): 文档库并入代码仓(R4 选 a)+ 索引/台账跟进
1) dsh-server-docs/ 从工作区(原 E:\...\aliyun-dsh-server\dsh-server-docs)**整体并入本仓**,
   保留目录名 ⇒ 仓库内 dsh-server-docs/... 的相对引用天然继续有效;旧目录(含其 .git)已归档到
   工作区 _中间产物_待清理/,未随本提交带入。
2) .gitattributes:新增 `dsh-server-docs/** -text` —— 原文档库是 `* -text` + autocrlf=false,
   必须保持纯 LF,否则会被本仓的 CRLF 规则翻掉。
3) 活引用里的绝对路径已全部改到新位置(docs 的 INDEX / README / scripts / skills + 用户级 skills
   + ~/.workbuddy/settings.json 的 hooks);历史档案(04-调整方案/、archive/)按「只增不改」未动。
   ⚠️ hooks 路径改动需「完全重启会话」才生效(配置是会话启动快照)。
4) 交接单/T08:新增 §16「生产整体切换执行记录」(形态 / 落地动作 / **4 个只有真上线才暴露的真 bug** /
   验收证据 / 回滚命令 / 残留项);台账 T08 行 → 已完成并归档;03-路线图 §二 登记 T08 收尾项。
5) 统一称谓:**「本机」只指跑 WorkBuddy 的开发机**,47 / 106 一律写「远程服务器」。
2026-09-15 18:47:13 +08:00

92 lines
8.7 KiB
Markdown
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.
# 88 · 内置 dsh 安装路径按序探测 —— 修「厂家目录 / 平台包目录在 /usr/lib 布局下静默失效」(P1)
- 日期:2026-09-14
- 状态:✅ **已修复并验证**(源仓 `cbaf3ad` = 主修;`1d72e8f` = 降级留痕 + 消漂移)
- 触发:开源导出会话提 P1 交接单(`dsh-laijing-github\_交原仓会话_模型目录安装路径缺陷_20260914.md`),复核成立后经用户「确认修改」落地
- 落点:**新文件** `src/web/dsh-install.ts`、`src/web/model-catalog.ts`、`src/web/plugin-compat.ts`、`poc/workspace-scoped-picker/lib/index.js`(+README)、`README.md`、`scripts/verify-dsh-install.mjs`、插件 `business-plugins` **0.3.13** / `workspace-scoped-picker` **0.1.5**
> **TL;DR**|平台有**三处**把「内置 dsh 的安装目录」写死成 `/usr/local/lib/node_modules/@deepseek-ai/dsh`。而 `npm root -g` 的落点**随发行版变**(发行版包管理器装的 Node 常见 `/usr/lib/node_modules`)⇒ 在那种机器上:**厂家目录读成空**(模型设置只剩内置 + 自定义)、**插件兼容性预检的安全网整体失效**(`platformPkgCount()`=0)、**目录选择器 import 即抛**。三处失败**都不报错**(前两处静默降级),所以在本机(`/usr/local`,恰好等于写死值)**怎么也测不出来**。修法 = 新增 `dsh-install.ts` 按序探测(env → dsh 可执行文件解软链 → 常见全局根 → `npm root -g` → 历史默认值),并把**降级改为可观测**。
---
## 一、症状与影响面
| 面向 | 影响 |
|---|---|
| 本平台(`bt-server` 等 `/usr/local` 布局) | **零影响** —— 写死值恰好等于真实落点(实测 `platformPkgCount`=223、厂家目录 39) |
| **`install.sh` 部署的机器**(如测试服 `test106`,`/usr/lib`) | ① **兼容性预检安全网失效** —— 不再拦 semver / 导出符号不兼容的插件(档案 71 白做);② **目录选择器不可用**(`workspace-scoped-picker` import 抛错);③ 厂家目录空 ⇒ 新增厂家只剩「内置 DeepSeek + 自定义」;④ 逃生口未暴露 ⇒ 使用者**无从自救** |
| 开源发布 | 发布阻断项:以「这功能好像没实现」的形式变成 issue,无任何报错可循 |
⚠️ **同一现象、两个根因**:用户当天抱怨的「模型厂商选择怎么这么少、国内一家都没有」——在导出部署上正是本缺陷的症状;在我们服务器上则是**另一个根因**(档案 87 第一版真没接官方目录)。只修一个是不够的。
## 二、三处写死位置(第三处是交接单漏的)
| # | 位置 | 失效表现 | 是否静默 |
|---|---|---|---|
| 1 | `src/web/model-catalog.ts:30` | 厂家目录读成空 ⇒ `GET /api/me/model-providers` → `{"providers":[]}` | **静默**(catch 返回 `[]`) |
| 2 | `src/web/plugin-compat.ts:36` | `platformPkgCount()` = 0 ⇒ 预检退化成「平台包目录不可读」 | **静默**(catch 返回 0) |
| 3 | **`poc/workspace-scoped-picker/lib/index.js:34-35`**(`DEFAULT_SEAM_CANDIDATES`,README 同处引用) | 本模块 import **抛错** ⇒ 实例内目录选择器不可用 | **响亮**(有 `DSH_SEAM_DIRECTORY_PICKER` 逃生口、抛错列已试路径) |
全仓复核:除这三处外**无第四处**(`rg "lib/node_modules"` 排除 `node_modules`/`lib/**`;`Dockerfile` 那两处是删镜像里的 npm,无关)。
## 三、为什么上一轮(以及更早两轮)没发现
1. **失败被设计成静默,还被写成"特性"**:`listCatalogProviders()` 的 catch 注释原文是「目录不存在 ⇒ 返回空数组,**不抛错**:调用方据此降级」⇒ 失败**等价于"这个功能没做"**。
2. **只在一个布局上验收**:写死值在我们 `/usr/local` 恰好是对的 ⇒ 端到端验收(38 家全绿)只证明了「**在这台机器上**对」,判据里没有第二种布局。
3. **前端验收用桩掩盖了后端**:`verify-platform-admin-section.mjs` 给 `/api/me/model-providers` 打了 fixture ⇒ 前端断言全绿,与后端能否读到目录**无关**。
4. **同一个未经检验的假设被复制了三轮**:picker(档案 18 v3,最早)→ `plugin-compat.ts`(档案 71)→ `model-catalog.ts`(档案 87)。**每轮都在同一种布局上验收通过** ⇒ 「同环境反复通过」掩盖了「跨环境从未验证」。
5. **预警信号被当成注释而不是待验证项**:`plugin-compat.ts` 原注释写着「平台内置包根目录(**本部署事实**;env 可覆盖…)」——"本部署事实"就是作者**自知这是假设**的痕迹。
6. **成本极低的一步没做**:当时只要在验收里加一句 `DSHS_DSH_BIN=/nonexistent` 或换个 root,**立刻暴露** ⇒ 不是"发现不了",是**有能力发现却漏了**。
## 四、修复
`src/web/dsh-install.ts`(**只依赖 node 内建** ⇒ 可单独编译、单独在目标机验证):
| 顺序 | 手段 |
|---|---|
| 1 | env `DSH_PACKAGE_DIR` / `DSHS_PACKAGE_DIR` / `DSH_COMPAT_ROOT` / `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` 而非"目录存在",避免撞同名空目录;结果缓存(`resetInstallPathsCache()` 供测试)。
⚠️ **env 名两侧都要认**(交接件只写了导出侧名):源仓是 **`DSHS_DSH_BIN`**(`src/config.ts:239`,本仓前缀 `DSHS_`),导出侧才是 `DSH_USERS_PLATFORM_DSH_BIN`。照抄会让「最可靠那一路」在源仓侧**永不生效**。
📌 补充事实:**我们服务器没设 `DSHS_DSH_BIN`**(`/etc/dshs.env` 无)⇒ 我们走「常见全局根」(`/usr/local` 命中);test106 走 env 那一路(`/usr/lib` 命中)。**两条路都必须留。**
三处调用点改走该模块;picker 因跑在**实例进程**内拿不到平台代码,**自带一份同思路实现**(两处注释互相指认,改一侧要同步另一侧)。
## 五、验证(全部实测)
| 项 | 结果 |
|---|---|
| 本机 `tsc --noEmit` | 0 错误 |
| `npm run verify` 全套(新增 + 5 个既有验收脚本) | 全绿 |
| 单测 | 16 pass / 0 fail |
| 服务器 `bt-server` 回归 | `platformPkgCount`=**223**、厂家目录 **39**(端点回 **38**)、诊断 `readable:true` |
| **测试服 `test106`(`/usr/lib` = 原故障机)** | 「无 env / `DSHS_DSH_BIN` / `DSH_USERS_PLATFORM_DSH_BIN`」**三种姿势全部**解析到 `/usr/lib/node_modules/@deepseek-ai/dsh`(scope 240 项、pi-ai 数据 **39**)。全程只读(只在 `/tmp` 放脚本且已删) |
| **picker 回归**(最要紧:别把本来能用的弄坏) | 在 `/usr/local` 下已装 0.1.5 的 lib `import` **OK** |
| 漂移消除 | 两实例 `workspace-scoped-picker` 实装 **0.1.5** 且含新代码;`business-plugins` **0.3.13** |
## 六、降级留痕(本轮补的第二刀)
原问题能潜伏,根因是**降级不可观测**。现改为:
- `model-catalog`:读不到 ⇒ `console.warn`(**每进程一次**,不刷日志)+ 新增 `catalogDiagnostics()`,并把 `{dir, readable, count}` 透出到 `GET /api/me/model-providers` 的 **`catalog`** 字段;
- `plugin-compat`:`platformPkgCount()=0` ⇒ 同样告警一次(原有 `note` 已在预检结果里可见);
- 插件 **0.3.13**:目录**不可读**(含目录路径)与**目录为空**两种文案**分开如实提示**,不再静默给空列表。
⇒ 「功能没做」与「做了但读不到目录」从此**可区分**。断言见 `verify-dsh-install.mjs` 第 6 组(含「读不到 ≠ 目录为空」的区分)与 `verify-platform-admin-section.mjs` 的 2 条新断言。
## 七、防再犯
1. 凡「读**别人**安装位置 / 版本」的代码 ⇒ 路径**必须走解析层**,且**至少一条单测用假根**。
2. **静默 catch 必须留痕**(告警或透出字段)。
3. **验收判据要显式包含"非本机布局"**(一句 env 注入即可)。
4. 注释里出现「本部署事实 / 目前是 / 暂时」⇒ **当场转成待验证项**。
## 八、仍未做(归开源导出会话)
把三个 env(`DSHS_PACKAGE_DIR` / `DSHS_COMPAT_ROOT` / `DSHS_PI_AI_DATA_DIR`)写进导出物的 `install.sh` + env 样例 + README。**源仓没有 `install.sh`/`deploy/`**(只在导出物里),故本档只在源仓 README 的配置表里登记了它们;回执见 `dsh-laijing-github\_回复源仓会话_安装路径缺陷已修复_20260914.md`。