# 发布说明 v0.4.1(代码审查收口) [← 返回 README](../README.zh.md) | 相关:[v0.4.0 发布说明](v0.4.0.md) **发布日期**:2026-09-16 | **区间**:`v0.4.0 → v0.4.1` **规模**:6 commits + 收尾文档;源码 8 文件 +570 / −213(含全量测试与文档 20 文件 +1134 / −237); 新增源码模块 1 个(`src/dsh-paths.ts`);测试套件仍为 6 套,新增 13 个场景 v0.4.1 是一次**纯修复与可见性**版本:把 v0.3.1 以来全部 TypeScript 变更的代码审查 (`docs/code-review/ts-review-since-v0.3.1.zh.md`)逐条落地。**配置文件格式没有任何变化**, 唯一可见的输出变化是 CLI 写出的 JSON 条目现在带显式 `type`。 --- ## 亮点速览 | 主题 | 一句话 | |---|---| | ⚠️ 项目侧压制不再自伤 | 只 deny「本轮真会全局装载」的名字,不再误伤项目自己的工具或宿主 patch | | `$DSH_HOME` 全链路生效 | 用户层三文件、全局诊断、CLI 读写路径统一走 `src/dsh-paths.ts` | | 坏条目不再被顺手删掉 | `mcpServers` 里的非对象条目跨读-改-写原样保留,可被指名删除 | | yml 写入并发安全 | 新增 `updateManagedRows`:读-改-写全在锁内,`EACCES` 不再被吞成「文件缺失」 | | 遮蔽诊断按层归因 | 纯用户层冲突只写全局诊断,零配置项目不再被动创建 `.mcp-diag.json` | | 告警按变化门控 | 文件错误、坏条目、`name-taken` 只在集合变化时告警一次 | | CLI 提示双真相 | `add` 提示新行会不会被遮蔽,`remove` 说明同名定义接管 | | `DSH_MCP_PROFILE` 校验 | `../../somewhere` 不再被拼进 profiles 目录 | --- ## 修复 ### 项目侧压制只 deny 真会全局装载的名字(`src/registry.ts`) 两个静默自伤场景:用户层 `~/.dsh/mcp.yml` 里的 `disabled: true` 占名行,以及被宿主全局 patch 行顶掉(`name-taken`)的用户层行——两者都**不产生全局实例**,却照旧进了该项目的 压制集合。压制按 `mcp__<原名>__` 前缀展开,于是 deny 打到了项目自己的服务器(它的生效名 从未改名)或宿主 patch 实例上,而快照仍报 `fiberPhase: "active"`、没有 `skipReason`。 修复:把用户层合并结果与「宿主已占名」集合前移到项目循环之前,`suppressedGlobals` 只收真正会全局装载的名字;`sweepRestrictions` 再加一道「该名字当前确有全局实例」的守卫。 ### `$DSH_HOME` 重定位不再让用户层整体消失(`src/dsh-paths.ts`) 此前用户层路径、全局诊断与 CLI 各处都硬编码 `homedir()/.dsh`,与 README 承诺的 「dshHome 默认 `%USERPROFILE%\.dsh`,设置了 `DSH_HOME` 则用其值」相矛盾。重定位后 用户层三文件全部指向不存在的路径,而「文件缺失」是合法的零配置状态,所以**没有任何告警**。 新模块 `src/dsh-paths.ts` 成为唯一解析口径:`dshHomeDir(home, env)` / `dshHomeFor()` / `userLayerPathsIn()` / `profileMcpJsonFile()`。注入的路径(registry 的 `userLayerPaths`、 CLI 的 `deps.home`)仍然优先于环境变量,测试才能与本机真实配置隔离。 ### 其余修复 - **`remove` 说明同名接管**:yml 与 json 同时有同名行时,仍只删首个命中,但会打印 接下来由哪个文件的定义接管生效;再执行一次才真正清空。 - **坏条目跨写入保留**:`readJsonServers`/`updateJsonServers` 不再过滤非对象条目, 手写的 `"legacy": "node x.js"` 不会被无关的 `add`/`remove` 永久删除,判重能看到它占的名, `dsh-mcp remove legacy` 可以清掉它。 - **yml 写路径锁内读-改-写**:`updateManagedRows(path, mutate, options)` 取代「锁外预读 + 锁内整体替换」,并发两个 `add` 不再互相丢行;`createIfMissing` 收紧为只接受 `ENOENT`, `EACCES` 与损坏的受管块不再被当成「文件缺失」而被覆盖。 - **`DSH_MCP_PROFILE` 先校验再拼路径**:不匹配 `^[A-Za-z0-9][A-Za-z0-9._-]*$`(含显式的 `.` / `..`)时按「解析不出 profile」降级并告警一次,不再读到 profiles 目录之外。 - **`serverView` 的用户层作用域**:报 `global` + 层文件路径,与快照分区口径一致 (原先固定报 `workspace` + 项目根)。 --- ## 变更 - **遮蔽诊断按层归因**:`IdentityShadow` 带上失败方与胜出方的来源,新增 `shadowedGlobal` 桶记录用户层之间的精确同名遮蔽(此前零可见性)。只涉及用户层的冲突在 `/.mcp-diag.json` 全局报一次;项目侧只保留至少一方是项目层行的条目, 零配置项目不再因为两个用户层文件互相冲突而被创建 `.dsh/.mcp-diag.json`。 - **告警按变化门控**:文件级错误、坏条目、`name-taken` 集合只在集合本身变化时告警, 不再每轮对账每项目重刷;`skipReason` 照旧每轮写入,快照仍能查到原因。 - **`name-taken` 文案**改为「宿主全局 patch 行(bundle 层或 profile patch 层)」, 与 `globalNames()` 实际收集的范围一致。 - **CLI 写出的 JSON 条目带显式 `type`**(`stdio` / `http`),便于要求该键的生态工具消费; 本插件自己的读取器仍然可以推断。 - **`add` 写入后复核**:新行被更高优先层按名字或按同服务遮蔽时提示「不会装载」, 同作用域另一方言文件也有定义时提示条目数。 - **JSON 读取失败带 errno**(`读取失败(EACCES)`),错误串不含文件内容。 - **`rowNameOf` 移入 `src/model.ts`** 由装载器与 CLI 共用:受管行 id 优先于 `config.serverName`。CLI 此前口径相反,id 与 `config.serverName` 不一致的行 没法用装载器实际使用的名字删除。 - **内部清理**:`reconcileAll` 的项目循环体抽成 `scanProject`;逐项目 Map 按本轮 known keys 剪枝;`planProjectChanges` 的 O(n²) 查找改 Map;`activeProfile()` 经 `Promise.resolve` 兜住同步抛错;CLI 消除 `.dsh` / `mcp.yml` 字面量重复,`getMissMessage` 改用实际查过的层。 --- ## 文档 - `docs/guide/layers.md` / `.zh.md`:补记 v0.4.0 起遗留 `.mcp.json` 层的三处行为变化—— 条目级显式 `cwd` 现在生效、DSH 透传键(`toolCallTimeoutMs`/`failOnStartupError`/`reconnect`) 现在生效、`disabled: true` 从「静默跳过不占名」改为「占名不装载」。 - `docs/guide/cli.md` / `.zh.md`:`-f`(`--format`)与 `-p`(`--profile`)自 v0.4.0 起是本 CLI 的保留短名,会吞掉下一个词元;要传给被 spawn 命令的同名参数写在 `--` 之后。 - `CHANGELOG.md`:补记快照消费方须知——全局层分区的 `project` 字段自 v0.4.0 起是**层文件 自身所在目录**(`mcp.yml`/`mcp.json` 为 dshHome,profile 文件为 `profiles/`), v0.3.1 报的是 `.dsh` 的父目录(家目录)。`dsh-skill-mcp-panel` 等消费方按「层自己的目录」读。 --- ## 升级 无需改动任何配置文件,无破坏性变更: - 设置了 `DSH_HOME` 的机器,用户层会从**它下面**读取(这是 README 一直承诺的行为, 此前的静默失效是缺陷)。若之前依赖「无论 `DSH_HOME` 如何都读 `%USERPROFILE%\.dsh`」, 把文件移到 `$DSH_HOME` 下即可。 - CLI 新写出的 JSON 条目多一个 `type` 键;既有文件不会被改写。 - 快照 / 诊断的字段只有新增(`shadowedGlobal`、`IdentityShadow.source` 与 `winnerSource`)。 安全边界不变:`stdio` 行仍在 dsh 宿主进程里 spawn `command`,项目与用户层配置文件都是 可执行代码载体,只在可信项目里启用。