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 一律写「远程服务器」。
92 lines
8.7 KiB
Markdown
92 lines
8.7 KiB
Markdown
# 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`。
|