dsh-project-mcp-manager 0.4.3 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +61 -14
- package/docs/README.zh.md +46 -12
- package/docs/code-review/ts-review-7e0088d-to-804662f.zh.md +277 -0
- package/docs/code-review/ts-review-v0.4.3-to-v0.6.0.zh.md +366 -0
- package/docs/design/adaptation-dsh-0.1.2-rc1.md +1 -1
- package/docs/design/adaptation-dsh-0.1.5-rc1.md +1 -1
- package/docs/design/adaptation-dsh-0.1.5-rc2.md +59 -0
- package/docs/design/proposal-runtime-robustness-and-json-interop.md +380 -0
- package/docs/guide/cli.md +33 -2
- package/docs/guide/cli.zh.md +24 -2
- package/docs/guide/env-expansion.md +3 -1
- package/docs/guide/env-expansion.zh.md +2 -1
- package/docs/guide/format.md +49 -2
- package/docs/guide/format.zh.md +41 -2
- package/docs/guide/layers.md +46 -5
- package/docs/guide/layers.zh.md +32 -3
- package/docs/releases/v0.6.0.md +120 -0
- package/lib/cli.js +424 -35
- package/lib/dsh-paths.js +5 -1
- package/lib/index.js +5 -3
- package/lib/json-file.js +143 -45
- package/lib/json-write.js +47 -28
- package/lib/model.js +269 -20
- package/lib/registry.js +881 -252
- package/lib/service.js +10 -0
- package/lib/status.js +53 -6
- package/package.json +9 -1
package/README.md
CHANGED
|
@@ -3,10 +3,22 @@
|
|
|
3
3
|
English | [中文](docs/README.zh.md)
|
|
4
4
|
|
|
5
5
|
A project-level MCP auto-loading plugin for DSH: write MCP server configs in
|
|
6
|
-
`<projectRoot>/.dsh/mcp.yml` and they are mounted
|
|
7
|
-
official `@deepseek-ai/dsh-mcp-client`) whenever a dsh
|
|
8
|
-
project. Changes to the file hot-reload into the running
|
|
9
|
-
visibility is scoped per session cwd. No UI — core
|
|
6
|
+
`<projectRoot>/.dsh/mcp.yml` or `.dsh/mcp.json` and they are mounted
|
|
7
|
+
automatically (via the official `@deepseek-ai/dsh-mcp-client`) whenever a dsh
|
|
8
|
+
session opens in that project. Changes to the file hot-reload into the running
|
|
9
|
+
dsh process, and tool visibility is scoped per session cwd. No UI — core
|
|
10
|
+
functionality only.
|
|
11
|
+
|
|
12
|
+
**Capability boundary**: this plugin = official `@deepseek-ai/dsh-mcp-client`
|
|
13
|
+
transports + six-layer source governance + per-session isolation.
|
|
14
|
+
**Transport types are decided by the official client**; this plugin does not
|
|
15
|
+
implement MCP transports.
|
|
16
|
+
|
|
17
|
+
If this plugin is useful, a GitHub
|
|
18
|
+
[star](https://github.com/wldxiaobai/dsh-project-mcp-manager) is appreciated.
|
|
19
|
+
Bugs, host mismatches, or ideas belong in
|
|
20
|
+
[Issues](https://github.com/wldxiaobai/dsh-project-mcp-manager/issues) — even a
|
|
21
|
+
short report helps.
|
|
10
22
|
|
|
11
23
|
## Documentation
|
|
12
24
|
|
|
@@ -21,16 +33,21 @@ Feature documentation lives in `docs/`, English and Chinese side by side:
|
|
|
21
33
|
its diagnostics.
|
|
22
34
|
- [CLI `dsh-mcp`](docs/guide/cli.md) — scopes, write formats, ownership contract.
|
|
23
35
|
|
|
24
|
-
Design and release records (Chinese): [dsh 0.1.5-rc.
|
|
36
|
+
Design and release records (Chinese): [dsh 0.1.5-rc.2 adaptation](docs/design/adaptation-dsh-0.1.5-rc2.md) ·
|
|
37
|
+
[dsh 0.1.5-rc.1 adaptation](docs/design/adaptation-dsh-0.1.5-rc1.md) ·
|
|
25
38
|
[dsh 0.1.2-rc.1 adaptation](docs/design/adaptation-dsh-0.1.2-rc1.md) ·
|
|
26
39
|
[JSON config layer proposal](docs/design/proposal-json-mcp-config.md) ·
|
|
40
|
+
[Runtime robustness & JSON interop proposal](docs/design/proposal-runtime-robustness-and-json-interop.md) ·
|
|
41
|
+
[v0.6.0 release notes](docs/releases/v0.6.0.md) ·
|
|
27
42
|
[v0.4.3 release notes](docs/releases/v0.4.3.md) ·
|
|
28
43
|
[v0.4.2 release notes](docs/releases/v0.4.2.md) ·
|
|
29
44
|
[v0.4.1 release notes](docs/releases/v0.4.1.md) ·
|
|
30
45
|
[v0.4.0 release notes](docs/releases/v0.4.0.md) ·
|
|
31
46
|
[v0.3.1 release notes](docs/releases/v0.3.1.md).
|
|
32
47
|
|
|
33
|
-
Code review records (Chinese): [TypeScript changes since v0.3.1](docs/code-review/ts-review-since-v0.3.1.zh.md)
|
|
48
|
+
Code review records (Chinese): [TypeScript changes since v0.3.1](docs/code-review/ts-review-since-v0.3.1.zh.md) ·
|
|
49
|
+
[v0.4.3 to v0.6.0](docs/code-review/ts-review-v0.4.3-to-v0.6.0.zh.md) ·
|
|
50
|
+
[7e0088d to 804662f (fix follow-up)](docs/code-review/ts-review-7e0088d-to-804662f.zh.md).
|
|
34
51
|
|
|
35
52
|
## Installation (mount into a profile)
|
|
36
53
|
|
|
@@ -57,7 +74,7 @@ dsh plugin --profile web add dsh-project-mcp-manager@latest
|
|
|
57
74
|
|
|
58
75
|
# Install a specific version (check available versions with
|
|
59
76
|
# npm view dsh-project-mcp-manager versions)
|
|
60
|
-
dsh plugin --profile web add dsh-project-mcp-manager@0.
|
|
77
|
+
dsh plugin --profile web add dsh-project-mcp-manager@0.6.0
|
|
61
78
|
```
|
|
62
79
|
|
|
63
80
|
**Option 2: install directly with pnpm** (equivalent to option 1):
|
|
@@ -84,7 +101,7 @@ pnpm add link:<path-to-your-dsh-mcp-project-source> # e.g. D:\dev\dsh-mcp-proj
|
|
|
84
101
|
> trigger the bundle reconcile.
|
|
85
102
|
|
|
86
103
|
**Upgrading / pinning versions**: re-run the `add` command from option 1 with
|
|
87
|
-
the desired version suffix — `@latest` upgrades to the newest release, `@0.
|
|
104
|
+
the desired version suffix — `@latest` upgrades to the newest release, `@0.6.0`
|
|
88
105
|
pins to a specific version.
|
|
89
106
|
|
|
90
107
|
## Build & test
|
|
@@ -104,8 +121,12 @@ pnpm test # node test/test-model.mjs / test-mcp-file / test-json-file /
|
|
|
104
121
|
- **Mounting**: each `(project, serverName)` pair in the project layers mounts
|
|
105
122
|
one `@deepseek-ai/dsh-mcp-client` instance (`ctx.plugin`) on the host ctx and
|
|
106
123
|
registers it into the global tool layer; multiple sessions inside the same
|
|
107
|
-
project share a single connection. **
|
|
108
|
-
|
|
124
|
+
project share a single connection. **Project-layer fibers are created only
|
|
125
|
+
for projects with a live session or the process cwd**; after the last
|
|
126
|
+
session leaves (and the project is not cwd) servers unmount following a
|
|
127
|
+
5 minute grace while the catalog entry and watcher remain. **Every
|
|
128
|
+
user-layer row mounts exactly one instance** (global, independent of the
|
|
129
|
+
number of projects) — see
|
|
109
130
|
[configuration sources and layers](docs/guide/layers.md).
|
|
110
131
|
- **Hot reload**: chokidar watches each project root (depth 2, ignoring
|
|
111
132
|
node_modules/.git/.hg/.svn), but only edits to the **exact** config files of
|
|
@@ -113,10 +134,12 @@ pnpm test # node test/test-model.mjs / test-mcp-file / test-json-file /
|
|
|
113
134
|
`<projectRoot>/.dsh/mcp.json` and `<projectRoot>/.mcp.json` — trigger a full
|
|
114
135
|
reconciliation after a 150 ms debounce: added rows are mounted, removed rows
|
|
115
136
|
are unmounted, and config changes are remounted. A second watcher covers the
|
|
116
|
-
user layer as
|
|
117
|
-
`~/.dsh/mcp.json
|
|
118
|
-
|
|
119
|
-
|
|
137
|
+
user layer as **exact file paths** — `~/.dsh/mcp.yml`,
|
|
138
|
+
`~/.dsh/mcp.json`, `~/.dsh/profiles/<active profile>/mcp.json`, and
|
|
139
|
+
`$DSH_HOME/dsh-mcp.json` (the other plugin's global store; watched only so
|
|
140
|
+
creating it can be diagnosed, never mounted). chokidar v5 can deliver an
|
|
141
|
+
event for a watched missing file when it is created, as long as its parent
|
|
142
|
+
directory exists — never the home directory at large.
|
|
120
143
|
- **Profile name resolution**: derived from the loader root include's
|
|
121
144
|
`config.path` (`~/.dsh/profiles/<name>/cordis.yml`) or `ctx.baseUrl`, and
|
|
122
145
|
overridable with `DSH_MCP_PROFILE=<name>`; when it cannot be resolved the
|
|
@@ -151,3 +174,27 @@ longer fanned out per project. Lines that fail to mount or are invalid are
|
|
|
151
174
|
skipped with a warning and do not affect other servers. Claude user-state
|
|
152
175
|
monoliths such as `~/.claude.json` (mixing credentials with project history)
|
|
153
176
|
are **no longer read at all** as of v0.4.0.
|
|
177
|
+
|
|
178
|
+
## Coexistence with other MCP manager plugins
|
|
179
|
+
|
|
180
|
+
This plugin and `@wingsky-1/dsh-mcp-manager` both auto-load per-project MCP
|
|
181
|
+
servers, but they do **not** share a file format:
|
|
182
|
+
|
|
183
|
+
1. **Project files are mutually incompatible.** This plugin reads
|
|
184
|
+
`{ mcpServers: { … } }` in `<projectRoot>/.dsh/mcp.json`. The other plugin
|
|
185
|
+
stores `{ version, servers: [] }` at the same path. A missing `mcpServers`
|
|
186
|
+
key is a legal empty layer here, so the other format would otherwise look
|
|
187
|
+
like "I configured it but nothing happens". The loader now writes a
|
|
188
|
+
diagnostic naming that format and suggesting `mcpServers` or
|
|
189
|
+
`.dsh/mcp.yml`. The same hint applies to `~/.dsh/dsh-mcp.json`. If that
|
|
190
|
+
file already uses this plugin's `mcpServers` dialect, the diagnostic tells
|
|
191
|
+
you to move the object into `mcp.json` — it is still not loaded from the
|
|
192
|
+
other plugin's filename.
|
|
193
|
+
2. **The same `serverName` can be started twice** (once by each plugin).
|
|
194
|
+
stdio servers may contend for ports or exclusive resources.
|
|
195
|
+
3. **Prefer one plugin per project**, or keep this plugin on `.dsh/mcp.yml`
|
|
196
|
+
and the other on `.dsh/mcp.json`.
|
|
197
|
+
|
|
198
|
+
`globalNames()` only sees official loader patch rows, not tools registered by
|
|
199
|
+
the other plugin at runtime, so rename-to-avoid-collision does **not** cover
|
|
200
|
+
that other instance.
|
package/docs/README.zh.md
CHANGED
|
@@ -2,11 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
[English](../README.md) | 中文
|
|
4
4
|
|
|
5
|
-
项目级 MCP 自动加载插件:在项目根 `<projectRoot>/.dsh/mcp.yml`
|
|
6
|
-
服务器配置,在该项目开启 dsh 会话时自动装载(经官方
|
|
5
|
+
项目级 MCP 自动加载插件:在项目根 `<projectRoot>/.dsh/mcp.yml` 或
|
|
6
|
+
`.dsh/mcp.json` 写入 MCP 服务器配置,在该项目开启 dsh 会话时自动装载(经官方
|
|
7
7
|
`@deepseek-ai/dsh-mcp-client`),文件改动热重载到运行中的 dsh 进程,并按
|
|
8
8
|
会话 cwd 控制工具可见性。无 UI,仅具备核心功能。
|
|
9
9
|
|
|
10
|
+
**能力边界**:本插件 = 官方 `@deepseek-ai/dsh-mcp-client` 传输 + 六层来源治理 +
|
|
11
|
+
按会话隔离。**传输类型由官方 client 决定**;本插件不实现任何 MCP 传输。
|
|
12
|
+
|
|
13
|
+
若这个插件对你有帮助,欢迎给仓库点一颗
|
|
14
|
+
[star](https://github.com/wldxiaobai/dsh-project-mcp-manager)。遇到问题、宿主
|
|
15
|
+
不适配或有想法,请开
|
|
16
|
+
[Issue](https://github.com/wldxiaobai/dsh-project-mcp-manager/issues)——哪怕几句话也很有用。
|
|
17
|
+
|
|
10
18
|
## 文档
|
|
11
19
|
|
|
12
20
|
功能说明已拆分到 `docs/`,中英双版并存:
|
|
@@ -18,16 +26,21 @@
|
|
|
18
26
|
- [`${VAR}` 展开](guide/env-expansion.zh.md)——装载时插值与对应诊断。
|
|
19
27
|
- [CLI `dsh-mcp`](guide/cli.zh.md)——作用域、写入格式与独占契约。
|
|
20
28
|
|
|
21
|
-
设计与发布记录(中文):[dsh 0.1.5-rc.
|
|
29
|
+
设计与发布记录(中文):[dsh 0.1.5-rc.2 适配记录](design/adaptation-dsh-0.1.5-rc2.md) ·
|
|
30
|
+
[dsh 0.1.5-rc.1 适配记录](design/adaptation-dsh-0.1.5-rc1.md) ·
|
|
22
31
|
[dsh 0.1.2-rc.1 适配记录](design/adaptation-dsh-0.1.2-rc1.md) ·
|
|
23
32
|
[JSON 配置层设计提案](design/proposal-json-mcp-config.md) ·
|
|
33
|
+
[运行时稳健性与 JSON 互通提案](design/proposal-runtime-robustness-and-json-interop.md) ·
|
|
34
|
+
[v0.6.0 发布说明](releases/v0.6.0.md) ·
|
|
24
35
|
[v0.4.3 发布说明](releases/v0.4.3.md) ·
|
|
25
36
|
[v0.4.2 发布说明](releases/v0.4.2.md) ·
|
|
26
37
|
[v0.4.1 发布说明](releases/v0.4.1.md) ·
|
|
27
38
|
[v0.4.0 发布说明](releases/v0.4.0.md) ·
|
|
28
39
|
[v0.3.1 发布说明](releases/v0.3.1.md)。
|
|
29
40
|
|
|
30
|
-
代码审查记录(中文):[v0.3.1 以来 TypeScript 变更审查](code-review/ts-review-since-v0.3.1.zh.md)
|
|
41
|
+
代码审查记录(中文):[v0.3.1 以来 TypeScript 变更审查](code-review/ts-review-since-v0.3.1.zh.md) ·
|
|
42
|
+
[v0.4.3 至 v0.6.0](code-review/ts-review-v0.4.3-to-v0.6.0.zh.md) ·
|
|
43
|
+
[7e0088d 至 804662f(审查落地复查)](code-review/ts-review-7e0088d-to-804662f.zh.md)。
|
|
31
44
|
|
|
32
45
|
## 安装(挂载到 profile)
|
|
33
46
|
|
|
@@ -50,7 +63,7 @@ npm install -g deepseek-ai/dsh # 或从 GitHub 源码安装
|
|
|
50
63
|
dsh plugin --profile web add dsh-project-mcp-manager@latest
|
|
51
64
|
|
|
52
65
|
# 安装指定版本(版本号可先 npm view dsh-project-mcp-manager versions 查看)
|
|
53
|
-
dsh plugin --profile web add dsh-project-mcp-manager@0.
|
|
66
|
+
dsh plugin --profile web add dsh-project-mcp-manager@0.6.0
|
|
54
67
|
```
|
|
55
68
|
|
|
56
69
|
**方式二:直接 pnpm 安装**(与方式一等价):
|
|
@@ -71,11 +84,11 @@ pnpm add link:<你的 dsh-mcp-project 源码目录> # 例如 D:\dev\dsh-mcp-pr
|
|
|
71
84
|
> **dsh ≥ 0.1.2 注意**:插件是否生效取决于 profile 的 `dsh.profile.bundles`,
|
|
72
85
|
> 而单纯 `pnpm add link:` **不会**把包写进 bundles。方式一/方式二会自动补齐;
|
|
73
86
|
> 若你手写了 pnpm 命令,请再跑一次任意 `dsh plugin --profile web list`(或
|
|
74
|
-
>
|
|
75
|
-
> bundle reconcile。
|
|
87
|
+
> 用 `dsh --profile web --dump-config` 检查合成结果里有没有
|
|
88
|
+
> `dsh-project-mcp-manager` 行)触发 bundle reconcile。
|
|
76
89
|
|
|
77
90
|
**升级/锁定版本**:重跑方式一的 `add` 命令并带上目标版本后缀——`@latest`
|
|
78
|
-
升级到最新,`@0.
|
|
91
|
+
升级到最新,`@0.6.0` 锁定到指定版本。
|
|
79
92
|
|
|
80
93
|
## 构建与测试
|
|
81
94
|
|
|
@@ -91,15 +104,19 @@ pnpm test # node 直跑 test/ 下六个 .mjs(model / mcp-file / json-
|
|
|
91
104
|
向上找最近的含 `.git` 的祖先目录作为项目根(无 `.git` 时退回目录本身)。
|
|
92
105
|
- **装载**:项目层每个 `(项目, serverName)` 在宿主 ctx 上装载一个
|
|
93
106
|
`@deepseek-ai/dsh-mcp-client` 实例(`ctx.plugin`),注册进全局工具层,同一
|
|
94
|
-
|
|
107
|
+
项目内多会话共享同一连接。**项目层只给有活跃会话或进程 cwd 的项目发起装载**;
|
|
108
|
+
最后一次会话离开且该项目不是 cwd 后,宽限 5 分钟再卸载服务器,条目与文件监听
|
|
109
|
+
保留。**用户层每行只装载一个实例**(全局,与项目数无关)
|
|
95
110
|
——详见[配置来源与分层](guide/layers.zh.md)。
|
|
96
111
|
- **热重载**:chokidar 监听各项目根(depth 2,忽略 node_modules/.git/.hg/
|
|
97
112
|
.svn),但只有**已知项目根的精确配置文件**(`<projectRoot>/.dsh/mcp.yml`、
|
|
98
113
|
`<projectRoot>/.dsh/mcp.json` 与 `<projectRoot>/.mcp.json`)的改动经 150ms
|
|
99
114
|
防抖触发全量对账:新增行装载、删除行卸载、配置变化重装。另有独立 watcher 以
|
|
100
|
-
**精确文件路径**监听用户层:`~/.dsh/mcp.yml`、`~/.dsh/mcp.json
|
|
101
|
-
`~/.dsh/profiles/<当前 profile>/mcp.json
|
|
102
|
-
|
|
115
|
+
**精确文件路径**监听用户层:`~/.dsh/mcp.yml`、`~/.dsh/mcp.json`、
|
|
116
|
+
`~/.dsh/profiles/<当前 profile>/mcp.json`,以及 `$DSH_HOME/dsh-mcp.json`
|
|
117
|
+
(对方插件的全局存储;只监听以便创建时能立刻诊断,从不装载)。chokidar v5
|
|
118
|
+
对被监听的缺失文件能在其创建时补发事件,前提是父目录已存在——不监听家目录
|
|
119
|
+
整体。
|
|
103
120
|
- **profile 名解析**:从 loader 根 include 的 `config.path`
|
|
104
121
|
(`~/.dsh/profiles/<name>/cordis.yml`)或 `ctx.baseUrl` 推导,可用
|
|
105
122
|
`DSH_MCP_PROFILE=<name>` 覆盖;解析不出时不读 profile 层(其余层照常)。
|
|
@@ -123,3 +140,20 @@ pnpm test # node 直跑 test/ 下六个 .mjs(model / mcp-file / json-
|
|
|
123
140
|
连接,所有项目可见),不再按项目 fan-out。装载失败/配置无效行仅告警跳过,不影响
|
|
124
141
|
其他服务器。`~/.claude.json` 这类 Claude 用户态单体文件(混存凭据与项目历史)
|
|
125
142
|
自 v0.4.0 起**完全不再读取**。
|
|
143
|
+
|
|
144
|
+
## 与同类插件共存
|
|
145
|
+
|
|
146
|
+
本插件与 `@wingsky-1/dsh-mcp-manager` 都做按项目自带 MCP,但**文件格式互不兼容**:
|
|
147
|
+
|
|
148
|
+
1. **项目文件格式互不兼容。** 本插件读 `<projectRoot>/.dsh/mcp.json` 里的
|
|
149
|
+
`{ mcpServers: { … } }`;对方在同一路径存 `{ version, servers: [] }`。缺
|
|
150
|
+
`mcpServers` 在本插件是合法空层,对方格式会表现为「我配了但没生效」。装载器
|
|
151
|
+
现在会写一条诊断,指认该格式并建议改用 `mcpServers` 或 `.dsh/mcp.yml`。
|
|
152
|
+
全局 `~/.dsh/dsh-mcp.json` 同样提示;若该文件已经是本插件的 `mcpServers`
|
|
153
|
+
方言,诊断会建议把对象搬到 `mcp.json`——仍不会从对方文件名装载。
|
|
154
|
+
2. **同名服务器会被两个插件各启动一次**,stdio 可能互相抢端口或独占资源。
|
|
155
|
+
3. **建议同一项目只启用一个**,或让两者分居 `mcp.yml`(本插件)与
|
|
156
|
+
`.dsh/mcp.json`(对方)。
|
|
157
|
+
|
|
158
|
+
`globalNames()` 只读官方 loader patch 行,看不到对方运行时注册的工具,因此
|
|
159
|
+
「改名避让」不会覆盖对方实例。
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
# 代码审查:7e0088d 至 804662f 的 TypeScript 变更
|
|
2
|
+
|
|
3
|
+
[← 返回 README](../README.zh.md) | 相关:[上一轮审查(v0.4.3 至 v0.6.0)](ts-review-v0.4.3-to-v0.6.0.zh.md) ·
|
|
4
|
+
[v0.6.0 发布说明](../releases/v0.6.0.md) ·
|
|
5
|
+
[运行时稳健性与 JSON 互通提案](../design/proposal-runtime-robustness-and-json-interop.md)
|
|
6
|
+
|
|
7
|
+
> 本文所列 M1–M3 / L1–L3 / L5–L6 已在 `dcdcdab` 及后续 commit 落地(健康配额恢复清零、
|
|
8
|
+
> 查询面进 enqueue 走内存目录、同世代 0 工具去抖、`warnGates` 剪枝、`schemaToolId` 统一、
|
|
9
|
+
> idle 进 `summary.idle`、`status --scope profile`、诊断锁超时告警),随 v0.6.0 一并发布。
|
|
10
|
+
> L4(显式 `type:"stdio"` 仍可带 `url`)与 L7(`timer.unref()`)按审查建议保持现状。
|
|
11
|
+
|
|
12
|
+
**审查日期**:2026-09-13 | **区间**:`7e0088d`…`804662f`(含两端;git 口径
|
|
13
|
+
`7e0088d^..804662f`)| 分支 `feat/runtime-robustness-and-json-interop`
|
|
14
|
+
**范围**:`src/*.ts`——6 文件 `+334 / −147`(`registry.ts` +256、`cli.ts` +145、
|
|
15
|
+
`model.ts` +42、`json-file.ts` +23、`service.ts` +13、`status.ts` +2)。本区间
|
|
16
|
+
**没有**新功能,是上一轮审查 H/M/L 的落地批次;`src/index.ts` / `mcp-file.ts` /
|
|
17
|
+
`json-write.ts` / `dsh-paths.ts` / `project-root.ts` 无改动。
|
|
18
|
+
**方法**:通读含两端的 `.ts` diff 与当前代码,对照上一轮发现、提案 A1/B3 与
|
|
19
|
+
`test/*.mjs`;行号以审查时 HEAD(`src/` 与 `804662f` 一致)为准。不重审 v0.4.3→v0.6.0
|
|
20
|
+
的功能集本身。
|
|
21
|
+
|
|
22
|
+
> 中文「A 到 B」含两端。git `A..B` 不含 A,故统计与 log 用 `7e0088d^..804662f`。
|
|
23
|
+
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## 1. 变更概览
|
|
27
|
+
|
|
28
|
+
18 个 commit,按依赖从 H → M → L,与上一轮「修复优先级建议」一致:
|
|
29
|
+
|
|
30
|
+
| commit | 关闭 | 内容 |
|
|
31
|
+
|---|---|---|
|
|
32
|
+
| `7e0088d` | H1 | 健康自愈按 fiber 世代清 `everHadTools`;`snapshot()` 不再入队 `reconcileAll`;`schemaToolId` 改 `name` 优先(L2) |
|
|
33
|
+
| `c3dcf7c` | H2 | 摘要按配置目录计 `rows`;idle 不覆盖已有 skip;give-up 进 `skipReasons` / `unhealthy` |
|
|
34
|
+
| `1457ea6` | M2 | `import` 同名 skip 放进 `updateJsonServers` / `updateManagedRows` 锁内 |
|
|
35
|
+
| `0ff8adf` | M1 | 指纹纳入现取 profile 名与排序后的宿主全局名 |
|
|
36
|
+
| `dcabbed` | M6 | `writeDiagAt` 走 `withPatchLock` |
|
|
37
|
+
| `a3bf9a3` | M3 | `canonicalConfig` 剥掉顶层 `tools`;仍挂着的行就地换 `row`/`source` |
|
|
38
|
+
| `9129f4b` | M4 | 预算回到阈值下时 `warnGated(..., "", ...)` 清门控 |
|
|
39
|
+
| `a6671e9` | M5 | `patchRowToView` / `get` / `list` 展示 `tools.allow`/`deny` |
|
|
40
|
+
| `f094587` | M7 | 无 `type`/`transport` 时 `command`+`url`/`httpUrl` 拒绝 |
|
|
41
|
+
| `34ee304` | M8 | 用户层 watcher 监听 `$DSH_HOME/dsh-mcp.json` |
|
|
42
|
+
| `1fee116` | L1 | 传输别名表 `Object.create(null)` + `Object.hasOwn` |
|
|
43
|
+
| `4914476` | L3 | 非法工具 glob 条目告警(`invalidToolGlobs`) |
|
|
44
|
+
| `d29bde2` | L6/L7 | 指纹签名只算一遍;`prunePerProjectState` 收 `idleSince` / `lastScanDesired` |
|
|
45
|
+
| `bb47ae8` | L8 | `serverView` 返回 `McpServerRuntimeView` |
|
|
46
|
+
| `781ad28` | L10 | `status --scope` 只打印对应层诊断 |
|
|
47
|
+
| `ea24c81` | L11 | 对方文件名里出现 `mcpServers` 给改路径提示 |
|
|
48
|
+
| `cb8fac5` | L12 | `reconnect` 对象字面量缩进 |
|
|
49
|
+
| `804662f` | L4/L9 | 文档写明生效名 glob 与 `add` 无过滤开关(无 `.ts` 逻辑) |
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## 2. 总体评价
|
|
54
|
+
|
|
55
|
+
这是一轮对得上审查单的修复:每条上一轮发现几乎都有对应 commit 和测试,没有把无关重构塞进来。值得肯定的点:
|
|
56
|
+
|
|
57
|
+
- **H1 的主因已拆开**:新 fiber 的 0 工具窗口不再立刻拆连接(测试断言
|
|
58
|
+
`new fiber without tools does not remount`);`snapshot()` 连续调用不再推高
|
|
59
|
+
`debugReconcileCount`,B3 轮询不会再当巡检驱动。
|
|
60
|
+
- **H2 状态机与摘要对齐**:`summarizeScope` 用 `lastScanDesired` / `lastGlobalDesired`
|
|
61
|
+
计行;idle 只在没有既有标记时落 `skipReason: "idle"`(`env-missing` 能活过宽限卸载);
|
|
62
|
+
give-up 即使 fiber 仍 `active` 也进 `unhealthy`,`dsh-mcp status` 打得出名字。
|
|
63
|
+
- **M2/M6 锁口径与 v0.4.1 的 add 一致**:import skip 在锁内读现网名字;诊断 RMW 进同一把
|
|
64
|
+
项目锁。并发 import 测试期望只有一条胜出。
|
|
65
|
+
- **M3 少拆连接**:只改 `tools.*` 时 `planProjectChanges` 视为未变,`kickSweep` 重扫
|
|
66
|
+
restrict 即可。
|
|
67
|
+
- **低优先级项大多是真修而不是文档搪塞**(L4/L9 按审查建议用文档关闭,可接受)。
|
|
68
|
+
|
|
69
|
+
上一轮 2 高 + 8 中已全部有落地 commit。本轮**没有新的高优先级项**(不再会因为查询快照而拆官方重连,诊断也不会把 idle/give-up 说成零配置/健康装载)。余下是修复引入的语义漂移、查询面并发,以及上一轮 H1 的边角。
|
|
70
|
+
|
|
71
|
+
以下发现按严重度排列:**高**=可在现实使用中触发错误行为或把用户可观测状态说错;
|
|
72
|
+
**中**=一致性/竞态/与已文档化语义不符;**低**=可维护性与文案。
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## 3. 发现清单
|
|
77
|
+
|
|
78
|
+
| # | 级别 | 位置 | 一句话 |
|
|
79
|
+
|---|---|---|---|
|
|
80
|
+
| M1 | 中 | registry.ts:1751-1786 | `remountCount` 在工具恢复后不重置,`givenUp` 也不清:A1「连续 3 次」变成进程内寿命配额 |
|
|
81
|
+
| M2 | 中 | registry.ts:2083-2114、2015-2031;service.ts:4-15 | `snapshot`/`serverView` 离开 `enqueue` 链且仍读盘,与「只读内存态」不符,可与对账交叉成撕裂视图 |
|
|
82
|
+
| M3 | 中 | registry.ts:1771-1798 | 同一 fiber 上 `tools/list_changed` 的短暂 0 工具窗口仍会触发重挂(H1 残留,严重度已下降) |
|
|
83
|
+
| L1 | 低 | registry.ts:1377-1390、695 | `warnGates` 仍不随 `prunePerProjectState` 剪 |
|
|
84
|
+
| L2 | 低 | registry.ts:1893-1895 vs status.ts:20-22 | deny 展开用 `id ?? name`,预算/巡检用 `name ?? id` |
|
|
85
|
+
| L3 | 低 | registry.ts:1569-1572;cli.ts:953 | idle 行进 `unhealthy`,`status` 打成「不健康:(idle)」 |
|
|
86
|
+
| L4 | 低 | json-file.ts:202-236 | 显式 `type:"stdio"` 仍可带 `url`,静默丢 url(测试视为有意) |
|
|
87
|
+
| L5 | 低 | cli.ts:73-81、973-991 | `status --scope profile` 实际等同 `--scope user`;HELP 未写 profile |
|
|
88
|
+
| L6 | 低 | registry.ts:1519-1545 | 诊断锁超时被吞,summary 可静默不写;`.mcp-diag.json.mcp-project.lock` 会出现在 `.dsh/` |
|
|
89
|
+
| L7 | 低 | registry.ts:798-806、1148-1154 | `kick` / 宽限 timer `.unref()`:进程退出可能丢掉已排队的 150ms 对账 |
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 4. 上一轮发现落地情况
|
|
94
|
+
|
|
95
|
+
### 高 / 中(全部有对应 commit)
|
|
96
|
+
|
|
97
|
+
| 原 # | 状态 | 说明 |
|
|
98
|
+
|---|---|---|
|
|
99
|
+
| H1 | **主因已修,语义见本轮 M1/M3** | `unmountServer(..., "generation")` 只清 `everHadTools`;新世代 0 工具不重挂。`snapshot()` 不再 `reconcileAll`。`tools > 0` **不再**清 `remountCount`(代码注释写明是为了让跨世代 give-up 能攒上)——与上一轮建议的「恢复则清零」相反,见 M1。 |
|
|
100
|
+
| H2 | **关闭** | 目录行数、idle、give-up 均有测试:idle 后 `rows` 仍 ≥1;`env-missing` 不被 idle 覆盖;give-up 出现在 summary / status。 |
|
|
101
|
+
| M1 | **关闭** | 跳过重读前现取 `resolveActiveProfileName()` 与 `host=` 指纹;改 profile / 宿主全局名会失配重读。 |
|
|
102
|
+
| M2 | **关闭** | `planImport` 在锁回调里根据当前 `servers`/`rows` 判定;并发测试断言只有一个 winner。dry-run 仍锁外预演(不写盘,可接受)。 |
|
|
103
|
+
| M3 | **关闭** | `canonicalConfig` 过滤 `key !== "tools"`;`reconcileContainer` 对仍挂着的行赋值 `state.row`。 |
|
|
104
|
+
| M4 | **关闭** | 回落到阈值下清门控;再超同一数量会第二次告警(测试覆盖)。 |
|
|
105
|
+
| M5 | **关闭** | view 含 `tools`;`list` 有 `toolFilterHint`;`get` 打印 Allow/Deny。 |
|
|
106
|
+
| M6 | **关闭** | 诊断写进 `withPatchLock`;fiber settle 的 diag 仍经 `enqueue`,与对账链串行。 |
|
|
107
|
+
| M7 | **关闭** | 未声明传输且同时有 command 与 url/httpUrl → entry error。显式 stdio+url 仍放行,见 L4。 |
|
|
108
|
+
| M8 | **关闭** | `syncUserWatcher` 加入 `foreignUserMcpJsonFile`(与 `mcp.json` 同路径时跳过);测试覆盖创建对方文件会 kick。 |
|
|
109
|
+
|
|
110
|
+
### 低
|
|
111
|
+
|
|
112
|
+
| 原 # | 状态 |
|
|
113
|
+
|---|---|
|
|
114
|
+
| L1 | 关闭(`Object.create(null)` 别名表) |
|
|
115
|
+
| L2 | 关闭(`schemaToolId`:`name` 然后 `id`)。deny 路径未改,见本轮 L2 |
|
|
116
|
+
| L3 | 关闭(非法 glob 告警,去重后列出) |
|
|
117
|
+
| L4 | 按建议文档关闭(glob 对生效名;不改匹配两边) |
|
|
118
|
+
| L5 | 关闭(registry 模块头已改成全局 `MountContainer` 一条) |
|
|
119
|
+
| L6 | 部分关闭(`idleSince`/`lastScanDesired` 已剪;`warnGates` 未剪 → 本轮 L1) |
|
|
120
|
+
| L7 | 关闭(`shouldSkipConfigReread` 返回 `{ skip, signature }`) |
|
|
121
|
+
| L8 | 关闭 |
|
|
122
|
+
| L9 | 按建议文档关闭 |
|
|
123
|
+
| L10 | 关闭(`--scope user\|profile` 不打项目诊断,`--scope project` 不打全局) |
|
|
124
|
+
| L11 | 关闭 |
|
|
125
|
+
| L12 | 关闭 |
|
|
126
|
+
|
|
127
|
+
---
|
|
128
|
+
|
|
129
|
+
## 5. 中优先级发现(详细)
|
|
130
|
+
|
|
131
|
+
### M1 `remountCount` 变成寿命配额,恢复后也不清 `givenUp`
|
|
132
|
+
|
|
133
|
+
**位置**:`registry.ts:1751-1756`(注释)、`1778-1786`(`tools > 0` 只置 `everHadTools`)、
|
|
134
|
+
`1783-1785`(`givenUp` 后直接 `continue`)
|
|
135
|
+
|
|
136
|
+
**上一轮 H1 建议**是:世代化 `everHadTools`,并且 **工具重新出现时清 `remountCount`**,
|
|
137
|
+
give-up 仍表示「连续 N 次重挂后仍为 0 工具」。提案 A1 / `CHANGELOG` / 发布说明写的也是
|
|
138
|
+
「连续 3 次」。
|
|
139
|
+
|
|
140
|
+
本轮实现为了让「每代都曾有过工具、再掉到 0」能攒满上限,选择**恢复后不清
|
|
141
|
+
`remountCount`**。测试也改成了这个语义(`three generation deaths remount three times`,
|
|
142
|
+
然后第四次 pulse 走 give-up)。日志仍说「连续重挂 N 次后仍无工具」,与计数方式不一致。
|
|
143
|
+
|
|
144
|
+
后果(长会话、项目一直 `keepMounts`):
|
|
145
|
+
|
|
146
|
+
1. 偶发掉线但每次都能救活的服务器,历史上重挂满 3 次后,第 4 次掉线直接
|
|
147
|
+
`give-up`、不再自愈——即使中间已经完全恢复过。idle 卸载会 `forget` 健康记录,
|
|
148
|
+
所以配额只在「会话/cwd 保活」期间累积,对常开的 dsh 会话仍然现实。
|
|
149
|
+
2. give-up **不拆 fiber**(这是对的,把官方内部重连留给现世代)。若之后官方重连成功、
|
|
150
|
+
工具回来:`tools > 0` 分支既不清 `givenUp`,也不删 `skipReasons` 的 `give-up`。
|
|
151
|
+
`summarizeScope` 见 `givenUp` 就记不健康并 `continue`,**不算已装载**。快照/
|
|
152
|
+
`status` 会把一个已经又有工具的实例说成 give-up。
|
|
153
|
+
|
|
154
|
+
**建议**:
|
|
155
|
+
|
|
156
|
+
- `tools > 0` 时:`remountCount = 0`、`givenUp = false`、删掉该行的 `give-up` 标记。
|
|
157
|
+
这样 give-up 重新表示「连续失败」,与 A1 一致;世代化 `everHadTools` 仍然防止新 fiber
|
|
158
|
+
的首连窗口被拆掉。
|
|
159
|
+
- 若产品就是要寿命配额:改发布说明 / CHANGELOG / 告警文案,去掉「连续」,并写明
|
|
160
|
+
重启宿主或 idle 卸载会重置。不要只留在代码注释里。
|
|
161
|
+
- 新世代若长时间从未列出工具,用**本世代超时**(或官方 `maxAttempts` 镜像)而不是
|
|
162
|
+
跨世代累加 `remountCount`。
|
|
163
|
+
|
|
164
|
+
**测试**:① 三次「有工具 → 清零 → 重挂 → 再有工具」之后第四次清零,应再重挂而不是
|
|
165
|
+
give-up;② give-up 之后把 schema 加回去再 `reconcileNow`,summary 不应再把该行标
|
|
166
|
+
give-up,且 `mounted` 计入。
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
### M2 `snapshot` / `serverView` 不是内存快照,且不再与对账串行
|
|
171
|
+
|
|
172
|
+
**位置**:`registry.ts:2083+`(`snapshot`)、`2015-2031` / `2050+`(`locateRow` /
|
|
173
|
+
`serverView`)、`1971-1974`(`reconcileNow` 仍走 `enqueue`);`service.ts:4-15`
|
|
174
|
+
写「只读内存态,不触发对账」
|
|
175
|
+
|
|
176
|
+
H1 把 `snapshot()` 从「先 `enqueue(reconcileAll)`」改成直接跑,避免查询驱动巡检——
|
|
177
|
+
这一半是对的。副作用有两层:
|
|
178
|
+
|
|
179
|
+
1. **不再进 `enqueue` 链**。`snapshot` 在 `for (const [key, entry] of this.projects)`
|
|
180
|
+
里 `await readPatchFile` / `readDshJsonFile`;`serverView` 同样 `await` 各层文件。
|
|
181
|
+
这些 await 点上 `reconcileAll` 可以改 `entry.servers`、`skipReasons`、`effective`、
|
|
182
|
+
`userLayer`。分区结果会把 T1 的文件行和 T2 的装载态拼在一起。JS 单线程不会把 Map
|
|
183
|
+
迭代打崩,但 B3 轮询正好会打在文件事件对账的中间。`globalState()` 才是同步内存读。
|
|
184
|
+
2. **仍读盘**,不是「内存快照」。文件已被 CLI 改、watcher 尚未 debounce 完时,查询面
|
|
185
|
+
显示新文件 + 旧 fiber(或相反),和 `lastScanDesired` 也不对齐。`reload()` 才能收敛,
|
|
186
|
+
这一点注释写了,但服务面文案「只读内存态」会让调用方以为不碰磁盘、也不需要自己
|
|
187
|
+
串行化。
|
|
188
|
+
|
|
189
|
+
**建议**(不必重新触发对账):
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
async snapshot(): Promise<ProjectFileState[]> {
|
|
193
|
+
return this.enqueue(async () => this.buildSnapshotFromMemory());
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
内存侧用 `lastScanDesired` + 当前 `servers` / `skipReasons` 拼 view;若必须反映尚未
|
|
198
|
+
对账的磁盘内容,在文档里写明「读盘且与 `reload` 并发不安全」,并让 `snapshot` 至少
|
|
199
|
+
`enqueue` 空转以与 `reconcileAll` 互斥。`serverView` 对用户层已有 `this.userLayer`
|
|
200
|
+
缓存,项目层也可走 `lastScanDesired` 而不是每次 `locateRow` 读文件。
|
|
201
|
+
|
|
202
|
+
**测试**:在 `reconcileNow` 进行中(或注入慢 `statFile`/`readPatchFile`)并发调用
|
|
203
|
+
`snapshot()`,断言不抛、且同一项目的 `servers` 与 `skipReason` 来自同一世代(例如
|
|
204
|
+
对账计数或 mount 代数)。
|
|
205
|
+
|
|
206
|
+
---
|
|
207
|
+
|
|
208
|
+
### M3 同一世代的 0 工具窗口仍会重挂
|
|
209
|
+
|
|
210
|
+
**位置**:`registry.ts:1771-1798`
|
|
211
|
+
|
|
212
|
+
H1 修的是**新 fiber** 尚未 `tools/list` 成功。同一 `state` 上官方 `list_changed`
|
|
213
|
+
若先清空再填回,`everHadTools === true` 且 `phase === "active"`,巡检会当成连接死亡。
|
|
214
|
+
`snapshot` 不再驱动对账后,这条只剩 watcher / agent / 指纹跳过后的 `reconcileAll`
|
|
215
|
+
(含健康巡检)会撞上。窗口比以前窄,但仍可能误拆一条正在热更新工具列表的连接,并
|
|
216
|
+
计入 M1 的 `remountCount`。
|
|
217
|
+
|
|
218
|
+
**建议**:0 工具持续超过本世代宽限(或连续 N 轮巡检)再重挂;不要在单次
|
|
219
|
+
`mcpToolCount === 0` 边沿拆 fiber。可与 M1 的「本世代超时」共用一条计时。
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
## 6. 低优先级发现
|
|
224
|
+
|
|
225
|
+
- **L1 `warnGates` 未剪**(registry.ts:1377-1390)。`idleSince` / `lastScanDesired`
|
|
226
|
+
已按 known keys 收;告警门控仍按历史 `gateKey` 常驻。量级小,项目拆掉后下次同样
|
|
227
|
+
签名不会再告——通常反而是想要的。若担心测试隔离或长期宿主泄漏,可按前缀扫掉
|
|
228
|
+
未知项目键。
|
|
229
|
+
- **L2 工具 id 口径分裂**。`registeredToolIds`(restrict/deny 展开)仍
|
|
230
|
+
`schema.id ?? schema.name`;`schemaToolId`(计数/预算)是 `name ?? id`。官方
|
|
231
|
+
`ToolSchema` 只有 `name`,两条路径在假 ctx 下都绿。一旦两边都在且不一致,deny
|
|
232
|
+
集与预算统计会对不上。建议两处都调用 `schemaToolId`。
|
|
233
|
+
- **L3 idle 进 `unhealthy`**。H2 要求 idle 可见,实现上走了同一条 `addUnhealthy`。
|
|
234
|
+
`dsh-mcp status` 因而打印 `不健康:alpha (idle)`——宽限卸载并不是故障。可把
|
|
235
|
+
`idle` 从 `unhealthy` 拆到摘要的另一字段,或 status 把 idle 打成「未装载(无会话)」。
|
|
236
|
+
- **L4 显式 `type:"stdio"` + `url`** 仍按 stdio 解析并丢掉 url。未声明并存已拒绝
|
|
237
|
+
(M7);这条是「你写了 type 就按 type」。建议报 warning,或在错误文案里点名
|
|
238
|
+
`httpUrl`(现在一律说「与 url」)。
|
|
239
|
+
- **L5 `status --scope profile`**。过滤条件是「是否项目源」,与 `--scope user`
|
|
240
|
+
相同(所有用户层 + 全部 profile 文件)。HELP 的 status 行仍写
|
|
241
|
+
`[--scope project|user]`。要么按 `--profile` 只打那一层,要么 HELP 写明
|
|
242
|
+
profile 与 user 同口径。
|
|
243
|
+
- **L6 诊断锁**。超时 5s 后 `writeDiagAt` 外层 `catch` 吞掉,对账继续、summary
|
|
244
|
+
可能丢一轮。进程内其实已有 `enqueue` 串行,锁主要防跨进程;崩溃残留锁需等 30s
|
|
245
|
+
陈旧才会被拆,中间 5s 等待失败就会静默。至少 `logger.warn` 一声。锁文件名
|
|
246
|
+
`.mcp-diag.json.mcp-project.lock` 会进 `.dsh/`,可接受但值得知道。
|
|
247
|
+
- **L7 `timer.unref()`**。debounce / 宽限不等待进程退出。长生命周期宿主无感;
|
|
248
|
+
短进程或测试若在 150ms 内退出,已 `kick()` 的对账会被丢掉。这是有意不挡退出,
|
|
249
|
+
文档或注释写一句即可。
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 7. 建议补充的测试(按发现编号)
|
|
254
|
+
|
|
255
|
+
| # | 用例 | 断言要点 |
|
|
256
|
+
|---|---|---|
|
|
257
|
+
| T1 (M1) | 有工具 → 0 → 重挂 → 再有工具,重复 3 轮后再次 0 | 第四次仍 remount,不 give-up;`remountCount` 在恢复后回到 0 |
|
|
258
|
+
| T2 (M1) | 走到 give-up 后把 schema 加回去再对账 | `givenUp === false`,summary 不再将该行标 give-up,`mounted` 含该行 |
|
|
259
|
+
| T3 (M2) | `snapshot()` 与进行中的 `reconcileNow` 交错(慢 IO) | 不抛;同一项目分区的 phase/skipReason 不出现「已 unmount 却无 skip、文件行还在」的撕裂 |
|
|
260
|
+
| T4 (M3) | 同一 fiber:`schemas.length = 0` 后立刻下一轮对账,backoff 内再填回工具 | 不 unmount(或至少 `ctx.plugin` 次数不增加) |
|
|
261
|
+
|
|
262
|
+
T1 与当前 `pulseDead`×4 用例冲突:落地 M1 时要改现有「三次世代死亡」断言,不要并存两套语义。
|
|
263
|
+
|
|
264
|
+
---
|
|
265
|
+
|
|
266
|
+
## 附录 A:与上一轮建议测试的对照
|
|
267
|
+
|
|
268
|
+
上一轮 T1–T9 在本区间均已有对应用例(世代窗口、snapshot 只读、idle/env-missing/give-up
|
|
269
|
+
摘要、改 profile 指纹、并发 import、只改 tools 不拆连接、预算回落再告警、command+url
|
|
270
|
+
entry error)。不必再补那些。本附录 A 的新 T1–T4 只覆盖**修复引入或故意留下**的缺口。
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
**修复优先级建议**:M1(恢复则清 `remountCount`/`givenUp`,让 A1「连续」重新成立,
|
|
275
|
+
否则长会话里偶发掉线会被寿命配额放弃,且 give-up 后工具回来 status 仍撒谎)→
|
|
276
|
+
M2(`snapshot`/`serverView` 进 `enqueue` 或不读盘,避免 B3 轮询打到撕裂视图)→
|
|
277
|
+
M3(同世代 0 工具去抖)→ L2(统一 `schemaToolId`)其余文案/HELP 可随手清。
|