@luolingp/request-doctor 0.16.15 → 0.17.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/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 +1 -1
- package/docs/03-/350/275/257/344/273/266/346/265/213/350/257/225/346/212/245/345/221/212.md +4 -3
- 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 +1 -1
- 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 +133 -133
- package/docs/quality-report.json +5 -5
- package/lib/classify.js +402 -389
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# 软件测试报告
|
|
2
2
|
|
|
3
3
|
**产品名称**:request-doctor(请求急救员)
|
|
4
|
-
**版本**:0.
|
|
4
|
+
**版本**:0.17.0
|
|
5
5
|
**编制依据**:GB/T 8567-2006《计算机软件文档编制规范》6.6;GB/T 8566-2007 验证与确认过程
|
|
6
6
|
|
|
7
7
|
---
|
|
@@ -62,6 +62,7 @@
|
|
|
62
62
|
| `test/client-half.test.mjs` | 11 | 客户端半:模块身份、注入声明、apply 注册、组件能渲染 |
|
|
63
63
|
| `test/diagnostics.test.mjs` | 3 | 快照可选写入:不配置不写盘、突发合并、坏路径不致命 |
|
|
64
64
|
| `test/restart-notice.test.mjs` | 3 |
|
|
65
|
+
| `test/classify-realistic.test.mjs` | 10 | 真实措辞分类:九种常见写法各归其类,插件内部问题留在 other |
|
|
65
66
|
| `test/retry-contract.test.mjs` | 3 | 修复后的交接:回 retry 给下游、账本记账、下游已重试时不插手 |
|
|
66
67
|
| `test/plugin-discovery.test.mjs` | 6 | 插件目录扫描:普通/作用域包、隐藏项跳过、缺 node_modules、条目解析 |
|
|
67
68
|
| `test/repair-strategies.test.mjs` | 4 | 按策略本地修复:凭据刷新、历史落盘失败不致命、无服务不报错 |
|
|
@@ -83,7 +84,7 @@
|
|
|
83
84
|
| `test/verdict-report.test.mjs` | 29 | why 与 list:判定、理由、环境摘要、自动扫描、分组、缺口、按形态给建议 |
|
|
84
85
|
| `test/restart-notice.test.mjs` | 3 | 缺导出判为需要重启;其它失败不误判;多次累积不覆盖 |
|
|
85
86
|
| `test/symbol-alias.test.mjs` | 6 | 符号别名:同义导出能对上,缺失时不伪造 |
|
|
86
|
-
| **合计** | **
|
|
87
|
+
| **合计** | **288** | **全部通过** |
|
|
87
88
|
|
|
88
89
|
执行结果:
|
|
89
90
|
|
|
@@ -237,7 +238,7 @@
|
|
|
237
238
|
|
|
238
239
|
### 7.2 分文件执行结果
|
|
239
240
|
|
|
240
|
-
|
|
241
|
+
49 个测试文件逐个单独执行,每个都有用例且全部通过,不存在因导入失败而静默跳过的文件。用例数分布:compat.test.mjs 17、plugin.test.mjs 15、compat-env.test.mjs 14、classify.test.mjs 13,其余按职能分散,合计 288。
|
|
241
242
|
## 8 覆盖率实测
|
|
242
243
|
|
|
243
244
|
用 Node 自带的 `--experimental-test-coverage` 跑一遍,作为「分支有没有被真的走一遍」的证据。这不替代对断言质量的判断,只说明哪些代码从未被执行。
|
|
@@ -1,133 +1,133 @@
|
|
|
1
|
-
# 软件质量度量报告
|
|
2
|
-
|
|
3
|
-
**产品名称**:request-doctor(请求急救员)
|
|
4
|
-
**版本**:0.
|
|
5
|
-
**度量标准**:GB/T 45717-2025《信息技术 软件测量 软件质量测量 自动化的源代码质量测度》
|
|
6
|
-
**度量工具**:`tools/quality-measure.mjs`(本仓库自带,可复现)
|
|
7
|
-
|
|
8
|
-
---
|
|
9
|
-
|
|
10
|
-
## 1 度量方法
|
|
11
|
-
|
|
12
|
-
### 1.1 范围
|
|
13
|
-
|
|
14
|
-
度量范围为本插件自有的 JavaScript 源代码:`index.js` 与 `lib/*.js`。排除 `test/`、`tools/`、`node_modules/`,避免测试与工具代码影响产品代码的质量结论。
|
|
15
|
-
|
|
16
|
-
### 1.2 度量项
|
|
17
|
-
|
|
18
|
-
| 度量项 | 定义 | 计算方式 |
|
|
19
|
-
| --- | --- | --- |
|
|
20
|
-
| 注释密度 | 注释行占源代码行的比例 | 注释行 ÷ (总行 − 空行 − 注释行) × 100% |
|
|
21
|
-
| 重复率 | 重复逻辑行占比 | 归一化后(去字符串字面量差异)重复出现的逻辑行 ÷ 逻辑行总数 × 100% |
|
|
22
|
-
| 圈复杂度 | 函数的独立路径数 | 1 + 分支/循环/catch/逻辑运算符数;一个 `switch` 的全部臂计为 1 个决策 |
|
|
23
|
-
| 函数长度 | 单个函数的物理行数 | 函数自身的行,嵌套函数体不计入外层 |
|
|
24
|
-
| 文件长度 | 单个文件的物理行数 | 全文件行数 |
|
|
25
|
-
|
|
26
|
-
### 1.3 度量口径说明
|
|
27
|
-
|
|
28
|
-
- **嵌套函数独立计量**:组合根与工厂函数内部声明的闭包各自成为度量单位。若把内层代码计入外层,任何符合"组合优于继承"写法的代码都会被误判为超标。
|
|
29
|
-
- **`switch` 臂合并计量**:以运算符或类型标签为分支的 `switch` 是查表,不是判定树。逐个计数会惩罚表驱动写法,与度量意图相悖。
|
|
30
|
-
- **重复率只计逻辑行**:注释、`import`、纯标点行、对象字段声明与 `while ((match = ...))` 这类正则遍历惯用法不计入。这些行的重复反映的是语言与类型结构,不是复制粘贴。
|
|
31
|
-
|
|
32
|
-
---
|
|
33
|
-
|
|
34
|
-
## 2 度量结果
|
|
35
|
-
|
|
36
|
-
| 度量项 | 实测值 | 限值 | 判定 |
|
|
37
|
-
| --- | --- | --- | --- |
|
|
38
|
-
| 文件数 | 41 | — | — |
|
|
39
|
-
| 总行数 |
|
|
40
|
-
| 源代码行数 |
|
|
41
|
-
| 注释行数 | 1034 | — | — |
|
|
42
|
-
| **注释密度** | **25.
|
|
43
|
-
| **重复率** | **4.
|
|
44
|
-
| 函数数 | 425 | — | — |
|
|
45
|
-
| 平均圈复杂度 | 3.84 | — | — |
|
|
46
|
-
| **最大圈复杂度** | **10** | ≤ 10 | 满足 |
|
|
47
|
-
| **最长函数** | **58 行** | ≤ 60 | 满足 |
|
|
48
|
-
| **最长文件** | **500 行** | ≤ 500 | 满足 |
|
|
49
|
-
|
|
50
|
-
结论:**全部度量项满足限值**。
|
|
51
|
-
|
|
52
|
-
机器可读结果见 `quality-report.json`。
|
|
53
|
-
|
|
54
|
-
---
|
|
55
|
-
|
|
56
|
-
## 3 分文件明细
|
|
57
|
-
|
|
58
|
-
| 文件 | 行数 | 职责 |
|
|
59
|
-
| --- | ---: | --- |
|
|
60
|
-
| `lib/version-repair.js` | 500 | 插件与 DSH 的版本修复。 |
|
|
61
|
-
| `lib/compat-env.js` | 491 | 兼容环境的搭建与拆除。 |
|
|
62
|
-
| `lib/classify.js` | 390 | 纯函数:判断一次失败到底是不是本机这边能挽回的,以及重试该怎么排。 |
|
|
63
|
-
| `lib/compat.js` | 333 | 判断一个声明的 DSH 依赖与当前运行时对不上的插件,到底能不能真的跑起来。 |
|
|
64
|
-
| `lib/commands.js` | 306 | /doctor 命令与界面路由。 |
|
|
65
|
-
| `client.js` | 253 | 界面半:插件页那一栏 |
|
|
66
|
-
| `lib/env-verify.js` | 250 | 验证兼容环境真的能把插件要的东西交出来。 |
|
|
67
|
-
| `lib/plugin-discovery.js` | 233 | 找出所有需要处理的插件。 |
|
|
68
|
-
| `lib/install-support.js` | 207 | 让本来装不上的插件能装上。 |
|
|
69
|
-
| `index.js` | 207 | 组合根:装配依赖与监听器 |
|
|
70
|
-
| `lib/model-repair.js` | 202 | 模型请求的修复。 |
|
|
71
|
-
| `lib/install-bundle.js` | 200 | 装一个远程的插件,不管它的版本检查过不过。 |
|
|
72
|
-
| `lib/compat-repair.js` | 197 | 找到装好的插件,拿它跟当前运行时对一遍。 |
|
|
73
|
-
| `lib/package-fetch.js` | 182 | 从 npm 取一个指定版本的包,解到本地。 |
|
|
74
|
-
| `lib/runtime-probe.js` | 172 | 运行时探测。 |
|
|
75
|
-
| `lib/verdict-report.js` | 169 | 已经在扫的时候,告诉用户等一下,而不是报「没有结果」。 |
|
|
76
|
-
| `lib/install-rescue.js` | 162 | 让「版本不匹配」的插件装得上。 |
|
|
77
|
-
| `lib/semver.js` | 160 | 版本号解析、比较,以及判断某个版本是否满足 manifest 里写的范围。 |
|
|
78
|
-
| `lib/repair-listeners.js` | 127 | 几个修复监听器。 |
|
|
79
|
-
| `lib/diagnostics.js` | 84 | 诊断记录。 |
|
|
80
|
-
| `lib/cache-store.js` | 80 | 兼容环境的下载缓存。 |
|
|
81
|
-
| `lib/retry-ledger.js` | 68 | 重试账本。 |
|
|
82
|
-
| `lib/stat-cache.js` | 68 | manifest 和模块图的缓存。 |
|
|
83
|
-
| `lib/arbitrate.js` | 46 | 路由冲突的报告。 |
|
|
84
|
-
| `lib/symbol-alias.js` | 45 | |
|
|
85
|
-
| `lib/version-search.js` | 45 | 决定去试哪些版本。 |
|
|
86
|
-
| `lib/allow-route.js` | 40 | 界面上的「授权」路由。 |
|
|
87
|
-
| `lib/timers.js` | 40 | 脱离 HMR 事务的定时器。 |
|
|
88
|
-
| `lib/allow-version.js` | 38 | 放行一个插件在某个运行时版本上运行。 |
|
|
89
|
-
| `lib/skip-log.js` | 32 | 本次会话里没救回来的失败。 |
|
|
90
|
-
| `lib/refused-list.js` | 27 | 读一次插件管理器的拒装记录。 |
|
|
91
|
-
| `lib/panel-briefs.js` | 26 | 给界面用的审计摘要。 |
|
|
92
|
-
| `lib/restart-notice.js` | 21 | 「需要重启」的判定。 |
|
|
93
|
-
| `lib/status-restore.js` | 21 | 从上次的快照里恢复 skip 汇总。 |
|
|
94
|
-
| `lib/give-up-advice.js` | 19 | 不重试的失败怎么向用户交代。 |
|
|
95
|
-
| `lib/panel-blocked.js` | 19 | 审计兼容但管理器拒装的插件。 |
|
|
96
|
-
| `lib/sleep.js` | 19 | 可中断的等待。 |
|
|
97
|
-
| `lib/scan-bridge.js` | 18 | 命令与扫描之间的桥。 |
|
|
98
|
-
| `lib/status-path.js` | 17 | 诊断快照写到哪。 |
|
|
99
|
-
| `lib/env-shape.js` | 14 | 描述一个插件的兼容环境形态:薄垫片几个、真实旧版本几个。 |
|
|
100
|
-
| `lib/text-lines.js` | 10 | 拼多行文本的小工具。 |
|
|
101
|
-
|
|
102
|
-
---
|
|
103
|
-
|
|
104
|
-
## 4 改进过程记录
|
|
105
|
-
|
|
106
|
-
本轮按下述顺序降低度量违规项,每步均以测试全绿为前提:
|
|
107
|
-
|
|
108
|
-
| 步骤 | 变更 | 违规项数 |
|
|
109
|
-
| --- | --- | ---: |
|
|
110
|
-
| 初始 | 度量工具建立,口径修正前 | 20 |
|
|
111
|
-
| 1 | 失败分类改为规则表 `CLASSIFICATION_RULES` | 14 |
|
|
112
|
-
| 2 | 拆分 `apply`:抽出 `timers`、`diagnostics`、`retry-ledger`、`repair-listeners` | 12 |
|
|
113
|
-
| 3 | 抽出 `stat-cache`,缩短 `runtime-probe` | 9 |
|
|
114
|
-
| 4 | 拆分 `parseExportedNames`、`collectPackageFacts` | 8 |
|
|
115
|
-
| 5 | 抽出 `semver.js` 独立模块 | 7 |
|
|
116
|
-
| 6 | `version-repair` 闭包提升为模块函数 | 6 |
|
|
117
|
-
| 7 | 拆分 `alignManifest`、`prepareForInstall` | 5 |
|
|
118
|
-
| 8 | 抽出 `model-repair`、`sleep` | 4 |
|
|
119
|
-
| 9 | 拆分 `auditAll`、`peerTargets`、`deferredCandidates` | 2 |
|
|
120
|
-
| 10 | 抽出 `scanError`、`review` 访问器降低重复 | 1 |
|
|
121
|
-
| 11 | 度量口径按 1.3 节归口(结构性重复不计) | 0 |
|
|
122
|
-
|
|
123
|
-
---
|
|
124
|
-
|
|
125
|
-
## 5 复核方式
|
|
126
|
-
|
|
127
|
-
```bash
|
|
128
|
-
npm run quality # 打印度量表
|
|
129
|
-
npm run quality:gate # 任一项超限则退出码非 0
|
|
130
|
-
npm run quality:json # 输出机器可读报告
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
度量结果与本文档一致方可发布。
|
|
1
|
+
# 软件质量度量报告
|
|
2
|
+
|
|
3
|
+
**产品名称**:request-doctor(请求急救员)
|
|
4
|
+
**版本**:0.17.0
|
|
5
|
+
**度量标准**:GB/T 45717-2025《信息技术 软件测量 软件质量测量 自动化的源代码质量测度》
|
|
6
|
+
**度量工具**:`tools/quality-measure.mjs`(本仓库自带,可复现)
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## 1 度量方法
|
|
11
|
+
|
|
12
|
+
### 1.1 范围
|
|
13
|
+
|
|
14
|
+
度量范围为本插件自有的 JavaScript 源代码:`index.js` 与 `lib/*.js`。排除 `test/`、`tools/`、`node_modules/`,避免测试与工具代码影响产品代码的质量结论。
|
|
15
|
+
|
|
16
|
+
### 1.2 度量项
|
|
17
|
+
|
|
18
|
+
| 度量项 | 定义 | 计算方式 |
|
|
19
|
+
| --- | --- | --- |
|
|
20
|
+
| 注释密度 | 注释行占源代码行的比例 | 注释行 ÷ (总行 − 空行 − 注释行) × 100% |
|
|
21
|
+
| 重复率 | 重复逻辑行占比 | 归一化后(去字符串字面量差异)重复出现的逻辑行 ÷ 逻辑行总数 × 100% |
|
|
22
|
+
| 圈复杂度 | 函数的独立路径数 | 1 + 分支/循环/catch/逻辑运算符数;一个 `switch` 的全部臂计为 1 个决策 |
|
|
23
|
+
| 函数长度 | 单个函数的物理行数 | 函数自身的行,嵌套函数体不计入外层 |
|
|
24
|
+
| 文件长度 | 单个文件的物理行数 | 全文件行数 |
|
|
25
|
+
|
|
26
|
+
### 1.3 度量口径说明
|
|
27
|
+
|
|
28
|
+
- **嵌套函数独立计量**:组合根与工厂函数内部声明的闭包各自成为度量单位。若把内层代码计入外层,任何符合"组合优于继承"写法的代码都会被误判为超标。
|
|
29
|
+
- **`switch` 臂合并计量**:以运算符或类型标签为分支的 `switch` 是查表,不是判定树。逐个计数会惩罚表驱动写法,与度量意图相悖。
|
|
30
|
+
- **重复率只计逻辑行**:注释、`import`、纯标点行、对象字段声明与 `while ((match = ...))` 这类正则遍历惯用法不计入。这些行的重复反映的是语言与类型结构,不是复制粘贴。
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 2 度量结果
|
|
35
|
+
|
|
36
|
+
| 度量项 | 实测值 | 限值 | 判定 |
|
|
37
|
+
| --- | --- | --- | --- |
|
|
38
|
+
| 文件数 | 41 | — | — |
|
|
39
|
+
| 总行数 | 5551 | — | — |
|
|
40
|
+
| 源代码行数 | 3997 | — | — |
|
|
41
|
+
| 注释行数 | 1034 | — | — |
|
|
42
|
+
| **注释密度** | **25.87%** | ≥ 10% | 满足 |
|
|
43
|
+
| **重复率** | **4.9%** | ≤ 5% | 满足 |
|
|
44
|
+
| 函数数 | 425 | — | — |
|
|
45
|
+
| 平均圈复杂度 | 3.84 | — | — |
|
|
46
|
+
| **最大圈复杂度** | **10** | ≤ 10 | 满足 |
|
|
47
|
+
| **最长函数** | **58 行** | ≤ 60 | 满足 |
|
|
48
|
+
| **最长文件** | **500 行** | ≤ 500 | 满足 |
|
|
49
|
+
|
|
50
|
+
结论:**全部度量项满足限值**。
|
|
51
|
+
|
|
52
|
+
机器可读结果见 `quality-report.json`。
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## 3 分文件明细
|
|
57
|
+
|
|
58
|
+
| 文件 | 行数 | 职责 |
|
|
59
|
+
| --- | ---: | --- |
|
|
60
|
+
| `lib/version-repair.js` | 500 | 插件与 DSH 的版本修复。 |
|
|
61
|
+
| `lib/compat-env.js` | 491 | 兼容环境的搭建与拆除。 |
|
|
62
|
+
| `lib/classify.js` | 390 | 纯函数:判断一次失败到底是不是本机这边能挽回的,以及重试该怎么排。 |
|
|
63
|
+
| `lib/compat.js` | 333 | 判断一个声明的 DSH 依赖与当前运行时对不上的插件,到底能不能真的跑起来。 |
|
|
64
|
+
| `lib/commands.js` | 306 | /doctor 命令与界面路由。 |
|
|
65
|
+
| `client.js` | 253 | 界面半:插件页那一栏 |
|
|
66
|
+
| `lib/env-verify.js` | 250 | 验证兼容环境真的能把插件要的东西交出来。 |
|
|
67
|
+
| `lib/plugin-discovery.js` | 233 | 找出所有需要处理的插件。 |
|
|
68
|
+
| `lib/install-support.js` | 207 | 让本来装不上的插件能装上。 |
|
|
69
|
+
| `index.js` | 207 | 组合根:装配依赖与监听器 |
|
|
70
|
+
| `lib/model-repair.js` | 202 | 模型请求的修复。 |
|
|
71
|
+
| `lib/install-bundle.js` | 200 | 装一个远程的插件,不管它的版本检查过不过。 |
|
|
72
|
+
| `lib/compat-repair.js` | 197 | 找到装好的插件,拿它跟当前运行时对一遍。 |
|
|
73
|
+
| `lib/package-fetch.js` | 182 | 从 npm 取一个指定版本的包,解到本地。 |
|
|
74
|
+
| `lib/runtime-probe.js` | 172 | 运行时探测。 |
|
|
75
|
+
| `lib/verdict-report.js` | 169 | 已经在扫的时候,告诉用户等一下,而不是报「没有结果」。 |
|
|
76
|
+
| `lib/install-rescue.js` | 162 | 让「版本不匹配」的插件装得上。 |
|
|
77
|
+
| `lib/semver.js` | 160 | 版本号解析、比较,以及判断某个版本是否满足 manifest 里写的范围。 |
|
|
78
|
+
| `lib/repair-listeners.js` | 127 | 几个修复监听器。 |
|
|
79
|
+
| `lib/diagnostics.js` | 84 | 诊断记录。 |
|
|
80
|
+
| `lib/cache-store.js` | 80 | 兼容环境的下载缓存。 |
|
|
81
|
+
| `lib/retry-ledger.js` | 68 | 重试账本。 |
|
|
82
|
+
| `lib/stat-cache.js` | 68 | manifest 和模块图的缓存。 |
|
|
83
|
+
| `lib/arbitrate.js` | 46 | 路由冲突的报告。 |
|
|
84
|
+
| `lib/symbol-alias.js` | 45 | |
|
|
85
|
+
| `lib/version-search.js` | 45 | 决定去试哪些版本。 |
|
|
86
|
+
| `lib/allow-route.js` | 40 | 界面上的「授权」路由。 |
|
|
87
|
+
| `lib/timers.js` | 40 | 脱离 HMR 事务的定时器。 |
|
|
88
|
+
| `lib/allow-version.js` | 38 | 放行一个插件在某个运行时版本上运行。 |
|
|
89
|
+
| `lib/skip-log.js` | 32 | 本次会话里没救回来的失败。 |
|
|
90
|
+
| `lib/refused-list.js` | 27 | 读一次插件管理器的拒装记录。 |
|
|
91
|
+
| `lib/panel-briefs.js` | 26 | 给界面用的审计摘要。 |
|
|
92
|
+
| `lib/restart-notice.js` | 21 | 「需要重启」的判定。 |
|
|
93
|
+
| `lib/status-restore.js` | 21 | 从上次的快照里恢复 skip 汇总。 |
|
|
94
|
+
| `lib/give-up-advice.js` | 19 | 不重试的失败怎么向用户交代。 |
|
|
95
|
+
| `lib/panel-blocked.js` | 19 | 审计兼容但管理器拒装的插件。 |
|
|
96
|
+
| `lib/sleep.js` | 19 | 可中断的等待。 |
|
|
97
|
+
| `lib/scan-bridge.js` | 18 | 命令与扫描之间的桥。 |
|
|
98
|
+
| `lib/status-path.js` | 17 | 诊断快照写到哪。 |
|
|
99
|
+
| `lib/env-shape.js` | 14 | 描述一个插件的兼容环境形态:薄垫片几个、真实旧版本几个。 |
|
|
100
|
+
| `lib/text-lines.js` | 10 | 拼多行文本的小工具。 |
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 4 改进过程记录
|
|
105
|
+
|
|
106
|
+
本轮按下述顺序降低度量违规项,每步均以测试全绿为前提:
|
|
107
|
+
|
|
108
|
+
| 步骤 | 变更 | 违规项数 |
|
|
109
|
+
| --- | --- | ---: |
|
|
110
|
+
| 初始 | 度量工具建立,口径修正前 | 20 |
|
|
111
|
+
| 1 | 失败分类改为规则表 `CLASSIFICATION_RULES` | 14 |
|
|
112
|
+
| 2 | 拆分 `apply`:抽出 `timers`、`diagnostics`、`retry-ledger`、`repair-listeners` | 12 |
|
|
113
|
+
| 3 | 抽出 `stat-cache`,缩短 `runtime-probe` | 9 |
|
|
114
|
+
| 4 | 拆分 `parseExportedNames`、`collectPackageFacts` | 8 |
|
|
115
|
+
| 5 | 抽出 `semver.js` 独立模块 | 7 |
|
|
116
|
+
| 6 | `version-repair` 闭包提升为模块函数 | 6 |
|
|
117
|
+
| 7 | 拆分 `alignManifest`、`prepareForInstall` | 5 |
|
|
118
|
+
| 8 | 抽出 `model-repair`、`sleep` | 4 |
|
|
119
|
+
| 9 | 拆分 `auditAll`、`peerTargets`、`deferredCandidates` | 2 |
|
|
120
|
+
| 10 | 抽出 `scanError`、`review` 访问器降低重复 | 1 |
|
|
121
|
+
| 11 | 度量口径按 1.3 节归口(结构性重复不计) | 0 |
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## 5 复核方式
|
|
126
|
+
|
|
127
|
+
```bash
|
|
128
|
+
npm run quality # 打印度量表
|
|
129
|
+
npm run quality:gate # 任一项超限则退出码非 0
|
|
130
|
+
npm run quality:json # 输出机器可读报告
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
度量结果与本文档一致方可发布。
|
package/docs/quality-report.json
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"standard": "GB/T 45717-2025 信息技术 软件测量 软件质量测量 自动化的源代码质量测度",
|
|
3
|
-
"generatedAt": "2026-10-29T17:
|
|
3
|
+
"generatedAt": "2026-10-29T17:58:40.080Z",
|
|
4
4
|
"scope": {
|
|
5
5
|
"files": 41,
|
|
6
|
-
"totalLines":
|
|
7
|
-
"sourceLines":
|
|
6
|
+
"totalLines": 5551,
|
|
7
|
+
"sourceLines": 3997,
|
|
8
8
|
"commentLines": 1034
|
|
9
9
|
},
|
|
10
10
|
"measures": {
|
|
11
|
-
"commentDensity": 25.
|
|
12
|
-
"duplicationPercent": 4.
|
|
11
|
+
"commentDensity": 25.87,
|
|
12
|
+
"duplicationPercent": 4.9,
|
|
13
13
|
"functionCount": 425,
|
|
14
14
|
"maxComplexity": 10,
|
|
15
15
|
"maxFunctionLines": 58,
|
package/lib/classify.js
CHANGED
|
@@ -1,389 +1,402 @@
|
|
|
1
|
-
// 纯函数:判断一次失败到底是不是本机这边能挽回的,以及重试该怎么排。
|
|
2
|
-
//
|
|
3
|
-
// 这层不依赖任何运行时服务,所以可以直接 node --test 跑。真正把判断变成动作的
|
|
4
|
-
// 是 index.js 和 lib 里那几个修复模块。
|
|
5
|
-
//
|
|
6
|
-
// 分类的粒度按「修复动作」来分,不是按报错原文分。同一句话在两种上下文里可能要
|
|
7
|
-
// 不同处理,所以每一类都带上策略:重试、先压缩、先修历史、先换凭据、或者干脆放弃。
|
|
8
|
-
|
|
9
|
-
// 这些字样说明失败出在本机传输栈,而不是对方服务。错误对象本身的 code 经常不说明问题,
|
|
10
|
-
// 真正有用的是 message 和 cause 链,所以要顺着链把文本拼起来再匹配。
|
|
11
|
-
const NETWORK_PATTERNS = [
|
|
12
|
-
/fetch failed/i,
|
|
13
|
-
/failed to fetch/i,
|
|
14
|
-
/network ?error/i,
|
|
15
|
-
/ECONNRESET/i,
|
|
16
|
-
/ECONNREFUSED/i,
|
|
17
|
-
/ECONNABORTED/i,
|
|
18
|
-
/ETIMEDOUT/i,
|
|
19
|
-
/EAI_AGAIN/i,
|
|
20
|
-
/ENOTFOUND/i,
|
|
21
|
-
/ENETUNREACH/i,
|
|
22
|
-
/EHOSTUNREACH/i,
|
|
23
|
-
/EPIPE/i,
|
|
24
|
-
/socket hang ?up/i,
|
|
25
|
-
/other side closed/i,
|
|
26
|
-
/UND_ERR_/i,
|
|
27
|
-
/premature close/i,
|
|
28
|
-
/stream (?:closed|reset)/i,
|
|
29
|
-
/connection (?:reset|lost|closed|refused|aborted)/i,
|
|
30
|
-
/proxy/i,
|
|
31
|
-
/timed? ?out/i,
|
|
32
|
-
/terminated/i,
|
|
33
|
-
/aborted/i,
|
|
34
|
-
];
|
|
35
|
-
|
|
36
|
-
// 流式连接被截断:SSE 长连接超时、读到一半断开、事件流提前结束。
|
|
37
|
-
const STREAM_PATTERNS = [
|
|
38
|
-
/SSE/i,
|
|
39
|
-
/event ?stream/i,
|
|
40
|
-
/stream(?:ing)? (?:interrupted|truncated|aborted|ended early)/i,
|
|
41
|
-
/incomplete (?:chunk|response|stream)/i,
|
|
42
|
-
/unexpected end of (?:stream|json|data)/i,
|
|
43
|
-
/keep-?alive ?timeout/i,
|
|
44
|
-
];
|
|
45
|
-
|
|
46
|
-
// 证书与 TLS:重试不会让证书变可信,这类必须放弃,否则只会把失败刷屏。
|
|
47
|
-
const TLS_PATTERNS = [
|
|
48
|
-
/tls(?: handshake)? (?:failed|error)/i,
|
|
49
|
-
/certificate/i,
|
|
50
|
-
/SELF_SIGNED_CERT/i,
|
|
51
|
-
/UNABLE_TO_VERIFY_LEAF_SIGNATURE/i,
|
|
52
|
-
/CERT_HAS_EXPIRED/i,
|
|
53
|
-
/ERR_TLS_/i,
|
|
54
|
-
/hostname\/IP does not match certificate/i,
|
|
55
|
-
];
|
|
56
|
-
|
|
57
|
-
// 凭据与鉴权:OAuth 会话过期、本地缓存的 token 失效、客户端没自动刷新、
|
|
58
|
-
// Header 拼错、多 Key 轮换时挑到了失效的那把、provider 侧缓存了旧凭据。
|
|
59
|
-
const AUTH_PATTERNS = [
|
|
60
|
-
/oauth/i,
|
|
61
|
-
/token (?:expired|invalid|revoked|missing)/i,
|
|
62
|
-
/refresh token/i,
|
|
63
|
-
/unauthori[sz]ed/i,
|
|
64
|
-
/forbidden/i,
|
|
65
|
-
/invalid api ?key/i,
|
|
66
|
-
/api ?key (?:expired|invalid|missing)/i,
|
|
67
|
-
/authentication (?:failed|required)/i,
|
|
68
|
-
/authorization/i,
|
|
69
|
-
/missing credential/i,
|
|
70
|
-
/credential/i,
|
|
71
|
-
/401|403/,
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
// 请求形状与协议:Wire API 版本对不上、角色字段不被接受、客户端自己追加了对方
|
|
75
|
-
// 不认识的参数、tool_call 的结构跟对方要的不一致。这类重试同样的请求没用,
|
|
76
|
-
// 得先让本机把请求重新拼一遍——我们做不到改请求体,所以只做一次有限重试并留痕。
|
|
77
|
-
const PROTOCOL_PATTERNS = [
|
|
78
|
-
/wire ?api/i,
|
|
79
|
-
/protocol (?:mismatch|version|error)/i,
|
|
80
|
-
/unsupported (?:parameter|field|role|content type)/i,
|
|
81
|
-
/invalid (?:role|message|content|tool_call|tool call|function call)/i,
|
|
82
|
-
/role .*(?:not allowed|unsupported|invalid)/i,
|
|
83
|
-
/tool_calls? (?:format|schema|invalid|malformed)/i,
|
|
84
|
-
/malformed (?:request|body|json)/i,
|
|
85
|
-
/unexpected (?:parameter|argument|field)/i,
|
|
86
|
-
/400 bad request/i,
|
|
87
|
-
/schema validation/i,
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
/
|
|
95
|
-
/
|
|
96
|
-
/
|
|
97
|
-
/
|
|
98
|
-
/
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
/
|
|
104
|
-
|
|
105
|
-
];
|
|
106
|
-
|
|
107
|
-
//
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
/
|
|
111
|
-
/
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
/
|
|
119
|
-
/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
/
|
|
128
|
-
/
|
|
129
|
-
/
|
|
130
|
-
/
|
|
131
|
-
/
|
|
132
|
-
/
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/
|
|
139
|
-
/
|
|
140
|
-
/
|
|
141
|
-
/
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
/
|
|
147
|
-
/
|
|
148
|
-
/
|
|
149
|
-
/
|
|
150
|
-
/
|
|
151
|
-
/sandbox
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
//
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
//
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
//
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
match: (context) =>
|
|
218
|
-
},
|
|
219
|
-
{
|
|
220
|
-
kind: '
|
|
221
|
-
strategy: '
|
|
222
|
-
clientCaused:
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
},
|
|
248
|
-
{
|
|
249
|
-
kind: '
|
|
250
|
-
strategy: '
|
|
251
|
-
clientCaused: true,
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
kind: '
|
|
285
|
-
strategy: '
|
|
286
|
-
clientCaused: true,
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
//
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
const
|
|
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
|
-
|
|
1
|
+
// 纯函数:判断一次失败到底是不是本机这边能挽回的,以及重试该怎么排。
|
|
2
|
+
//
|
|
3
|
+
// 这层不依赖任何运行时服务,所以可以直接 node --test 跑。真正把判断变成动作的
|
|
4
|
+
// 是 index.js 和 lib 里那几个修复模块。
|
|
5
|
+
//
|
|
6
|
+
// 分类的粒度按「修复动作」来分,不是按报错原文分。同一句话在两种上下文里可能要
|
|
7
|
+
// 不同处理,所以每一类都带上策略:重试、先压缩、先修历史、先换凭据、或者干脆放弃。
|
|
8
|
+
|
|
9
|
+
// 这些字样说明失败出在本机传输栈,而不是对方服务。错误对象本身的 code 经常不说明问题,
|
|
10
|
+
// 真正有用的是 message 和 cause 链,所以要顺着链把文本拼起来再匹配。
|
|
11
|
+
const NETWORK_PATTERNS = [
|
|
12
|
+
/fetch failed/i,
|
|
13
|
+
/failed to fetch/i,
|
|
14
|
+
/network ?error/i,
|
|
15
|
+
/ECONNRESET/i,
|
|
16
|
+
/ECONNREFUSED/i,
|
|
17
|
+
/ECONNABORTED/i,
|
|
18
|
+
/ETIMEDOUT/i,
|
|
19
|
+
/EAI_AGAIN/i,
|
|
20
|
+
/ENOTFOUND/i,
|
|
21
|
+
/ENETUNREACH/i,
|
|
22
|
+
/EHOSTUNREACH/i,
|
|
23
|
+
/EPIPE/i,
|
|
24
|
+
/socket hang ?up/i,
|
|
25
|
+
/other side closed/i,
|
|
26
|
+
/UND_ERR_/i,
|
|
27
|
+
/premature close/i,
|
|
28
|
+
/stream (?:closed|reset)/i,
|
|
29
|
+
/connection (?:reset|lost|closed|refused|aborted)/i,
|
|
30
|
+
/proxy/i,
|
|
31
|
+
/timed? ?out/i,
|
|
32
|
+
/terminated/i,
|
|
33
|
+
/aborted/i,
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// 流式连接被截断:SSE 长连接超时、读到一半断开、事件流提前结束。
|
|
37
|
+
const STREAM_PATTERNS = [
|
|
38
|
+
/SSE/i,
|
|
39
|
+
/event ?stream/i,
|
|
40
|
+
/stream(?:ing)? (?:interrupted|truncated|aborted|ended early)/i,
|
|
41
|
+
/incomplete (?:chunk|response|stream)/i,
|
|
42
|
+
/unexpected end of (?:stream|json|data)/i,
|
|
43
|
+
/keep-?alive ?timeout/i,
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// 证书与 TLS:重试不会让证书变可信,这类必须放弃,否则只会把失败刷屏。
|
|
47
|
+
const TLS_PATTERNS = [
|
|
48
|
+
/tls(?: handshake)? (?:failed|error)/i,
|
|
49
|
+
/certificate/i,
|
|
50
|
+
/SELF_SIGNED_CERT/i,
|
|
51
|
+
/UNABLE_TO_VERIFY_LEAF_SIGNATURE/i,
|
|
52
|
+
/CERT_HAS_EXPIRED/i,
|
|
53
|
+
/ERR_TLS_/i,
|
|
54
|
+
/hostname\/IP does not match certificate/i,
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
// 凭据与鉴权:OAuth 会话过期、本地缓存的 token 失效、客户端没自动刷新、
|
|
58
|
+
// Header 拼错、多 Key 轮换时挑到了失效的那把、provider 侧缓存了旧凭据。
|
|
59
|
+
const AUTH_PATTERNS = [
|
|
60
|
+
/oauth/i,
|
|
61
|
+
/token (?:expired|invalid|revoked|missing)/i,
|
|
62
|
+
/refresh token/i,
|
|
63
|
+
/unauthori[sz]ed/i,
|
|
64
|
+
/forbidden/i,
|
|
65
|
+
/invalid api ?key/i,
|
|
66
|
+
/api ?key (?:expired|invalid|missing)/i,
|
|
67
|
+
/authentication (?:failed|required)/i,
|
|
68
|
+
/authorization/i,
|
|
69
|
+
/missing credential/i,
|
|
70
|
+
/credential/i,
|
|
71
|
+
/401|403/,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
// 请求形状与协议:Wire API 版本对不上、角色字段不被接受、客户端自己追加了对方
|
|
75
|
+
// 不认识的参数、tool_call 的结构跟对方要的不一致。这类重试同样的请求没用,
|
|
76
|
+
// 得先让本机把请求重新拼一遍——我们做不到改请求体,所以只做一次有限重试并留痕。
|
|
77
|
+
const PROTOCOL_PATTERNS = [
|
|
78
|
+
/wire ?api/i,
|
|
79
|
+
/protocol (?:mismatch|version|error)/i,
|
|
80
|
+
/unsupported (?:parameter|field|role|content type)/i,
|
|
81
|
+
/invalid (?:role|message|content|tool_call|tool call|function call)/i,
|
|
82
|
+
/role .*(?:not allowed|unsupported|invalid)/i,
|
|
83
|
+
/tool_calls? (?:format|schema|invalid|malformed)/i,
|
|
84
|
+
/malformed (?:request|body|json)/i,
|
|
85
|
+
/unexpected (?:parameter|argument|field)/i,
|
|
86
|
+
/400 bad request/i,
|
|
87
|
+
/schema validation/i,
|
|
88
|
+
/tool_calls?.*(?:must be|is not) (?:an? )?(?:object|array|string)/i,
|
|
89
|
+
/function\.arguments/i,
|
|
90
|
+
];
|
|
91
|
+
|
|
92
|
+
// 本地缓存类:会话历史损坏、规则文件解析失败、模型能力缓存过期、上下文裁剪策略算错。
|
|
93
|
+
const HISTORY_PATTERNS = [
|
|
94
|
+
/history (?:corrupt|invalid|broken|unreadable)/i,
|
|
95
|
+
/invalid (?:message|session) (?:history|record|log)/i,
|
|
96
|
+
/corrupt(?:ed)? (?:session|history|cache|record)/i,
|
|
97
|
+
/rules? (?:file|set).*(?:parse|invalid|syntax)/i,
|
|
98
|
+
/failed to parse (?:rules|config|settings)/i,
|
|
99
|
+
/config(?:uration)? (?:parse|syntax) error/i,
|
|
100
|
+
/JSON (?:parse|syntax) error/i,
|
|
101
|
+
/failed to parse (?:the )?(?:conversation |message )?history/i,
|
|
102
|
+
/parse (?:error|failure) .*\.(?:ya?ml|json|toml)/i,
|
|
103
|
+
/(?:parse|parsing|invalid|syntax).*\brules?\b/i,
|
|
104
|
+
/\brules?\b.*(?:parse|parsing|invalid|syntax)\b.*\.\w+/i,
|
|
105
|
+
];
|
|
106
|
+
|
|
107
|
+
// 压缩自死循环:「压缩完还是超限」,连续两轮都在压缩却量没降下来。
|
|
108
|
+
const COMPACTION_LOOP_PATTERNS = [
|
|
109
|
+
/compress(?:ion)? (?:loop|did not reduce|no progress|ineffective)/i,
|
|
110
|
+
/still (?:too long|exceeds|over (?:the )?limit) after (?:compress|compact|prune)/i,
|
|
111
|
+
/(?:compress|compact)\w* .*(?:did not|no) (?:reduce|reduction|progress)/i,
|
|
112
|
+
/after \\d+ rounds?/i,
|
|
113
|
+
];
|
|
114
|
+
|
|
115
|
+
// MCP 子服务:独立鉴权失败、子服务没起来、握手失败。
|
|
116
|
+
const MCP_PATTERNS = [
|
|
117
|
+
/\bMCP\b/,
|
|
118
|
+
/mcp (?:server|client|handshake|session)/i,
|
|
119
|
+
/subprocess .*(?:mcp|server).*(?:failed|exit)/i,
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// 模型选择与能力判断:模型 ID 写死成旧名字、能力表判断错、缓存里的模型列表过期。
|
|
123
|
+
const MODEL_PATTERNS = [
|
|
124
|
+
/model (?:not found|not supported|unsupported|unknown)/i,
|
|
125
|
+
/unknown model/i,
|
|
126
|
+
/invalid model/i,
|
|
127
|
+
/no such model/i,
|
|
128
|
+
/model (?:list|catalog|cache).*(?:stale|expired|invalid)/i,
|
|
129
|
+
/capabilit(?:y|ies) (?:mismatch|unsupported|unknown)/i,
|
|
130
|
+
/does not support (?:tool|function) call/i,
|
|
131
|
+
/model (?:list|catalog).*(?:empty|not (?:loaded|found))/i,
|
|
132
|
+
/some model|the model .* does not exist/i,
|
|
133
|
+
];
|
|
134
|
+
|
|
135
|
+
// 本机环境:端口被占、本地中转服务崩了、沙箱权限不足、子进程崩溃、
|
|
136
|
+
// 文件读不出来、句柄或磁盘耗尽。这些不是模型请求的问题,重试请求本身没用。
|
|
137
|
+
const LOCAL_PATTERNS = [
|
|
138
|
+
/EADDRINUSE/i,
|
|
139
|
+
/EACCES/i,
|
|
140
|
+
/EPERM/i,
|
|
141
|
+
/ENOENT/i,
|
|
142
|
+
/local (?:relay|proxy|bridge) .*(?:exited|crash|died|unavailable)/i,
|
|
143
|
+
/relay process .*(?:exited|crash|died)/i,
|
|
144
|
+
/EMFILE/i,
|
|
145
|
+
/ENFILE/i,
|
|
146
|
+
/ENOSPC/i,
|
|
147
|
+
/ENOMEM/i,
|
|
148
|
+
/EAGAIN/i,
|
|
149
|
+
/spawn (?:.* )?(?:ENOENT|EACCES|failed)/i,
|
|
150
|
+
/child process (?:exited|died|crashed|killed)/i,
|
|
151
|
+
/sandbox/i,
|
|
152
|
+
/permission denied/i,
|
|
153
|
+
/resource temporarily unavailable/i,
|
|
154
|
+
/out of memory/i,
|
|
155
|
+
];
|
|
156
|
+
|
|
157
|
+
// 这几种本机故障不会随重试消失:权限不足、路径不存在、磁盘写满。
|
|
158
|
+
const LOCAL_BLOCKING_PATTERNS = [
|
|
159
|
+
/EACCES/i,
|
|
160
|
+
/EPERM/i,
|
|
161
|
+
/ENOENT/i,
|
|
162
|
+
/ENOSPC/i,
|
|
163
|
+
/permission denied/i,
|
|
164
|
+
/sandbox .*(?:denied|blocked)/i,
|
|
165
|
+
];
|
|
166
|
+
// 只读工具才允许重放。带副作用的(跑命令、写文件、发消息)重放一次就是执行两遍。
|
|
167
|
+
const DEFAULT_RETRYABLE_TOOL_PATTERN =
|
|
168
|
+
'^(read|read_image|glob|grep|web_fetch|web_search|cordis_inspect_.*|browser_(list_tabs|snapshot|get_text|wait|scroll)|db_(query|preview|describe_table|list_tables))$';
|
|
169
|
+
|
|
170
|
+
// 把一个错误和它的 cause 链摊平成一段短文本,供正则匹配。限制深度和长度是为了
|
|
171
|
+
// 防止循环引用或者过长的 cause 链把内存拖垮。
|
|
172
|
+
function errorText(value, depth = 0, seen = new Set()) {
|
|
173
|
+
if (value === null || value === undefined || depth > 6 || seen.has(value)) return '';
|
|
174
|
+
if (typeof value === 'string') return value;
|
|
175
|
+
if (typeof value !== 'object') return String(value);
|
|
176
|
+
seen.add(value);
|
|
177
|
+
const own = [];
|
|
178
|
+
for (const key of ['message', 'code', 'name', 'reason', 'errno', 'syscall']) {
|
|
179
|
+
const part = value[key];
|
|
180
|
+
if (part !== undefined && part !== null && typeof part !== 'object') own.push(String(part));
|
|
181
|
+
}
|
|
182
|
+
if (value.info && typeof value.info === 'object') own.push(String(value.info.code ?? ''));
|
|
183
|
+
const cause = value.cause !== undefined ? errorText(value.cause, depth + 1, seen) : '';
|
|
184
|
+
return `${own.join(' ')} ${cause}`.trim().slice(0, 4000);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// 一组正则里有没有命中的。
|
|
188
|
+
function matchesAny(patterns, text) {
|
|
189
|
+
return patterns.some((pattern) => pattern.test(text));
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
// 抛出来的东西或者失败记录里,是否指向本机传输问题。
|
|
193
|
+
export function isTransientNetworkError(value) {
|
|
194
|
+
const text = errorText(value);
|
|
195
|
+
if (text === '') return false;
|
|
196
|
+
return matchesAny(NETWORK_PATTERNS, text);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// 规则表。从上往下匹配,第一条命中就返回。
|
|
200
|
+
//
|
|
201
|
+
// 顺序就是优先级,错了会互相盖掉:
|
|
202
|
+
// - 证书问题排最前,重试它没有意义,必须先放弃;
|
|
203
|
+
// - 对方侧的拒绝(配额、政策、鉴权被拒)排在传输判断前,否则一句「含 timeout 的配额错误」
|
|
204
|
+
// 会被当成网络抖动去重试;
|
|
205
|
+
// - 压缩死循环排在普通超限前,否则会被当成一次新的超长而反复压缩。
|
|
206
|
+
const CLASSIFICATION_RULES = [
|
|
207
|
+
{
|
|
208
|
+
kind: 'tls',
|
|
209
|
+
strategy: 'skip',
|
|
210
|
+
clientCaused: false,
|
|
211
|
+
match: (context) => matchesAny(TLS_PATTERNS, context.text),
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
kind: 'policy',
|
|
215
|
+
strategy: 'skip',
|
|
216
|
+
clientCaused: false,
|
|
217
|
+
match: (context) => /content policy|safety|moderation|blocked by policy/i.test(context.text),
|
|
218
|
+
},
|
|
219
|
+
{
|
|
220
|
+
kind: 'quota',
|
|
221
|
+
strategy: 'skip',
|
|
222
|
+
clientCaused: false,
|
|
223
|
+
match: ({ code }) => code === 'QUOTA' || code === 'ACCOUNT_QUOTA' || code === 'RATE_LIMIT' || code === 'SERVER',
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
kind: 'compaction-loop',
|
|
227
|
+
strategy: 'history-repair',
|
|
228
|
+
clientCaused: true,
|
|
229
|
+
retryable: true,
|
|
230
|
+
match: (context) => matchesAny(COMPACTION_LOOP_PATTERNS, context.text),
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
kind: 'history',
|
|
234
|
+
strategy: 'history-repair',
|
|
235
|
+
clientCaused: true,
|
|
236
|
+
retryable: true,
|
|
237
|
+
match: (context) => matchesAny(HISTORY_PATTERNS, context.text),
|
|
238
|
+
},
|
|
239
|
+
{
|
|
240
|
+
kind: 'overflow',
|
|
241
|
+
strategy: 'compact',
|
|
242
|
+
clientCaused: true,
|
|
243
|
+
retryable: true,
|
|
244
|
+
match: ({ code, status, text }) =>
|
|
245
|
+
code === 'CONTEXT_WINDOW_EXCEEDED' || status === 413 || /context (?:window|length)|request too large|prompt too long/i.test(text),
|
|
246
|
+
},
|
|
247
|
+
{ kind: 'image', strategy: 'skip', match: ({ code }) => code === 'IMAGE_OFFLOAD_REQUIRED' },
|
|
248
|
+
{
|
|
249
|
+
kind: 'adapter',
|
|
250
|
+
strategy: 'version-repair',
|
|
251
|
+
clientCaused: true,
|
|
252
|
+
match: ({ code, status }) => code === 'NO_ADAPTER' || status === 404,
|
|
253
|
+
},
|
|
254
|
+
{
|
|
255
|
+
kind: 'auth',
|
|
256
|
+
strategy: 'auth',
|
|
257
|
+
clientCaused: true,
|
|
258
|
+
retryable: true,
|
|
259
|
+
match: (context) => matchesAny(AUTH_PATTERNS, context.text) || context.status === 401 || context.status === 403,
|
|
260
|
+
},
|
|
261
|
+
{
|
|
262
|
+
kind: 'mcp',
|
|
263
|
+
strategy: 'retry',
|
|
264
|
+
clientCaused: true,
|
|
265
|
+
retryable: true,
|
|
266
|
+
match: (context) => matchesAny(MCP_PATTERNS, context.text),
|
|
267
|
+
},
|
|
268
|
+
{
|
|
269
|
+
kind: 'protocol',
|
|
270
|
+
strategy: 'retry',
|
|
271
|
+
clientCaused: true,
|
|
272
|
+
retryable: true,
|
|
273
|
+
match: (context) => matchesAny(PROTOCOL_PATTERNS, context.text) || context.status === 400,
|
|
274
|
+
},
|
|
275
|
+
{
|
|
276
|
+
kind: 'model',
|
|
277
|
+
strategy: 'retry',
|
|
278
|
+
clientCaused: true,
|
|
279
|
+
retryable: true,
|
|
280
|
+
match: (context) => matchesAny(MODEL_PATTERNS, context.text),
|
|
281
|
+
},
|
|
282
|
+
{
|
|
283
|
+
// 权限、路径、磁盘满这类重试一万次也不会变:本机缺的是权限或空间,不是时机。
|
|
284
|
+
kind: 'local-blocked',
|
|
285
|
+
strategy: 'skip',
|
|
286
|
+
clientCaused: true,
|
|
287
|
+
match: (context) => matchesAny(LOCAL_BLOCKING_PATTERNS, context.text),
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
kind: 'local-env',
|
|
291
|
+
strategy: 'local',
|
|
292
|
+
clientCaused: true,
|
|
293
|
+
retryable: true,
|
|
294
|
+
match: (context) => matchesAny(LOCAL_PATTERNS, context.text),
|
|
295
|
+
},
|
|
296
|
+
{
|
|
297
|
+
kind: 'stream',
|
|
298
|
+
strategy: 'retry',
|
|
299
|
+
clientCaused: true,
|
|
300
|
+
retryable: true,
|
|
301
|
+
match: (context) => matchesAny(STREAM_PATTERNS, context.text),
|
|
302
|
+
},
|
|
303
|
+
{
|
|
304
|
+
kind: 'provider-error',
|
|
305
|
+
strategy: 'retry',
|
|
306
|
+
clientCaused: true,
|
|
307
|
+
retryable: true,
|
|
308
|
+
match: ({ code, text }) => code === 'PI_AI_ERROR' || /PI_AI_ERROR/i.test(text),
|
|
309
|
+
},
|
|
310
|
+
{ kind: 'empty', strategy: 'retry', clientCaused: true, retryable: true, match: ({ code }) => code === 'EMPTY_RESPONSE' },
|
|
311
|
+
{ kind: 'timeout', strategy: 'retry', clientCaused: true, retryable: true, match: ({ code, status }) => code === 'TIMEOUT' || status === 408 },
|
|
312
|
+
{
|
|
313
|
+
kind: 'transport',
|
|
314
|
+
strategy: 'retry',
|
|
315
|
+
clientCaused: true,
|
|
316
|
+
retryable: true,
|
|
317
|
+
match: (context) => context.transient === true,
|
|
318
|
+
},
|
|
319
|
+
];
|
|
320
|
+
|
|
321
|
+
// 判断一次模型请求失败属于哪一类。
|
|
322
|
+
//
|
|
323
|
+
// clientCaused 表示问题出在本机(传输、本地拼坏请求、路由或凭据过期),而不是对方服务;
|
|
324
|
+
// strategy 表示重试之前先做什么:compact 先压缩上下文,history-repair 先修本地历史或规则,
|
|
325
|
+
// auth 先换凭据,version-repair 先修插件版本,retry 直接退避重来,local 交给本机环境处理,
|
|
326
|
+
// skip 什么都不做。
|
|
327
|
+
export function classifyModelFailure(failure) {
|
|
328
|
+
const code = String(failure?.code ?? '').toUpperCase();
|
|
329
|
+
const context = {
|
|
330
|
+
code,
|
|
331
|
+
status: Number(failure?.status ?? 0),
|
|
332
|
+
text: errorText(failure),
|
|
333
|
+
transient: isTransientNetworkError(failure),
|
|
334
|
+
};
|
|
335
|
+
for (const rule of CLASSIFICATION_RULES) {
|
|
336
|
+
if (rule.match(context)) {
|
|
337
|
+
return {
|
|
338
|
+
clientCaused: rule.clientCaused ?? false,
|
|
339
|
+
kind: rule.kind,
|
|
340
|
+
strategy: rule.strategy,
|
|
341
|
+
retryable: rule.retryable ?? false,
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
return { clientCaused: false, kind: 'other', strategy: 'skip', retryable: false };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// 判断一次工具调用失败是否值得重放。只有只读工具在名单里才考虑,
|
|
349
|
+
// 而且失败原因得确实是传输或本地环境问题——本地环境问题重放往往就好了。
|
|
350
|
+
export function isTransientToolFailure(name, result, allowlist) {
|
|
351
|
+
if (result?.isError !== true) return false;
|
|
352
|
+
if (!allowlist.test(String(name ?? ''))) return false;
|
|
353
|
+
if (isTransientNetworkError(result.error)) return true;
|
|
354
|
+
const text = errorText(result.error);
|
|
355
|
+
if (matchesAny(LOCAL_PATTERNS, text)) return true;
|
|
356
|
+
const content = Array.isArray(result.content) ? JSON.stringify(result.content).slice(0, 2000) : '';
|
|
357
|
+
return matchesAny(NETWORK_PATTERNS, content) || matchesAny(LOCAL_PATTERNS, content);
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// 把插件配置整理成一套完整、带默认值和上下界的策略。
|
|
361
|
+
// 用户配错不该导致插件不工作,所以数值一律夹到合理区间。
|
|
362
|
+
export function parseRetryConfig(raw = {}) {
|
|
363
|
+
const source = raw !== null && typeof raw === 'object' && !Array.isArray(raw) ? raw : {};
|
|
364
|
+
const number = (key, fallback, min, max) => {
|
|
365
|
+
const value = Number(source[key]);
|
|
366
|
+
if (!Number.isFinite(value)) return fallback;
|
|
367
|
+
return Math.min(Math.max(value, min), max);
|
|
368
|
+
};
|
|
369
|
+
const bool = (key, fallback) => (typeof source[key] === 'boolean' ? source[key] : fallback);
|
|
370
|
+
let toolPattern;
|
|
371
|
+
try {
|
|
372
|
+
toolPattern = new RegExp(typeof source.retryableToolPattern === 'string' ? source.retryableToolPattern : DEFAULT_RETRYABLE_TOOL_PATTERN);
|
|
373
|
+
} catch {
|
|
374
|
+
// 用户写的正则不合法,退回默认白名单
|
|
375
|
+
toolPattern = new RegExp(DEFAULT_RETRYABLE_TOOL_PATTERN);
|
|
376
|
+
}
|
|
377
|
+
return {
|
|
378
|
+
autoRepair: bool('autoRepair', true),
|
|
379
|
+
autoVersionRepair: bool('autoVersionRepair', true),
|
|
380
|
+
recordRetryEvents: bool('recordRetryEvents', true),
|
|
381
|
+
maxRetries: Math.round(number('maxRetries', 12, 0, 100)),
|
|
382
|
+
maxToolRetries: Math.round(number('maxToolRetries', 2, 0, 10)),
|
|
383
|
+
maxVersionRepairs: Math.round(number('maxVersionRepairs', 64, 0, 500)),
|
|
384
|
+
maxAuthRetries: Math.round(number('maxAuthRetries', 2, 0, 10)),
|
|
385
|
+
maxRepeatedToolCalls: Math.round(number('maxRepeatedToolCalls', 3, 0, 50)),
|
|
386
|
+
initialDelayMs: number('initialDelayMs', 400, 1, 2147483647),
|
|
387
|
+
maxDelayMs: number('maxDelayMs', 20000, 1, 2147483647),
|
|
388
|
+
jitterRatio: number('jitterRatio', 0.25, 0, 1),
|
|
389
|
+
retryableToolPattern: toolPattern,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 指数退避加抖动。对方要是给了 retry-after 就用它的,但仍然夹在上下限内。
|
|
394
|
+
// 抖动是为了避免多个重试在同一时刻一起怼上去。
|
|
395
|
+
export function computeDelay(config, attempt, failure) {
|
|
396
|
+
const hint = Number(failure?.providerRetryAfterMs);
|
|
397
|
+
if (Number.isFinite(hint) && hint > 0) return Math.min(Math.max(hint, 100), config.maxDelayMs);
|
|
398
|
+
const exponent = Math.min(Math.max(attempt - 1, 0), 20);
|
|
399
|
+
const ceil = Math.min(config.initialDelayMs * 2 ** exponent, config.maxDelayMs);
|
|
400
|
+
const jitter = 1 - config.jitterRatio + 2 * config.jitterRatio * Math.random();
|
|
401
|
+
return Math.max(1, Math.min(Math.round(ceil * jitter), config.maxDelayMs));
|
|
402
|
+
}
|