@luolingp/request-doctor 0.14.1 → 0.14.2
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/docs/01-/350/275/257/344/273/266/351/234/200/346/261/202/350/247/204/346/240/274/350/257/264/346/230/216.md +1 -1
- package/docs/02-/350/275/257/344/273/266/350/256/276/350/256/241/350/257/264/346/230/216.md +392 -391
- package/docs/03-/350/275/257/344/273/266/346/265/213/350/257/225/346/212/245/345/221/212.md +1 -1
- package/docs/04-/345/256/211/350/243/205/351/203/250/347/275/262/350/257/264/346/230/216.md +1 -1
- package/docs/05-/347/224/250/346/210/267/346/211/213/345/206/214.md +30 -4
- package/docs/06-/350/275/257/344/273/266/350/264/250/351/207/217/345/272/246/351/207/217/346/212/245/345/221/212.md +1 -1
- package/docs/quality-report.json +1 -1
- package/package.json +1 -1
|
@@ -1,391 +1,392 @@
|
|
|
1
|
-
# 软件设计说明
|
|
2
|
-
|
|
3
|
-
**产品名称**:request-doctor(请求急救员)
|
|
4
|
-
**版本**:0.14.
|
|
5
|
-
**编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.2
|
|
6
|
-
|
|
7
|
-
---
|
|
8
|
-
|
|
9
|
-
## 1 引言
|
|
10
|
-
|
|
11
|
-
### 1.1 标识
|
|
12
|
-
|
|
13
|
-
本文档描述 request-doctor 0.10.0 的体系结构、模块划分、接口与关键算法。需求来源见《软件需求规格说明》。
|
|
14
|
-
|
|
15
|
-
### 1.2 系统概述
|
|
16
|
-
|
|
17
|
-
插件是 DSH 宿主进程内的一个 Cordis 插件,通过四类扩展点介入运行时行为,并在后台周期性校验插件生态的一致性。整体为单进程、事件驱动、无外部依赖。
|
|
18
|
-
|
|
19
|
-
### 1.3 文档概述
|
|
20
|
-
|
|
21
|
-
第 2 章给出体系结构,第 3 章逐模块说明职责与接口,第 4 章给出关键算法与数据结构,第 5 章说明错误处理与恢复,第 6 章说明设计权衡。
|
|
22
|
-
|
|
23
|
-
---
|
|
24
|
-
|
|
25
|
-
## 2 体系结构
|
|
26
|
-
|
|
27
|
-
### 2.1 分层
|
|
28
|
-
|
|
29
|
-
```
|
|
30
|
-
┌─────────────────────────────────────────┐
|
|
31
|
-
│ index.js(组合根) │
|
|
32
|
-
│ 生命周期 / 扩展点注册 / 扫描调度 / 命令 │
|
|
33
|
-
└───────┬──────────┬──────────┬───────────┘
|
|
34
|
-
│ │ │
|
|
35
|
-
┌─────────────▼──┐ ┌────▼─────┐ ┌─▼──────────────┐
|
|
36
|
-
│ repair-listeners│ │ scans │ │ doctor command │
|
|
37
|
-
└───┬────────┬───┘ └────┬─────┘ └─┬──────────────┘
|
|
38
|
-
│ │ │ │
|
|
39
|
-
┌────────▼──┐ ┌──▼────────┐ │ ┌──────▼──────────┐
|
|
40
|
-
│model-repair│ │ tool/carr.│ │ │install-support │
|
|
41
|
-
└────┬──────┘ └──┬────────┘ │ └──────┬──────────┘
|
|
42
|
-
│ │ │ │
|
|
43
|
-
┌────▼───────────▼───────────▼───────────▼────┐
|
|
44
|
-
│ 领域层:classify / semver / compat / │
|
|
45
|
-
│ compat-repair / compat-env / arbitrate│
|
|
46
|
-
└────┬────────────────────────────────────────┘
|
|
47
|
-
│
|
|
48
|
-
┌────▼───────────────────────────────────────┐
|
|
49
|
-
│ 基础设施:timers / sleep / stat-cache / │
|
|
50
|
-
│ runtime-probe / diagnostics / │
|
|
51
|
-
│ retry-ledger │
|
|
52
|
-
└────────────────────────────────────────────┘
|
|
53
|
-
```
|
|
54
|
-
|
|
55
|
-
依赖只向下。领域层不感知 Cordis;基础设施层只提供无状态能力或显式注入的状态。
|
|
56
|
-
|
|
57
|
-
### 2.2 模块清单
|
|
58
|
-
|
|
59
|
-
| 模块 | 层级 | 职责 |
|
|
60
|
-
| --- | --- | --- |
|
|
61
|
-
| `index.js` | 组合根 | 组装依赖、注册扩展点、调度扫描、注册命令、释放资源 |
|
|
62
|
-
| `lib/repair-listeners.js` | 编排 | 工具重放与载体 503 的处理,并转发模型请求给 `model-repair` |
|
|
63
|
-
| `lib/model-repair.js` | 编排 | 模型请求分类、修复、退避与重试决策 |
|
|
64
|
-
| `lib/version-repair.js` | 编排 | 审计、适配、重载不兼容插件 |
|
|
65
|
-
| `lib/plugin-discovery.js` | 编排 | 从四个来源找出需要处理的插件 |
|
|
66
|
-
| `lib/env-verify.js` | 领域 | 核对兼容环境是否真能提供插件要的符号 |
|
|
67
|
-
| `lib/package-fetch.js` | 基础设施 | 从注册表取指定版本的包并解压 |
|
|
68
|
-
| `lib/version-search.js` | 领域 | 决定按什么顺序去试哪个版本 |
|
|
69
|
-
| `lib/cache-store.js` | 基础设施 | 下载缓存的维护与淘汰 |
|
|
70
|
-
| `lib/install-support.js` | 编排 | 本地目录的安装前预置 |
|
|
71
|
-
| `lib/install-bundle.js` | 编排 | 注册表包的自行安装(取包、依赖、环境、登记) |
|
|
72
|
-
| `lib/install-rescue.js` | 编排 | 授权版本豁免、重装、装完补齐兼容形态 |
|
|
73
|
-
| `lib/classify.js` | 领域 | 失败分类与重试策略解析 |
|
|
74
|
-
| `lib/semver.js` | 领域 | 版本解析、比较、范围满足判定 |
|
|
75
|
-
| `lib/compat.js` | 领域 | 兼容性判定、源码符号提取 |
|
|
76
|
-
| `lib/compat-repair.js` | 领域 | 模块图扫描、单插件审计 |
|
|
77
|
-
| `lib/compat-env.js` | 领域 | 兼容环境构建、manifest 对齐与还原 |
|
|
78
|
-
| `lib/arbitrate.js` | 领域 | 路由冲突描述(只报告) |
|
|
79
|
-
| `lib/timers.js` | 基础设施 | 脱离 HMR 事务的定时器 |
|
|
80
|
-
| `lib/sleep.js` | 基础设施 | 可中断延时 |
|
|
81
|
-
| `lib/stat-cache.js` | 基础设施 | 按文件签名缓存 manifest 与模块图 |
|
|
82
|
-
| `lib/runtime-probe.js` | 基础设施 | 运行时版本、profile、包目录、provider 注册表 |
|
|
83
|
-
| `lib/diagnostics.js` | 基础设施 | 计数、事件日志、快照写入 |
|
|
84
|
-
| `lib/retry-ledger.js` | 基础设施 | 每 agent 重试链预算与事件记录 |
|
|
85
|
-
| `lib/symbol-alias.js` | 领域 | 符号别名表:把旧名映射到现行实现里的等价导出 |
|
|
86
|
-
| `client.js` | 界面 | 插件面板里的「不兼容安装」页:调 Host 路由,负责展示与触发 |
|
|
87
|
-
|
|
88
|
-
### 2.3 并发模型
|
|
89
|
-
|
|
90
|
-
- 单线程事件循环。
|
|
91
|
-
- 版本扫描以布尔标志互斥,进行中的扫描不被第二次扫描打断。
|
|
92
|
-
- 事件驱动的扫描请求合并:同一时刻最多一个待执行扫描。
|
|
93
|
-
- 定时器为 `unref`,不阻止进程退出。
|
|
94
|
-
|
|
95
|
-
---
|
|
96
|
-
|
|
97
|
-
## 3 模块设计
|
|
98
|
-
|
|
99
|
-
### 3.1 组合根 `index.js`
|
|
100
|
-
|
|
101
|
-
导出 `name` 与 `apply(ctx, rawConfig)`。
|
|
102
|
-
|
|
103
|
-
`apply` 的执行顺序:
|
|
104
|
-
|
|
105
|
-
1. 解析配置;`autoRepair` 为假则记录日志并直接返回。
|
|
106
|
-
2. 建立 `AbortController`(插件生命周期)、定时器集合、定时器助手。
|
|
107
|
-
3. 建立诊断、运行时探针、重试账本、版本修复器、修复监听器。
|
|
108
|
-
4. 注册三类事件监听:
|
|
109
|
-
- `agent/request-error`(局部)
|
|
110
|
-
- `tools/execute`(局部)
|
|
111
|
-
- `connection/request`(全局:载体在兄弟上下文)
|
|
112
|
-
5. 注册扫描调度(`app-boot/config-reload`、`plugin-manager/changed`,均为全局事件)。
|
|
113
|
-
6. 注入 `commands` 注册 `/doctor` 命令;注入 `webServer` 注册界面用的三条路由。
|
|
114
|
-
7. 注册释放钩子:中止生命周期、停止扫描、清空定时器、刷新诊断。
|
|
115
|
-
|
|
116
|
-
### 3.2 `lib/model-repair.js`
|
|
117
|
-
|
|
118
|
-
| 接口 | 说明 |
|
|
119
|
-
| --- | --- |
|
|
120
|
-
| `createModelRepair(input)` | 返回 `{ handle }` |
|
|
121
|
-
| `handle(payload, next)` | 处理一次请求失败 |
|
|
122
|
-
|
|
123
|
-
`handle` 流程:
|
|
124
|
-
|
|
125
|
-
```
|
|
126
|
-
await next() // 先让下游决定
|
|
127
|
-
下游要求重试 → 直接返回(尊重 provider 策略)
|
|
128
|
-
信号已中止 → 返回下游结果
|
|
129
|
-
分类失败 → 非客户端原因或 skip 策略 → 返回下游结果
|
|
130
|
-
strategy = version-repair → 触发版本修复;未修复则放弃
|
|
131
|
-
strategy = compact → 压缩上下文 + 裁剪工具结果
|
|
132
|
-
scheduleRetry:
|
|
133
|
-
取预算条目,计数 +1
|
|
134
|
-
超过上限 → 首次超限记录 give-up,返回下游结果
|
|
135
|
-
计算退避 → 记录 llm/retry → 等待(可中断)
|
|
136
|
-
记录 llm/retry-started → 返回 { kind: 'retry' }
|
|
137
|
-
```
|
|
138
|
-
|
|
139
|
-
### 3.3 `lib/repair-listeners.js`
|
|
140
|
-
|
|
141
|
-
| 接口 | 说明 |
|
|
142
|
-
| --- | --- |
|
|
143
|
-
| `createRepairListeners(input)` | 返回三个监听函数 |
|
|
144
|
-
|
|
145
|
-
- `onToolExecute`:`attemptDispatch` 判定一次执行是否需要重放;需要则退避后重来,直到预算耗尽或错误不可重放。
|
|
146
|
-
- `onConnectionRequest`:响应头未发出且为瞬时传输错误时写 503;否则原样抛出。
|
|
147
|
-
|
|
148
|
-
### 3.4 `lib/version-repair.js` 与 `lib/plugin-discovery.js`
|
|
149
|
-
|
|
150
|
-
| 接口 | 说明 |
|
|
151
|
-
| --- | --- |
|
|
152
|
-
| `createVersionRepair(input)` | 返回 `{ run, isBusy }` |
|
|
153
|
-
| `run(reason, manager?, attempt?)` | 执行一轮版本修复 |
|
|
154
|
-
| `collectMismatched(bundles, facts, plugins, probe)` | 收集所有需要处理的插件(在 `plugin-discovery`) |
|
|
155
|
-
|
|
156
|
-
内部阶段:
|
|
157
|
-
|
|
158
|
-
1. **发现** `collectMismatched`:合并四类候选并按包名去重。
|
|
159
|
-
- `deferredCandidates`:有 staging 标记且未建环境的包
|
|
160
|
-
- `refusedCandidates`:Loader 已拒绝的包(含其算出的未满足集合)
|
|
161
|
-
- `installedCandidates`:清单里已存在的插件中,声明未满足或环境缺件者
|
|
162
|
-
- `scannedCandidates`:直接扫 profile 的 `node_modules`,补上 `listPlugins()` 看不到的插件
|
|
163
|
-
(加载失败的不在 Loader 树里,恰恰最需要处理;`link:` 安装的是符号链接,也要算)
|
|
164
|
-
2. **审计** `auditAll` → `auditOne`:逐包调用 `auditInstalledPlugin`,异常降级为 `review`。
|
|
165
|
-
3. **仲裁** `providerRoster` + `describeContention`:只生成报告条目。
|
|
166
|
-
4. **适配** `adapt`:构建兼容环境、对齐 manifest、**核对环境**。
|
|
167
|
-
5. **重载**:对本次适配成功的包调用 `setBundleEnabled(name, true)`;检测到 HMR 嵌套错误则退避重试。
|
|
168
|
-
|
|
169
|
-
适配之后的核对是必要的一步。建好环境不等于能用:垫片少写一个导出,插件在加载那一刻
|
|
170
|
-
就会崩,而修复流程还以为成功了。`verifyEnvironment` 逐条确认插件要的每个符号环境是否真能
|
|
171
|
-
给出来,通不过就记为未修复而不是谎报成功。
|
|
172
|
-
|
|
173
|
-
### 3.4a `lib/env-verify.js`
|
|
174
|
-
|
|
175
|
-
| 接口 | 说明 |
|
|
176
|
-
| --- | --- |
|
|
177
|
-
| `verifyEnvironment({pluginDir, imports})` | 核对整个环境,返回 `{ok, problems, unverified}` |
|
|
178
|
-
| `verifyPackage(pluginDir, name, symbols)` | 核对单个包,返回 `provided` / `missing` / `unverifiable` |
|
|
179
|
-
|
|
180
|
-
三种结论的区分很重要:
|
|
181
|
-
|
|
182
|
-
- `missing`:目标读得到,但确实没有这个符号。这是真问题。
|
|
183
|
-
- `unverifiable`:目标读不到(运行时包常装在归档里)。不算失败,只是这次核对不了。
|
|
184
|
-
- `provided`:能找到。
|
|
185
|
-
|
|
186
|
-
`default` 需要单独判断——`export *` 从不转发默认导出,目标有 `default` 不等于插件拿得到,
|
|
187
|
-
必须由垫片自己写出来。
|
|
188
|
-
|
|
189
|
-
### 3.4b `lib/package-fetch.js`
|
|
190
|
-
|
|
191
|
-
| 接口 | 说明 |
|
|
192
|
-
| --- | --- |
|
|
193
|
-
| `fetchPackage(name, version, destDir)` | 从注册表取指定版本,解压到目标目录,返回 `{ok, error?}` |
|
|
194
|
-
|
|
195
|
-
兼容模式的落地靠这个模块。插件当初是对着某个版本写的,那个版本的代码还在仓库里,取下来装上就行——跑的是它认识的那份实现,不是转发到今天实现的替身。
|
|
196
|
-
|
|
197
|
-
实现是纯 Node:`fetch` 拿 tarball,`zlib.gunzipSync` 解压,自己解析 tar 头。不引第三方依赖,因为插件要在没有额外包的 profile 里运行。
|
|
198
|
-
|
|
199
|
-
解压时做了目录穿越防护:每个解出来的路径必须仍在目标目录内。
|
|
200
|
-
|
|
201
|
-
### 3.4c 旧版本的获取流程
|
|
202
|
-
|
|
203
|
-
1. `adapt` 先做一次 `dryRun` 探测:只问「能不能建」,一个字节不写。
|
|
204
|
-
2. 探测返回 `needs-old-version` 时进入版本搜索:先试插件声明的版本,再试声明范围内的其余版本,最后才试范围外的。第一个凑齐符号的版本胜出。取回的缓存在 `<profile>/.request-doctor-cache/`。
|
|
205
|
-
3. 取回了再建一次环境:符号在旧版本里确实有,就把整份旧实现放进去;旧版本也没有,如实报缺。
|
|
206
|
-
4. 旧版本自己的运行时依赖(`@deepseek-ai/*`,版本按声明钉死)也一起解析、一起装。只装它一个的话,它加载时 import 的兄弟包要么找不到,要么拿到新版本,两种都不是插件当初的环境。闭包最多 16 个包。
|
|
207
|
-
|
|
208
|
-
`dryRun` 不是可选项。探测调用如果落盘,在 profile 目录上执行时会把里面的真实包覆盖成垫片——一次只该读的检查,改掉了运行时依赖。
|
|
209
|
-
|
|
210
|
-
### 3.5 `lib/compat-env.js`
|
|
211
|
-
|
|
212
|
-
| 接口 | 说明 |
|
|
213
|
-
| --- | --- |
|
|
214
|
-
| `installCompatEnvironment(input)` | 构建环境,返回 `installed` / `unchanged` / `refused` / `failed` |
|
|
215
|
-
| `alignManifest(input)` | 对齐 DSH 依赖声明,返回 `aligned` / `already` / `skipped` / `failed` |
|
|
216
|
-
| `restoreManifest(dir)` | 还原原 manifest |
|
|
217
|
-
| `removeCompatEnvironment(dir)` | 只删除本插件创建的包并清理空目录 |
|
|
218
|
-
| `resolveInstalledDir(input)` | 定位真实包目录 |
|
|
219
|
-
| `pinnedVersion(range, fallback)` | 由范围推导可用的具体版本 |
|
|
220
|
-
|
|
221
|
-
### 3.6 `lib/install-support.js`
|
|
222
|
-
|
|
223
|
-
| 接口 | 说明 |
|
|
224
|
-
| --- | --- |
|
|
225
|
-
| `prepareForInstall(input)` | 安装前预置,返回 `{prepared, packages, deferred, reason}` |
|
|
226
|
-
| `classifySpec(spec, profileDir)` | 判定本地目录或远程包 |
|
|
227
|
-
| `abandonPreparation(dir)` | 回滚预置 |
|
|
228
|
-
| `readStagedPreparation(dir)` | 读取 staging 标记 |
|
|
229
|
-
| `clearStagedPreparation(dir)` | 清除 staging 标记 |
|
|
230
|
-
| `preparationSurvived(dir)` | 判断预置是否仍在 |
|
|
231
|
-
|
|
232
|
-
`prepareForInstall` 分解为:`inspectCandidate`(校验)→ `resetPreviousAttempt`(清残留)→ 构建 → 分支:
|
|
233
|
-
|
|
234
|
-
- 仅因运行时包不可见而拒绝 → `deferEnvironment`:只对齐 manifest,交给后续扫描建环境
|
|
235
|
-
- 成功 → `finalizePreparation`:对齐 manifest 并写 staging 标记
|
|
236
|
-
|
|
237
|
-
### 3.7 领域模块
|
|
238
|
-
|
|
239
|
-
- `lib/semver.js`:`parseSemver`、`compareVersions`、`satisfies`、`classifyDistance`。`satisfies` 支持 `=`、`>`、`>=`、`<`、`<=`、`^`、`~`、`x` 通配、空格与 `||` 组合。
|
|
240
|
-
- `lib/classify.js`:失败分类采用**规则表** `CLASSIFICATION_RULES`,首条匹配即返回。顺序编码优先级:provider 侧拒绝先于通用传输判定。
|
|
241
|
-
- `lib/compat.js`:`collectDshImports`(解析具名导入)、`collectProviderIds`、`collectIdLiterals`(要求 `id` 与 `region` 成对)、`parseExportedNames`、`auditPluginCompatibility`。
|
|
242
|
-
- `lib/compat-repair.js`:`collectPackageFacts` 遍历模块图,`walkModuleGraph` 限 40 个文件;`auditInstalledPlugin` 产出 verdict 与 `importMap`。
|
|
243
|
-
- `lib/arbitrate.js`:给定静态声明、冲突集与运行时注册表,描述争用关系,不产生任何环境变更。
|
|
244
|
-
|
|
245
|
-
### 3.8 基础设施模块
|
|
246
|
-
|
|
247
|
-
- `lib/timers.js`:`createTimers(ctx, timers)` → `{ later }`。通过 `hmr.executing.exit()` 脱离激活事务,否则插件管理器报 "HMR transactions cannot be nested"。
|
|
248
|
-
- `lib/sleep.js`:`cancellableSleep(ms, signal)`,中断返回 `false`。
|
|
249
|
-
- `lib/stat-cache.js`:`createManifestReader()`(上限 512)、`createFactsReader(collect)`(上限 256)。键为文件签名,变更自失效。
|
|
250
|
-
- `lib/runtime-probe.js`:`createRuntimeProbe(input)`,提供 `runtimeFacts`、`runtimeDirOf`、`runtimeEntryOf`、`activeProviders`、`factsOf`、`get`。
|
|
251
|
-
- `lib/diagnostics.js`:`createDiagnostics(input)`,250 ms 合并写入,日志上限 30 条。
|
|
252
|
-
- `lib/retry-ledger.js`:`createRetryLedger(input)`,每 agent 上限 128 条链。
|
|
253
|
-
|
|
254
|
-
---
|
|
255
|
-
|
|
256
|
-
## 4 关键算法与数据结构
|
|
257
|
-
|
|
258
|
-
### 4.1 兼容环境构建
|
|
259
|
-
|
|
260
|
-
对插件导入的每个 `@deepseek-ai/*` 包:
|
|
261
|
-
|
|
262
|
-
1. 定位真实包目录;找不到则记入 `missing` 并拒绝。
|
|
263
|
-
2. 读取真实包入口,解析其导出名集合。
|
|
264
|
-
3. 若插件使用了真实包中不存在的符号 → 拒绝,报告该符号。
|
|
265
|
-
4. 否则在 `<插件>/node_modules/<包名>/` 写入:
|
|
266
|
-
- `package.json`:`name` 为原包名,`version` 为目标旧版本,`type: module`,`exports` 指向 `index.js`
|
|
267
|
-
- `index.js`:`export * from '<真实入口 file URL>'`;仅当真实入口有默认导出时追加 `export { default } from ...`
|
|
268
|
-
5. 写入仅在内容变化时发生。
|
|
269
|
-
|
|
270
|
-
Node 的向上查找使嵌套 `node_modules` 遮蔽 profile 中的同名包,从而实现"给插件一个它期望的旧环境"。
|
|
271
|
-
|
|
272
|
-
### 4.2 Manifest 对齐
|
|
273
|
-
|
|
274
|
-
改写 `peerDependencies` 中所有 `@deepseek-ai/*` 项,以及 `engines.dsh`,目标为环境实际服务的版本(无环境时用运行时版本)。写入前若备份不存在则先备份;写入采用写临时文件后 `rename`,因为 pnpm 以硬链接方式把 manifest 放入 `node_modules`,原地写入会污染存储。
|
|
275
|
-
|
|
276
|
-
### 4.3 审计判定
|
|
277
|
-
|
|
278
|
-
`auditPluginCompatibility` 产出 `verdict`:
|
|
279
|
-
|
|
280
|
-
| verdict | 条件 |
|
|
281
|
-
| --- | --- |
|
|
282
|
-
| `compatible` | 无未满足 peer;或距离 ∈ {satisfied, prerelease-delta, patch-delta} 且导入符号齐全 |
|
|
283
|
-
| `incompatible` | 存在缺失符号,或距离 ∈ {major-delta} |
|
|
284
|
-
| `review` | 距离 ∈ {minor-delta, unknown},或无法读取源码 |
|
|
285
|
-
|
|
286
|
-
只有 `compatible` 会被自动适配。
|
|
287
|
-
|
|
288
|
-
### 4.4 退避计算
|
|
289
|
-
|
|
290
|
-
`computeDelay(config, attempt, failure)`:`initialDelayMs * 2^(attempt-1)`,上限 `maxDelayMs`,再乘以 `1 ± jitterRatio` 的随机因子。默认 400 ms、上限 20 s、抖动 25%。
|
|
291
|
-
|
|
292
|
-
### 4.5 主要数据结构
|
|
293
|
-
|
|
294
|
-
```js
|
|
295
|
-
// 重试预算条目
|
|
296
|
-
{ count: number, retryId: string }
|
|
297
|
-
|
|
298
|
-
// 运行时事实(每次扫描重建,缓存不在其中)
|
|
299
|
-
{ runtimeVersion, profileDir, packages, baseUrl }
|
|
300
|
-
|
|
301
|
-
// 审计行
|
|
302
|
-
{ name, version, runtimeVersion, requirements, deferred,
|
|
303
|
-
verdict, reasons, missing, imports, importMap, providers, conflicts, contention }
|
|
304
|
-
|
|
305
|
-
// 兼容环境标记
|
|
306
|
-
// .request-doctor-compat.json
|
|
307
|
-
{ runtimeVersion, packages: string[] }
|
|
308
|
-
|
|
309
|
-
// 安装预置标记
|
|
310
|
-
// .request-doctor-staging
|
|
311
|
-
{ runtimeVersion, packages: string[], deferred?, requirements? }
|
|
312
|
-
```
|
|
313
|
-
|
|
314
|
-
---
|
|
315
|
-
|
|
316
|
-
## 5 错误处理与恢复
|
|
317
|
-
|
|
318
|
-
| 情形 | 处置 |
|
|
319
|
-
| --- | --- |
|
|
320
|
-
| 可选服务缺失 | `ctx.get(name)` 包裹 try/catch,缺失即降级,不中断 |
|
|
321
|
-
| 单个插件不可读 | 该插件审计降级为 `review`,其余继续 |
|
|
322
|
-
| 环境构建被拒绝 | 记录 `compat-refused`,不重试 |
|
|
323
|
-
| 环境构建失败 | 记录 `compat-failed`,下轮扫描重试 |
|
|
324
|
-
| HMR 事务嵌套 | 退避后重试,上限 `maxHmrRetries` |
|
|
325
|
-
| 诊断写入失败 | 吞掉异常,诊断不得影响修复 |
|
|
326
|
-
| 插件释放 | 中止生命周期信号、清空定时器、刷新诊断 |
|
|
327
|
-
|
|
328
|
-
---
|
|
329
|
-
|
|
330
|
-
## 6 设计权衡
|
|
331
|
-
|
|
332
|
-
1. **不绕过检查而给环境**:直接写版本豁免会让插件在错误假设下运行;给环境使其声明与实现一致,是唯一能同时满足用户诉求与正确性的做法。
|
|
333
|
-
2. **只适配审计通过者**:把"声明不匹配"与"能否真正运行"分开判定,避免把不兼容插件伪装成可用。
|
|
334
|
-
3. **规则表驱动分类**:相较于长 `if` 链,规则表让优先级与判定粒度显式可读,也便于单测逐条覆盖。
|
|
335
|
-
4. **缓存键用文件签名**:不引入文件监听,避免监听器自身成为资源负担与竞态源;签名失配即重算。
|
|
336
|
-
5. **扫描合并与退避**:健康 profile 的稳态开销趋近于零,异常时立即回到基础周期。
|
|
337
|
-
6. **路由冲突只报告**:插件归属用户,自动化不应替用户决定停用哪一个。
|
|
338
|
-
|
|
339
|
-
### 2.6 界面层
|
|
340
|
-
|
|
341
|
-
界面是一个独立的客户端模块 `client.js`,通过 `dsh.client` 声明,注册到插件面板的标签栏与设置里插件页的标签。它不持有业务逻辑:清单从 Host 路由读,安装请求交给 Host 执行。
|
|
342
|
-
|
|
343
|
-
客户端与 Host 之间有两条通道,实际只用后者:
|
|
344
|
-
|
|
345
|
-
| 通道 | 用法 | 结论 |
|
|
346
|
-
| --- | --- | --- |
|
|
347
|
-
| typert 远程服务 | `ctx.remote.pluginManager` | 动态加载的客户端模块拿不到该门面,实测恒为 undefined |
|
|
348
|
-
| 同源 HTTP 路由 | `fetch('/doctor/...')` | 可用;与插件管理器自身界面同一机制 |
|
|
349
|
-
|
|
350
|
-
Host 侧提供三条路由:
|
|
351
|
-
|
|
352
|
-
| 方法 | 路径 | 作用 |
|
|
353
|
-
| --- | --- | --- |
|
|
354
|
-
| GET | `/doctor/incompatible` | 返回需要处理的插件:本插件审计结果 + 管理器拒装记录,去重 |
|
|
355
|
-
| POST | `/doctor/install` | 装一个包,复用 `/doctor install` 的实现 |
|
|
356
|
-
| GET | `/doctor/rescan` | 立即触发一轮审计 |
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
|
366
|
-
|
|
|
367
|
-
| `
|
|
368
|
-
| `
|
|
369
|
-
| `
|
|
370
|
-
| `
|
|
371
|
-
| `
|
|
372
|
-
| `
|
|
373
|
-
| `
|
|
374
|
-
| `
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
|
380
|
-
|
|
|
381
|
-
|
|
|
382
|
-
|
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
|
388
|
-
|
|
|
389
|
-
|
|
|
390
|
-
|
|
|
391
|
-
|
|
|
1
|
+
# 软件设计说明
|
|
2
|
+
|
|
3
|
+
**产品名称**:request-doctor(请求急救员)
|
|
4
|
+
**版本**:0.14.2
|
|
5
|
+
**编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.2
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 1 引言
|
|
10
|
+
|
|
11
|
+
### 1.1 标识
|
|
12
|
+
|
|
13
|
+
本文档描述 request-doctor 0.10.0 的体系结构、模块划分、接口与关键算法。需求来源见《软件需求规格说明》。
|
|
14
|
+
|
|
15
|
+
### 1.2 系统概述
|
|
16
|
+
|
|
17
|
+
插件是 DSH 宿主进程内的一个 Cordis 插件,通过四类扩展点介入运行时行为,并在后台周期性校验插件生态的一致性。整体为单进程、事件驱动、无外部依赖。
|
|
18
|
+
|
|
19
|
+
### 1.3 文档概述
|
|
20
|
+
|
|
21
|
+
第 2 章给出体系结构,第 3 章逐模块说明职责与接口,第 4 章给出关键算法与数据结构,第 5 章说明错误处理与恢复,第 6 章说明设计权衡。
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## 2 体系结构
|
|
26
|
+
|
|
27
|
+
### 2.1 分层
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
┌─────────────────────────────────────────┐
|
|
31
|
+
│ index.js(组合根) │
|
|
32
|
+
│ 生命周期 / 扩展点注册 / 扫描调度 / 命令 │
|
|
33
|
+
└───────┬──────────┬──────────┬───────────┘
|
|
34
|
+
│ │ │
|
|
35
|
+
┌─────────────▼──┐ ┌────▼─────┐ ┌─▼──────────────┐
|
|
36
|
+
│ repair-listeners│ │ scans │ │ doctor command │
|
|
37
|
+
└───┬────────┬───┘ └────┬─────┘ └─┬──────────────┘
|
|
38
|
+
│ │ │ │
|
|
39
|
+
┌────────▼──┐ ┌──▼────────┐ │ ┌──────▼──────────┐
|
|
40
|
+
│model-repair│ │ tool/carr.│ │ │install-support │
|
|
41
|
+
└────┬──────┘ └──┬────────┘ │ └──────┬──────────┘
|
|
42
|
+
│ │ │ │
|
|
43
|
+
┌────▼───────────▼───────────▼───────────▼────┐
|
|
44
|
+
│ 领域层:classify / semver / compat / │
|
|
45
|
+
│ compat-repair / compat-env / arbitrate│
|
|
46
|
+
└────┬────────────────────────────────────────┘
|
|
47
|
+
│
|
|
48
|
+
┌────▼───────────────────────────────────────┐
|
|
49
|
+
│ 基础设施:timers / sleep / stat-cache / │
|
|
50
|
+
│ runtime-probe / diagnostics / │
|
|
51
|
+
│ retry-ledger │
|
|
52
|
+
└────────────────────────────────────────────┘
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
依赖只向下。领域层不感知 Cordis;基础设施层只提供无状态能力或显式注入的状态。
|
|
56
|
+
|
|
57
|
+
### 2.2 模块清单
|
|
58
|
+
|
|
59
|
+
| 模块 | 层级 | 职责 |
|
|
60
|
+
| --- | --- | --- |
|
|
61
|
+
| `index.js` | 组合根 | 组装依赖、注册扩展点、调度扫描、注册命令、释放资源 |
|
|
62
|
+
| `lib/repair-listeners.js` | 编排 | 工具重放与载体 503 的处理,并转发模型请求给 `model-repair` |
|
|
63
|
+
| `lib/model-repair.js` | 编排 | 模型请求分类、修复、退避与重试决策 |
|
|
64
|
+
| `lib/version-repair.js` | 编排 | 审计、适配、重载不兼容插件 |
|
|
65
|
+
| `lib/plugin-discovery.js` | 编排 | 从四个来源找出需要处理的插件 |
|
|
66
|
+
| `lib/env-verify.js` | 领域 | 核对兼容环境是否真能提供插件要的符号 |
|
|
67
|
+
| `lib/package-fetch.js` | 基础设施 | 从注册表取指定版本的包并解压 |
|
|
68
|
+
| `lib/version-search.js` | 领域 | 决定按什么顺序去试哪个版本 |
|
|
69
|
+
| `lib/cache-store.js` | 基础设施 | 下载缓存的维护与淘汰 |
|
|
70
|
+
| `lib/install-support.js` | 编排 | 本地目录的安装前预置 |
|
|
71
|
+
| `lib/install-bundle.js` | 编排 | 注册表包的自行安装(取包、依赖、环境、登记) |
|
|
72
|
+
| `lib/install-rescue.js` | 编排 | 授权版本豁免、重装、装完补齐兼容形态 |
|
|
73
|
+
| `lib/classify.js` | 领域 | 失败分类与重试策略解析 |
|
|
74
|
+
| `lib/semver.js` | 领域 | 版本解析、比较、范围满足判定 |
|
|
75
|
+
| `lib/compat.js` | 领域 | 兼容性判定、源码符号提取 |
|
|
76
|
+
| `lib/compat-repair.js` | 领域 | 模块图扫描、单插件审计 |
|
|
77
|
+
| `lib/compat-env.js` | 领域 | 兼容环境构建、manifest 对齐与还原 |
|
|
78
|
+
| `lib/arbitrate.js` | 领域 | 路由冲突描述(只报告) |
|
|
79
|
+
| `lib/timers.js` | 基础设施 | 脱离 HMR 事务的定时器 |
|
|
80
|
+
| `lib/sleep.js` | 基础设施 | 可中断延时 |
|
|
81
|
+
| `lib/stat-cache.js` | 基础设施 | 按文件签名缓存 manifest 与模块图 |
|
|
82
|
+
| `lib/runtime-probe.js` | 基础设施 | 运行时版本、profile、包目录、provider 注册表 |
|
|
83
|
+
| `lib/diagnostics.js` | 基础设施 | 计数、事件日志、快照写入 |
|
|
84
|
+
| `lib/retry-ledger.js` | 基础设施 | 每 agent 重试链预算与事件记录 |
|
|
85
|
+
| `lib/symbol-alias.js` | 领域 | 符号别名表:把旧名映射到现行实现里的等价导出 |
|
|
86
|
+
| `client.js` | 界面 | 插件面板里的「不兼容安装」页:调 Host 路由,负责展示与触发 |
|
|
87
|
+
|
|
88
|
+
### 2.3 并发模型
|
|
89
|
+
|
|
90
|
+
- 单线程事件循环。
|
|
91
|
+
- 版本扫描以布尔标志互斥,进行中的扫描不被第二次扫描打断。
|
|
92
|
+
- 事件驱动的扫描请求合并:同一时刻最多一个待执行扫描。
|
|
93
|
+
- 定时器为 `unref`,不阻止进程退出。
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## 3 模块设计
|
|
98
|
+
|
|
99
|
+
### 3.1 组合根 `index.js`
|
|
100
|
+
|
|
101
|
+
导出 `name` 与 `apply(ctx, rawConfig)`。
|
|
102
|
+
|
|
103
|
+
`apply` 的执行顺序:
|
|
104
|
+
|
|
105
|
+
1. 解析配置;`autoRepair` 为假则记录日志并直接返回。
|
|
106
|
+
2. 建立 `AbortController`(插件生命周期)、定时器集合、定时器助手。
|
|
107
|
+
3. 建立诊断、运行时探针、重试账本、版本修复器、修复监听器。
|
|
108
|
+
4. 注册三类事件监听:
|
|
109
|
+
- `agent/request-error`(局部)
|
|
110
|
+
- `tools/execute`(局部)
|
|
111
|
+
- `connection/request`(全局:载体在兄弟上下文)
|
|
112
|
+
5. 注册扫描调度(`app-boot/config-reload`、`plugin-manager/changed`,均为全局事件)。
|
|
113
|
+
6. 注入 `commands` 注册 `/doctor` 命令;注入 `webServer` 注册界面用的三条路由。
|
|
114
|
+
7. 注册释放钩子:中止生命周期、停止扫描、清空定时器、刷新诊断。
|
|
115
|
+
|
|
116
|
+
### 3.2 `lib/model-repair.js`
|
|
117
|
+
|
|
118
|
+
| 接口 | 说明 |
|
|
119
|
+
| --- | --- |
|
|
120
|
+
| `createModelRepair(input)` | 返回 `{ handle }` |
|
|
121
|
+
| `handle(payload, next)` | 处理一次请求失败 |
|
|
122
|
+
|
|
123
|
+
`handle` 流程:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
await next() // 先让下游决定
|
|
127
|
+
下游要求重试 → 直接返回(尊重 provider 策略)
|
|
128
|
+
信号已中止 → 返回下游结果
|
|
129
|
+
分类失败 → 非客户端原因或 skip 策略 → 返回下游结果
|
|
130
|
+
strategy = version-repair → 触发版本修复;未修复则放弃
|
|
131
|
+
strategy = compact → 压缩上下文 + 裁剪工具结果
|
|
132
|
+
scheduleRetry:
|
|
133
|
+
取预算条目,计数 +1
|
|
134
|
+
超过上限 → 首次超限记录 give-up,返回下游结果
|
|
135
|
+
计算退避 → 记录 llm/retry → 等待(可中断)
|
|
136
|
+
记录 llm/retry-started → 返回 { kind: 'retry' }
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### 3.3 `lib/repair-listeners.js`
|
|
140
|
+
|
|
141
|
+
| 接口 | 说明 |
|
|
142
|
+
| --- | --- |
|
|
143
|
+
| `createRepairListeners(input)` | 返回三个监听函数 |
|
|
144
|
+
|
|
145
|
+
- `onToolExecute`:`attemptDispatch` 判定一次执行是否需要重放;需要则退避后重来,直到预算耗尽或错误不可重放。
|
|
146
|
+
- `onConnectionRequest`:响应头未发出且为瞬时传输错误时写 503;否则原样抛出。
|
|
147
|
+
|
|
148
|
+
### 3.4 `lib/version-repair.js` 与 `lib/plugin-discovery.js`
|
|
149
|
+
|
|
150
|
+
| 接口 | 说明 |
|
|
151
|
+
| --- | --- |
|
|
152
|
+
| `createVersionRepair(input)` | 返回 `{ run, isBusy }` |
|
|
153
|
+
| `run(reason, manager?, attempt?)` | 执行一轮版本修复 |
|
|
154
|
+
| `collectMismatched(bundles, facts, plugins, probe)` | 收集所有需要处理的插件(在 `plugin-discovery`) |
|
|
155
|
+
|
|
156
|
+
内部阶段:
|
|
157
|
+
|
|
158
|
+
1. **发现** `collectMismatched`:合并四类候选并按包名去重。
|
|
159
|
+
- `deferredCandidates`:有 staging 标记且未建环境的包
|
|
160
|
+
- `refusedCandidates`:Loader 已拒绝的包(含其算出的未满足集合)
|
|
161
|
+
- `installedCandidates`:清单里已存在的插件中,声明未满足或环境缺件者
|
|
162
|
+
- `scannedCandidates`:直接扫 profile 的 `node_modules`,补上 `listPlugins()` 看不到的插件
|
|
163
|
+
(加载失败的不在 Loader 树里,恰恰最需要处理;`link:` 安装的是符号链接,也要算)
|
|
164
|
+
2. **审计** `auditAll` → `auditOne`:逐包调用 `auditInstalledPlugin`,异常降级为 `review`。
|
|
165
|
+
3. **仲裁** `providerRoster` + `describeContention`:只生成报告条目。
|
|
166
|
+
4. **适配** `adapt`:构建兼容环境、对齐 manifest、**核对环境**。
|
|
167
|
+
5. **重载**:对本次适配成功的包调用 `setBundleEnabled(name, true)`;检测到 HMR 嵌套错误则退避重试。
|
|
168
|
+
|
|
169
|
+
适配之后的核对是必要的一步。建好环境不等于能用:垫片少写一个导出,插件在加载那一刻
|
|
170
|
+
就会崩,而修复流程还以为成功了。`verifyEnvironment` 逐条确认插件要的每个符号环境是否真能
|
|
171
|
+
给出来,通不过就记为未修复而不是谎报成功。
|
|
172
|
+
|
|
173
|
+
### 3.4a `lib/env-verify.js`
|
|
174
|
+
|
|
175
|
+
| 接口 | 说明 |
|
|
176
|
+
| --- | --- |
|
|
177
|
+
| `verifyEnvironment({pluginDir, imports})` | 核对整个环境,返回 `{ok, problems, unverified}` |
|
|
178
|
+
| `verifyPackage(pluginDir, name, symbols)` | 核对单个包,返回 `provided` / `missing` / `unverifiable` |
|
|
179
|
+
|
|
180
|
+
三种结论的区分很重要:
|
|
181
|
+
|
|
182
|
+
- `missing`:目标读得到,但确实没有这个符号。这是真问题。
|
|
183
|
+
- `unverifiable`:目标读不到(运行时包常装在归档里)。不算失败,只是这次核对不了。
|
|
184
|
+
- `provided`:能找到。
|
|
185
|
+
|
|
186
|
+
`default` 需要单独判断——`export *` 从不转发默认导出,目标有 `default` 不等于插件拿得到,
|
|
187
|
+
必须由垫片自己写出来。
|
|
188
|
+
|
|
189
|
+
### 3.4b `lib/package-fetch.js`
|
|
190
|
+
|
|
191
|
+
| 接口 | 说明 |
|
|
192
|
+
| --- | --- |
|
|
193
|
+
| `fetchPackage(name, version, destDir)` | 从注册表取指定版本,解压到目标目录,返回 `{ok, error?}` |
|
|
194
|
+
|
|
195
|
+
兼容模式的落地靠这个模块。插件当初是对着某个版本写的,那个版本的代码还在仓库里,取下来装上就行——跑的是它认识的那份实现,不是转发到今天实现的替身。
|
|
196
|
+
|
|
197
|
+
实现是纯 Node:`fetch` 拿 tarball,`zlib.gunzipSync` 解压,自己解析 tar 头。不引第三方依赖,因为插件要在没有额外包的 profile 里运行。
|
|
198
|
+
|
|
199
|
+
解压时做了目录穿越防护:每个解出来的路径必须仍在目标目录内。
|
|
200
|
+
|
|
201
|
+
### 3.4c 旧版本的获取流程
|
|
202
|
+
|
|
203
|
+
1. `adapt` 先做一次 `dryRun` 探测:只问「能不能建」,一个字节不写。
|
|
204
|
+
2. 探测返回 `needs-old-version` 时进入版本搜索:先试插件声明的版本,再试声明范围内的其余版本,最后才试范围外的。第一个凑齐符号的版本胜出。取回的缓存在 `<profile>/.request-doctor-cache/`。
|
|
205
|
+
3. 取回了再建一次环境:符号在旧版本里确实有,就把整份旧实现放进去;旧版本也没有,如实报缺。
|
|
206
|
+
4. 旧版本自己的运行时依赖(`@deepseek-ai/*`,版本按声明钉死)也一起解析、一起装。只装它一个的话,它加载时 import 的兄弟包要么找不到,要么拿到新版本,两种都不是插件当初的环境。闭包最多 16 个包。
|
|
207
|
+
|
|
208
|
+
`dryRun` 不是可选项。探测调用如果落盘,在 profile 目录上执行时会把里面的真实包覆盖成垫片——一次只该读的检查,改掉了运行时依赖。
|
|
209
|
+
|
|
210
|
+
### 3.5 `lib/compat-env.js`
|
|
211
|
+
|
|
212
|
+
| 接口 | 说明 |
|
|
213
|
+
| --- | --- |
|
|
214
|
+
| `installCompatEnvironment(input)` | 构建环境,返回 `installed` / `unchanged` / `refused` / `failed` |
|
|
215
|
+
| `alignManifest(input)` | 对齐 DSH 依赖声明,返回 `aligned` / `already` / `skipped` / `failed` |
|
|
216
|
+
| `restoreManifest(dir)` | 还原原 manifest |
|
|
217
|
+
| `removeCompatEnvironment(dir)` | 只删除本插件创建的包并清理空目录 |
|
|
218
|
+
| `resolveInstalledDir(input)` | 定位真实包目录 |
|
|
219
|
+
| `pinnedVersion(range, fallback)` | 由范围推导可用的具体版本 |
|
|
220
|
+
|
|
221
|
+
### 3.6 `lib/install-support.js`
|
|
222
|
+
|
|
223
|
+
| 接口 | 说明 |
|
|
224
|
+
| --- | --- |
|
|
225
|
+
| `prepareForInstall(input)` | 安装前预置,返回 `{prepared, packages, deferred, reason}` |
|
|
226
|
+
| `classifySpec(spec, profileDir)` | 判定本地目录或远程包 |
|
|
227
|
+
| `abandonPreparation(dir)` | 回滚预置 |
|
|
228
|
+
| `readStagedPreparation(dir)` | 读取 staging 标记 |
|
|
229
|
+
| `clearStagedPreparation(dir)` | 清除 staging 标记 |
|
|
230
|
+
| `preparationSurvived(dir)` | 判断预置是否仍在 |
|
|
231
|
+
|
|
232
|
+
`prepareForInstall` 分解为:`inspectCandidate`(校验)→ `resetPreviousAttempt`(清残留)→ 构建 → 分支:
|
|
233
|
+
|
|
234
|
+
- 仅因运行时包不可见而拒绝 → `deferEnvironment`:只对齐 manifest,交给后续扫描建环境
|
|
235
|
+
- 成功 → `finalizePreparation`:对齐 manifest 并写 staging 标记
|
|
236
|
+
|
|
237
|
+
### 3.7 领域模块
|
|
238
|
+
|
|
239
|
+
- `lib/semver.js`:`parseSemver`、`compareVersions`、`satisfies`、`classifyDistance`。`satisfies` 支持 `=`、`>`、`>=`、`<`、`<=`、`^`、`~`、`x` 通配、空格与 `||` 组合。
|
|
240
|
+
- `lib/classify.js`:失败分类采用**规则表** `CLASSIFICATION_RULES`,首条匹配即返回。顺序编码优先级:provider 侧拒绝先于通用传输判定。
|
|
241
|
+
- `lib/compat.js`:`collectDshImports`(解析具名导入)、`collectProviderIds`、`collectIdLiterals`(要求 `id` 与 `region` 成对)、`parseExportedNames`、`auditPluginCompatibility`。
|
|
242
|
+
- `lib/compat-repair.js`:`collectPackageFacts` 遍历模块图,`walkModuleGraph` 限 40 个文件;`auditInstalledPlugin` 产出 verdict 与 `importMap`。
|
|
243
|
+
- `lib/arbitrate.js`:给定静态声明、冲突集与运行时注册表,描述争用关系,不产生任何环境变更。
|
|
244
|
+
|
|
245
|
+
### 3.8 基础设施模块
|
|
246
|
+
|
|
247
|
+
- `lib/timers.js`:`createTimers(ctx, timers)` → `{ later }`。通过 `hmr.executing.exit()` 脱离激活事务,否则插件管理器报 "HMR transactions cannot be nested"。
|
|
248
|
+
- `lib/sleep.js`:`cancellableSleep(ms, signal)`,中断返回 `false`。
|
|
249
|
+
- `lib/stat-cache.js`:`createManifestReader()`(上限 512)、`createFactsReader(collect)`(上限 256)。键为文件签名,变更自失效。
|
|
250
|
+
- `lib/runtime-probe.js`:`createRuntimeProbe(input)`,提供 `runtimeFacts`、`runtimeDirOf`、`runtimeEntryOf`、`activeProviders`、`factsOf`、`get`。
|
|
251
|
+
- `lib/diagnostics.js`:`createDiagnostics(input)`,250 ms 合并写入,日志上限 30 条。
|
|
252
|
+
- `lib/retry-ledger.js`:`createRetryLedger(input)`,每 agent 上限 128 条链。
|
|
253
|
+
|
|
254
|
+
---
|
|
255
|
+
|
|
256
|
+
## 4 关键算法与数据结构
|
|
257
|
+
|
|
258
|
+
### 4.1 兼容环境构建
|
|
259
|
+
|
|
260
|
+
对插件导入的每个 `@deepseek-ai/*` 包:
|
|
261
|
+
|
|
262
|
+
1. 定位真实包目录;找不到则记入 `missing` 并拒绝。
|
|
263
|
+
2. 读取真实包入口,解析其导出名集合。
|
|
264
|
+
3. 若插件使用了真实包中不存在的符号 → 拒绝,报告该符号。
|
|
265
|
+
4. 否则在 `<插件>/node_modules/<包名>/` 写入:
|
|
266
|
+
- `package.json`:`name` 为原包名,`version` 为目标旧版本,`type: module`,`exports` 指向 `index.js`
|
|
267
|
+
- `index.js`:`export * from '<真实入口 file URL>'`;仅当真实入口有默认导出时追加 `export { default } from ...`
|
|
268
|
+
5. 写入仅在内容变化时发生。
|
|
269
|
+
|
|
270
|
+
Node 的向上查找使嵌套 `node_modules` 遮蔽 profile 中的同名包,从而实现"给插件一个它期望的旧环境"。
|
|
271
|
+
|
|
272
|
+
### 4.2 Manifest 对齐
|
|
273
|
+
|
|
274
|
+
改写 `peerDependencies` 中所有 `@deepseek-ai/*` 项,以及 `engines.dsh`,目标为环境实际服务的版本(无环境时用运行时版本)。写入前若备份不存在则先备份;写入采用写临时文件后 `rename`,因为 pnpm 以硬链接方式把 manifest 放入 `node_modules`,原地写入会污染存储。
|
|
275
|
+
|
|
276
|
+
### 4.3 审计判定
|
|
277
|
+
|
|
278
|
+
`auditPluginCompatibility` 产出 `verdict`:
|
|
279
|
+
|
|
280
|
+
| verdict | 条件 |
|
|
281
|
+
| --- | --- |
|
|
282
|
+
| `compatible` | 无未满足 peer;或距离 ∈ {satisfied, prerelease-delta, patch-delta} 且导入符号齐全 |
|
|
283
|
+
| `incompatible` | 存在缺失符号,或距离 ∈ {major-delta} |
|
|
284
|
+
| `review` | 距离 ∈ {minor-delta, unknown},或无法读取源码 |
|
|
285
|
+
|
|
286
|
+
只有 `compatible` 会被自动适配。
|
|
287
|
+
|
|
288
|
+
### 4.4 退避计算
|
|
289
|
+
|
|
290
|
+
`computeDelay(config, attempt, failure)`:`initialDelayMs * 2^(attempt-1)`,上限 `maxDelayMs`,再乘以 `1 ± jitterRatio` 的随机因子。默认 400 ms、上限 20 s、抖动 25%。
|
|
291
|
+
|
|
292
|
+
### 4.5 主要数据结构
|
|
293
|
+
|
|
294
|
+
```js
|
|
295
|
+
// 重试预算条目
|
|
296
|
+
{ count: number, retryId: string }
|
|
297
|
+
|
|
298
|
+
// 运行时事实(每次扫描重建,缓存不在其中)
|
|
299
|
+
{ runtimeVersion, profileDir, packages, baseUrl }
|
|
300
|
+
|
|
301
|
+
// 审计行
|
|
302
|
+
{ name, version, runtimeVersion, requirements, deferred,
|
|
303
|
+
verdict, reasons, missing, imports, importMap, providers, conflicts, contention }
|
|
304
|
+
|
|
305
|
+
// 兼容环境标记
|
|
306
|
+
// .request-doctor-compat.json
|
|
307
|
+
{ runtimeVersion, packages: string[] }
|
|
308
|
+
|
|
309
|
+
// 安装预置标记
|
|
310
|
+
// .request-doctor-staging
|
|
311
|
+
{ runtimeVersion, packages: string[], deferred?, requirements? }
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
---
|
|
315
|
+
|
|
316
|
+
## 5 错误处理与恢复
|
|
317
|
+
|
|
318
|
+
| 情形 | 处置 |
|
|
319
|
+
| --- | --- |
|
|
320
|
+
| 可选服务缺失 | `ctx.get(name)` 包裹 try/catch,缺失即降级,不中断 |
|
|
321
|
+
| 单个插件不可读 | 该插件审计降级为 `review`,其余继续 |
|
|
322
|
+
| 环境构建被拒绝 | 记录 `compat-refused`,不重试 |
|
|
323
|
+
| 环境构建失败 | 记录 `compat-failed`,下轮扫描重试 |
|
|
324
|
+
| HMR 事务嵌套 | 退避后重试,上限 `maxHmrRetries` |
|
|
325
|
+
| 诊断写入失败 | 吞掉异常,诊断不得影响修复 |
|
|
326
|
+
| 插件释放 | 中止生命周期信号、清空定时器、刷新诊断 |
|
|
327
|
+
|
|
328
|
+
---
|
|
329
|
+
|
|
330
|
+
## 6 设计权衡
|
|
331
|
+
|
|
332
|
+
1. **不绕过检查而给环境**:直接写版本豁免会让插件在错误假设下运行;给环境使其声明与实现一致,是唯一能同时满足用户诉求与正确性的做法。
|
|
333
|
+
2. **只适配审计通过者**:把"声明不匹配"与"能否真正运行"分开判定,避免把不兼容插件伪装成可用。
|
|
334
|
+
3. **规则表驱动分类**:相较于长 `if` 链,规则表让优先级与判定粒度显式可读,也便于单测逐条覆盖。
|
|
335
|
+
4. **缓存键用文件签名**:不引入文件监听,避免监听器自身成为资源负担与竞态源;签名失配即重算。
|
|
336
|
+
5. **扫描合并与退避**:健康 profile 的稳态开销趋近于零,异常时立即回到基础周期。
|
|
337
|
+
6. **路由冲突只报告**:插件归属用户,自动化不应替用户决定停用哪一个。
|
|
338
|
+
|
|
339
|
+
### 2.6 界面层
|
|
340
|
+
|
|
341
|
+
界面是一个独立的客户端模块 `client.js`,通过 `dsh.client` 声明,注册到插件面板的标签栏与设置里插件页的标签。它不持有业务逻辑:清单从 Host 路由读,安装请求交给 Host 执行。
|
|
342
|
+
|
|
343
|
+
客户端与 Host 之间有两条通道,实际只用后者:
|
|
344
|
+
|
|
345
|
+
| 通道 | 用法 | 结论 |
|
|
346
|
+
| --- | --- | --- |
|
|
347
|
+
| typert 远程服务 | `ctx.remote.pluginManager` | 动态加载的客户端模块拿不到该门面,实测恒为 undefined |
|
|
348
|
+
| 同源 HTTP 路由 | `fetch('/doctor/...')` | 可用;与插件管理器自身界面同一机制 |
|
|
349
|
+
|
|
350
|
+
Host 侧提供三条路由:
|
|
351
|
+
|
|
352
|
+
| 方法 | 路径 | 作用 |
|
|
353
|
+
| --- | --- | --- |
|
|
354
|
+
| GET | `/doctor/incompatible` | 返回需要处理的插件:本插件审计结果 + 管理器拒装记录,去重 |
|
|
355
|
+
| POST | `/doctor/install` | 装一个包,复用 `/doctor install` 的实现 |
|
|
356
|
+
| GET | `/doctor/rescan` | 立即触发一轮审计 |
|
|
357
|
+
| POST | `/doctor/allow` | 放行一个插件版本,规则与 `/doctor allow` 相同 |
|
|
358
|
+
|
|
359
|
+
界面元素与数据流:面板读清单 → 每行一个「授权并安装」→ POST 安装路由 → Host 完成授权、重装、声明对齐 → 面板刷新清单。
|
|
360
|
+
|
|
361
|
+
### 2.7 失败分类与处置
|
|
362
|
+
|
|
363
|
+
分类的粒度按「修复动作」划分,不按报错原文划分:同一句话在不同上下文里可能要不同处理。
|
|
364
|
+
|
|
365
|
+
| 归类 | 处置策略 | 具体动作 |
|
|
366
|
+
| --- | --- | --- |
|
|
367
|
+
| `tls` | 放弃 | 证书问题重试不会变好 |
|
|
368
|
+
| `quota` / `policy` / `image` | 放弃 | 对方侧原因,不干预 |
|
|
369
|
+
| `local-blocked` | 放弃 | 权限、路径、磁盘、沙箱:缺的不是时机 |
|
|
370
|
+
| `auth` | 刷新凭据后重试 | 依次询问 `credentials` / `auth` / `accounts` 服务 |
|
|
371
|
+
| `model` | 刷新模型目录后重试 | 依次询问 `modelCatalog` / `models` / `providerRegistry` |
|
|
372
|
+
| `overflow` | 压缩后重试 | `compaction.compactIfNeeded` + `toolResultPruner.pruneSession` |
|
|
373
|
+
| `compaction-loop` / `history` | 修历史后重试 | 裁剪工具结果 + `sessionStore.flush` |
|
|
374
|
+
| `version-repair`(适配器缺失) | 修版本后重试 | 交给版本修复模块,未修成则不重试 |
|
|
375
|
+
| `local-env` / `stream` / `mcp` / `protocol` / `provider-error` / `timeout` / `empty` / `transport` | 退避重试 | 指数退避 + 抖动,受预算约束 |
|
|
376
|
+
|
|
377
|
+
### 2.8 三道防护
|
|
378
|
+
|
|
379
|
+
| 防护 | 机制 | 对应缺陷 |
|
|
380
|
+
| --- | --- | --- |
|
|
381
|
+
| 并发 | 同一 `turn:step:provider:kind` 同时只允许一个重试在飞,其余直接交还下游 | 并发控制 bug |
|
|
382
|
+
| 无进展 | 同一处连续两轮修复未见效即停手并记 `give-up` | 压缩自死循环、重试策略缺陷 |
|
|
383
|
+
| 循环 | 同名同参调用超过 `maxRepeatedToolCalls` 后不再重放 | 工具调用循环检测失效 |
|
|
384
|
+
|
|
385
|
+
### 2.9 预算分配
|
|
386
|
+
|
|
387
|
+
| 策略 | 额度 | 默认值 |
|
|
388
|
+
| --- | --- | --- |
|
|
389
|
+
| 普通重试 | `maxRetries` | 12 |
|
|
390
|
+
| 鉴权 | `maxAuthRetries` | 2 |
|
|
391
|
+
| 修历史 | `maxRetries` 的三分之一 | 4 |
|
|
392
|
+
| 工具重放 | `maxToolRetries` | 2 |
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 软件用户手册
|
|
2
2
|
|
|
3
3
|
**产品名称**:request-doctor(请求急救员)
|
|
4
|
-
**版本**:0.14.
|
|
4
|
+
**版本**:0.14.2
|
|
5
5
|
**编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.5
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -195,7 +195,13 @@ Plugin xxx@1.0.0 is incompatible with dsh 0.2.1-rc.3
|
|
|
195
195
|
```
|
|
196
196
|
|
|
197
197
|
看到这条说明修复本身已经完成,缺的只是一次重启——不要重复触发修复,那不会加快任何事。
|
|
198
|
-
## 8
|
|
198
|
+
## 8 界面上在哪操作
|
|
199
|
+
|
|
200
|
+
插件面板里有一个「不兼容安装」标签页,设置里的插件页也有同一个入口,两处内容一致。
|
|
201
|
+
|
|
202
|
+
页面顶部是输入框和两个按钮:「授权并安装」把包名或路径填进去点一下即可;「重新扫描」让 Host 立刻重算一轮审计。下面列出需要处理的插件。
|
|
203
|
+
|
|
204
|
+
## 9 界面里每行的来源
|
|
199
205
|
|
|
200
206
|
插件页那一栏每行都标出来源:
|
|
201
207
|
|
|
@@ -213,7 +219,7 @@ needs older api
|
|
|
213
219
|
```n
|
|
214
220
|
这样不必切到命令行敲 `/doctor why` 也能看清原因。
|
|
215
221
|
|
|
216
|
-
##
|
|
222
|
+
## 10 查某个插件为什么装不上
|
|
217
223
|
|
|
218
224
|
```
|
|
219
225
|
/doctor why dsh-hyper-tools
|
|
@@ -272,4 +278,24 @@ dsh-hyper-tools@0.1.0:incompatible
|
|
|
272
278
|
|
|
273
279
|
全部兼容时不会出现这句。
|
|
274
280
|
|
|
275
|
-
列表里只留环境提示的前半句,完整内容用 `/doctor why` 看。
|
|
281
|
+
列表里只留环境提示的前半句,完整内容用 `/doctor why` 看。
|
|
282
|
+
|
|
283
|
+
## 11 放行一个版本
|
|
284
|
+
|
|
285
|
+
插件本身没问题、只是 DSH 的版本检查没过时,先放行再装:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
/doctor allow dsh-ssh-ops@0.4.0
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
必须写清版本。放行之后在插件页点安装即可,声明对齐与兼容环境会自动补上。
|
|
292
|
+
|
|
293
|
+
界面上也能做这件事:当某个插件「审计说兼容、管理器仍拒装」时,那一行会多出一个按钮。
|
|
294
|
+
|
|
295
|
+
| 按钮 | 作用 |
|
|
296
|
+
| --- | --- |
|
|
297
|
+
| 只授权 | 只放行版本,不安装。放行后自己点安装 |
|
|
298
|
+
| 授权并安装 | 放行并立刻安装 |
|
|
299
|
+
| 重新扫描 | 让 Host 立刻重算一轮审计,不用等下一次定时扫描 |
|
|
300
|
+
|
|
301
|
+
「只授权」只出现在冲突行上。普通行只有「授权并安装」。
|
package/docs/quality-report.json
CHANGED