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 一律写「远程服务器」。
8.7 KiB
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-plugins0.3.13 /workspace-scoped-picker0.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,无关)。
三、为什么上一轮(以及更早两轮)没发现
- 失败被设计成静默,还被写成"特性":
listCatalogProviders()的 catch 注释原文是「目录不存在 ⇒ 返回空数组,不抛错:调用方据此降级」⇒ 失败等价于"这个功能没做"。 - 只在一个布局上验收:写死值在我们
/usr/local恰好是对的 ⇒ 端到端验收(38 家全绿)只证明了「在这台机器上对」,判据里没有第二种布局。 - 前端验收用桩掩盖了后端:
verify-platform-admin-section.mjs给/api/me/model-providers打了 fixture ⇒ 前端断言全绿,与后端能否读到目录无关。 - 同一个未经检验的假设被复制了三轮:picker(档案 18 v3,最早)→
plugin-compat.ts(档案 71)→model-catalog.ts(档案 87)。每轮都在同一种布局上验收通过 ⇒ 「同环境反复通过」掩盖了「跨环境从未验证」。 - 预警信号被当成注释而不是待验证项:
plugin-compat.ts原注释写着「平台内置包根目录(本部署事实;env 可覆盖…)」——"本部署事实"就是作者自知这是假设的痕迹。 - 成本极低的一步没做:当时只要在验收里加一句
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 条新断言。
七、防再犯
- 凡「读别人安装位置 / 版本」的代码 ⇒ 路径必须走解析层,且至少一条单测用假根。
- 静默 catch 必须留痕(告警或透出字段)。
- 验收判据要显式包含"非本机布局"(一句 env 注入即可)。
- 注释里出现「本部署事实 / 目前是 / 暂时」⇒ 当场转成待验证项。
八、仍未做(归开源导出会话)
把三个 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。