job51-gitlab-cr-node-jt-1 3.1.2 → 3.1.4

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.
@@ -0,0 +1,25 @@
1
+ {
2
+ "default_rule": "code-review-rules.md",
3
+ "path_rule_map": {
4
+ "**/*.java": "java.md",
5
+ "**/*.{ts,tsx}": "typescript.md",
6
+ "**/*.js": "javascript.md",
7
+ "**/*.py": "python.md",
8
+ "**/*.go": "go.md",
9
+ "**/*.rs": "rust.md",
10
+ "**/pom.xml": "maven.md",
11
+ "**/package.json": "npm.md",
12
+ "**/Cargo.toml": "cargo.md",
13
+ "**/*.xml": "xml.md",
14
+ "**/*.sql": "sql.md",
15
+ "**/*.yaml": "yaml.md",
16
+ "**/*.yml": "yaml.md",
17
+ "**/*.json": "json.md",
18
+ "**/*.properties": "properties.md",
19
+ "**/*.md": "markdown.md",
20
+ ".github/workflows/**/*.{yaml,yml}": "github_workflows.md",
21
+ "**/Dockerfile": "dockerfile.md",
22
+ "**/docker-compose.{yaml,yml}": "docker_compose.md"
23
+ },
24
+ "_comment": "规则优先级: CLI覆盖 > 项目配置 > 用户配置 > 内置规则"
25
+ }
@@ -0,0 +1,229 @@
1
+ # TypeScript/JavaScript 代码审查规则
2
+
3
+ **继承通用规则**:本规则基于 `code-review-rules.md`,并补充 TypeScript/JavaScript 语言特定的审查要点。
4
+
5
+ ---
6
+
7
+ ## TypeScript/JavaScript 语言特定审查规则
8
+
9
+ ### 一、类型安全问题(TypeScript 特定)
10
+
11
+ #### 1. any 类型滥用
12
+ > - **检测场景**:变量、参数、返回值显式声明为 any
13
+ > - **建议报告**:应使用具体类型或 unknown(需类型守卫)
14
+ > - **示例**:
15
+ ```typescript
16
+ // 错误 ❌
17
+ function process(data: any) { ... }
18
+
19
+ // 正确 ✓
20
+ function process(data: unknown) {
21
+ if (typeof data === 'string') { ... }
22
+ }
23
+ ```
24
+
25
+ #### 2. 类型断言风险
26
+ > - **检测场景**:使用 as 进行类型断言,可能绕过类型检查
27
+ > - **风险**:断言错误时运行时崩溃
28
+ > - **建议报告**:类型断言后的代码应验证目标类型
29
+
30
+ #### 3. 可空类型未处理
31
+ > - **检测场景**:访问可能为 null/undefined 的对象属性
32
+ > - **必须报告**:未使用可选链 (?.) 或非空断言 (!) 且无判空逻辑
33
+ > - **示例**:
34
+ ```typescript
35
+ // 错误 ❌
36
+ const name = user.profile.name; // profile 可能为 undefined
37
+
38
+ // 正确 ✓
39
+ const name = user.profile?.name;
40
+ const name = user.profile!.name; // 明确非空断言
41
+ ```
42
+
43
+ ### 二、异步编程问题(JS/TS 特定)
44
+
45
+ #### 1. Promise 未处理
46
+ > - **检测场景**:调用 async 函数或 Promise 但未 await/catch
47
+ > - **必须报告**:Promise rejection 未处理会导致进程退出
48
+ > - **示例**:
49
+ ```typescript
50
+ // 错误 ❌
51
+ asyncFunction(); // 未 await,异常丢失
52
+ asyncFunction().then(data => ...); // 无 catch
53
+
54
+ // 正确 ✓
55
+ await asyncFunction();
56
+ asyncFunction().then(...).catch(err => ...);
57
+ ```
58
+
59
+ #### 2. async/await 混用问题
60
+ > - **检测场景**:async 函数内使用 .then() 而非 await
61
+ > - **建议报告**:async 函数内应统一使用 await
62
+
63
+ #### 3. Promise 构造函数反模式
64
+ > - **检测场景**:new Promise 内包裹已有 Promise
65
+ > - **必须报告**:
66
+ ```typescript
67
+ // 错误 ❌
68
+ return new Promise((resolve, reject) => {
69
+ otherPromise.then(resolve).catch(reject); // 无意义包装
70
+ });
71
+
72
+ // 正确 ✓
73
+ return otherPromise;
74
+ ```
75
+
76
+ #### 4. 并行异步等待问题
77
+ > - **检测场景**:顺序 await 多个独立 Promise(应并行)
78
+ > - **建议报告**:
79
+ ```typescript
80
+ // 错误 ❌:顺序等待,浪费时间
81
+ const a = await fetchA();
82
+ const b = await fetchB(); // fetchA 和 fetchB 独立,可并行
83
+
84
+ // 正确 ✓
85
+ const [a, b] = await Promise.all([fetchA(), fetchB()]);
86
+ ```
87
+
88
+ ### 三、内存泄漏问题(JS/TS 特定)
89
+
90
+ #### 1. 事件监听器未移除
91
+ > - **检测场景**:addEventListener 但无对应 removeEventListener
92
+ > - **必须报告**:组件销毁时应移除事件监听器
93
+ > - **特别注意**:React useEffect 中的事件监听器清理
94
+
95
+ #### 2. 定时器未清理
96
+ > - **检测场景**:setInterval/setTimeout 但无对应 clearInterval/clearTimeout
97
+ > - **必须报告**:组件销毁时未清理定时器
98
+
99
+ #### 3. closure 内存泄漏
100
+ > - **检测场景**:闭包持有大对象引用且生命周期过长
101
+ > - **建议报告**:闭包内持有 DOM 元素、大数组等
102
+
103
+ ### 四、React/Vue 特定问题
104
+
105
+ #### 1. React Hooks 依赖缺失
106
+ > - **检测场景**:useEffect/useMemo/useCallback 缺少必要依赖
107
+ > - **必须报告**:依赖数组遗漏会触发 stale closure 问题
108
+ > - **示例**:
109
+ ```typescript
110
+ // 错误 ❌
111
+ useEffect(() => {
112
+ fetchData(userId); // userId 在依赖中
113
+ }, []); // 缺少 userId
114
+
115
+ // 正确 ✓
116
+ useEffect(() => {
117
+ fetchData(userId);
118
+ }, [userId]);
119
+ ```
120
+
121
+ #### 2. React 状态直接修改
122
+ > - **检测场景**:直接修改 state 对象而非 setState/useState
123
+ > - **必须报告**:
124
+ ```typescript
125
+ // 错误 ❌
126
+ state.items.push(newItem);
127
+ state.count = state.count + 1;
128
+
129
+ // 正确 ✓
130
+ setState({ ...state, items: [...state.items, newItem] });
131
+ setState(prev => ({ count: prev.count + 1 }));
132
+ ```
133
+
134
+ #### 3. React useEffect 无限循环
135
+ > - **检测场景**:useEffect 内 setState 触发重新渲染
136
+ > - **必须报告**:依赖数组包含每次渲染都变化的值(如对象引用)
137
+
138
+ #### 4. Vue computed vs methods 混用
139
+ > - **检测场景**:computed 属性内执行副作用操作
140
+ > - **必须报告**:computed 应纯计算,methods 可执行副作用
141
+
142
+ ### 五、数组/对象操作问题(JS/TS 特定)
143
+
144
+ #### 1. 数组遍历中修改
145
+ > - **检测场景**:forEach/map/filter 中修改原数组
146
+ > - **必须报告**:可能导致意外行为
147
+ > - **示例**:
148
+ ```typescript
149
+ // 错误 ❌
150
+ arr.forEach(item => {
151
+ if (item.id === 1) arr.splice(arr.indexOf(item), 1); // 遍历中删除
152
+ });
153
+
154
+ // 正确 ✓
155
+ const newArr = arr.filter(item => item.id !== 1);
156
+ ```
157
+
158
+ #### 2. 对象引用共享问题
159
+ > - **检测场景**:多个变量引用同一对象,修改相互影响
160
+ > - **建议报告**:需要独立副本时应使用深拷贝或浅拷贝
161
+
162
+ #### 3. 数组 indexOf vs includes
163
+ > - **检测场景**:使用 arr.indexOf(x) !== -1 检查存在性
164
+ > - **建议报告**:arr.includes(x) 更语义化(非强制)
165
+
166
+ ### 六、错误处理问题(JS/TS 特定)
167
+
168
+ #### 1. try-catch 空 catch
169
+ > - **检测场景**:catch 块为空或仅 console.log
170
+ > - **必须报告**:错误被吞没
171
+
172
+ #### 2. throw 非错误对象
173
+ > - **检测场景**:throw string/object 而非 Error 实例
174
+ > - **必须报告**:应 throw new Error(message) 或自定义 Error 类
175
+
176
+ #### 3. async 函数未捕获同步异常
177
+ > - **检测场景**:async 函数内同步代码异常未包裹
178
+ > - **建议报告**:async 函数内所有可能异常的代码应在 try-catch 内
179
+
180
+ ### 七、Node.js 特定问题
181
+
182
+ #### 1. 回调地狱
183
+ > - **检测场景**:多层嵌套回调函数
184
+ > - **建议报告**:应使用 async/await 或 Promise 链
185
+
186
+ #### 2. 未处理 process 事件
187
+ > - **检测场景**:未监听 uncaughtException, unhandledRejection
188
+ > - **建议报告**:生产环境应监听这些事件防止进程崩溃
189
+
190
+ #### 3. 流未正确处理
191
+ > - **检测场景**:ReadStream/WriteStream 未处理 error 事件
192
+ > - **必须报告**:流操作应有错误处理
193
+
194
+ #### 4. require 动态路径
195
+ > - **检测场景**:require(variablePath) 动态加载模块
196
+ > - **建议报告**:可能加载恶意代码,应限制路径范围
197
+
198
+ ### 八、安全问题(JS/TS 特定)
199
+
200
+ #### 1. eval 使用
201
+ > - **检测场景**:使用 eval, Function, setTimeout(string) 执行动态代码
202
+ > - **必须报告**:存在 XSS/代码注入风险
203
+
204
+ #### 2. 内联用户输入
205
+ > - **检测场景**:用户输入直接拼接到 HTML/SQL/命令
206
+ > - **必须报告**:XSS/SQL 注入/命令注入风险
207
+
208
+ #### 3. localStorage 安全
209
+ > - **检测场景**:敏感数据存储到 localStorage
210
+ > - **必须报告**:localStorage 无加密,敏感信息泄露风险
211
+
212
+ ---
213
+
214
+ ## TypeScript/JavaScript 审查优先级
215
+
216
+ | 问题类型 | 优先级 | 说明 |
217
+ |----------|--------|------|
218
+ | Promise 未处理 | 🔴 严重 | 进程可能崩溃 |
219
+ | async 函数空 catch | 🔴 严重 | 错误被吞没 |
220
+ | 事件监听器未移除 | 🔴 严重 | 内存泄漏 |
221
+ | React 状态直接修改 | 🔴 严重 | 状态不一致 |
222
+ | React Hooks 依赖缺失 | 🔴 严重 | stale closure |
223
+ | 可空类型未处理 | 🔴 严重 | 运行时 TypeError |
224
+ | 数组遍历中修改 | 🔴 严重 | 意外行为 |
225
+ | eval 使用 | 🔴 严重 | 安全漏洞 |
226
+ | throw 非错误对象 | 🟡 警告 | 错误追踪困难 |
227
+ | any 类型滥用 | 🟡 警告 | 类型安全降低 |
228
+ | 并行异步顺序等待 | 🟡 警告 | 性能问题 |
229
+ | 回调地狱 | 🟡 警告 | 可读性差 |
package/README.md CHANGED
@@ -46,10 +46,29 @@ gitlab-cr
46
46
  - 使用 Claude AI 进行代码审查
47
47
  - 生成结构化的审查报告
48
48
  - 将审查结果发布到 GitLab MR
49
+ - **分阶段审查**:快速扫描过滤低价值变更,减少 token 消耗
49
50
  - 自动跳过测试文件(如 `*Test.java`、`*.test.js` 等)
50
51
  - 自动跳过 DTO/VO/Query 文件(如 `*Dto.java`、`*VO.java`、`*Query.java`、`*Request.java` 等)
51
52
  - 自动跳过非代码文件(如配置文件、文档文件、资源文件等)
52
53
 
54
+ ## Token 优化
55
+
56
+ ### 快速扫描机制
57
+
58
+ 在调用 AI 完整审查前,会先进行本地快速扫描,过滤以下低价值变更:
59
+
60
+ | 跳过类型 | 示例 |
61
+ |---------|------|
62
+ | 纯 import 变更 | `import java.util.List;` |
63
+ | 纯注释变更 | `// 注释内容`、`/* 多行注释 */` |
64
+ | 纯空白/格式变更 | 空行、缩进调整 |
65
+ | 纯注解变更 | `@Override`、`@Autowired` |
66
+ | 纯 getter/setter | `getXxx()`、`setXxx()` |
67
+ | 纯日志打印 | `log.info()`、`console.log()` |
68
+ | 纯常量定义 | `private static final String XXX = "xxx";` |
69
+
70
+ **预计效果**:减少 30-50% 的 AI 调用,显著降低 token 消耗。
71
+
53
72
  ## 依赖要求
54
73
 
55
74
  - Node.js 10+
package/index.js CHANGED
@@ -5,6 +5,170 @@ const path = require('path');
5
5
  const fs = require('fs');
6
6
  const { GitLabAPIClient, debugLog, infoLog, warnLog, errorLog, extractReportContent, MetricsCollector } = require('./utils');
7
7
 
8
+ // ========== 两阶段审查配置 ==========
9
+ // Plan 阶段触发阈值:改动行数超过此值时执行风险分析
10
+ const PLAN_LINE_THRESHOLD = 30;
11
+ // Plan 阶段超时时间(毫秒)
12
+ const PLAN_TIMEOUT_MS = 60000;
13
+
14
+ /**
15
+ * 计算 diff 内容的改动行数(新增 + 删除)
16
+ * @param {string} diffContent diff 内容
17
+ * @returns {number} 改动行数
18
+ */
19
+ function countChangeLines(diffContent) {
20
+ if (!diffContent || diffContent.trim() === '') return 0;
21
+ const lines = diffContent.split('\n');
22
+ // 只统计 + 或 - 开头的实际变更行(排除 +++ 和 --- 头信息)
23
+ return lines.filter(line =>
24
+ (line.startsWith('+') && !line.startsWith('+++')) ||
25
+ (line.startsWith('-') && !line.startsWith('---'))
26
+ ).length;
27
+ }
28
+
29
+ /**
30
+ * Plan 阶段:风险分析
31
+ * 分析代码变更,识别高风险区域,生成审查指导
32
+ * @param {string} filePath 文件路径
33
+ * @param {string} diffContent diff 内容
34
+ * @param {string} blockIdentifier 块标识(用于日志)
35
+ * @returns {Promise<Object|null>} Plan 结果对象或 null
36
+ */
37
+ async function runPlanPhase(filePath, diffContent, blockIdentifier) {
38
+ debugLog(`[${blockIdentifier}] 开始 Plan 阶段风险分析...`);
39
+
40
+ const planPrompt = `你是一个代码审查风险分析专家。请分析以下代码变更,识别高风险点并生成审查指导。
41
+
42
+ **文件路径**:${filePath}
43
+
44
+ **变更内容**:
45
+ ${diffContent}
46
+
47
+ ---
48
+
49
+ ## 分析要求
50
+
51
+ 1. **只分析新增和修改的代码**(+ 开头的行),忽略删除代码
52
+ 2. **识别高风险区域**,按严重程度排序
53
+ 3. **输出结构化的审查指导**,帮助后续审查重点关注
54
+
55
+ ## 风险类型定义
56
+
57
+ - **high(严重)**:安全漏洞、数据丢失风险、系统崩溃、关键功能失效
58
+ - 如:SQL注入、硬编码密钥、事务边界问题、并发安全问题
59
+ - **medium(中等)**:性能问题、边界情况、潜在兼容性问题
60
+ - 如:循环内重复查询、资源未释放、异常处理不当
61
+ - **low(轻微)**:代码风格、可读性、非关键最佳实践
62
+ - 如:命名不规范、注释缺失、代码冗余
63
+
64
+ ---
65
+
66
+ ## 输出格式(严格 JSON)
67
+
68
+ 直接输出 JSON,不要包裹在代码块中,不要任何额外文本:
69
+
70
+ {
71
+ "change_summary": "改动摘要(一句话描述改动目的)",
72
+ "risk_areas": [
73
+ {
74
+ "severity": "high|medium|low",
75
+ "area": "风险类型(如:安全、并发、事务、边界)",
76
+ "location_hint": "大概位置提示(如:第 X-XX 行附近)",
77
+ "description": "具体风险描述(为什么这里是风险点)",
78
+ "focus_hint": "审查时重点检查什么"
79
+ }
80
+ ],
81
+ "review_guidance": "整体审查建议(一句话)"
82
+ }
83
+
84
+ ---
85
+
86
+ **开始分析**:`;
87
+
88
+ try {
89
+ const planStartTime = Date.now();
90
+ const planResult = await runClaudeCommand(planPrompt);
91
+ const planDuration = Date.now() - planStartTime;
92
+ debugLog(`[${blockIdentifier}] Plan 阶段完成,耗时 ${planDuration}ms,结果长度 ${planResult?.length || 0}`);
93
+
94
+ if (!planResult || planResult.trim() === '') {
95
+ debugLog(`[${blockIdentifier}] Plan 结果为空,跳过`);
96
+ return null;
97
+ }
98
+
99
+ // 解析 JSON 结果
100
+ // 尝试从 markdown 代码块中提取 JSON
101
+ let jsonContent = planResult.trim();
102
+ const jsonMatch = planResult.match(/```(?:json)?\s*([\s\S]*?)\s*```/);
103
+ if (jsonMatch) {
104
+ jsonContent = jsonMatch[1].trim();
105
+ }
106
+
107
+ try {
108
+ const parsed = JSON.parse(jsonContent);
109
+ debugLog(`[${blockIdentifier}] Plan 结果解析成功:发现 ${parsed.risk_areas?.length || 0} 个风险点`);
110
+
111
+ // 验证结果结构
112
+ if (!parsed.change_summary || !parsed.risk_areas) {
113
+ debugLog(`[${blockIdentifier}] Plan 结果结构不完整,跳过`);
114
+ return null;
115
+ }
116
+
117
+ return parsed;
118
+ } catch (parseError) {
119
+ debugLog(`[${blockIdentifier}] Plan JSON 解析失败:${parseError.message}`);
120
+ // 解析失败但可能有有用的文本,尝试提取关键信息
121
+ return {
122
+ change_summary: '无法解析风险分析结果',
123
+ review_guidance: planResult.slice(0, 200),
124
+ risk_areas: []
125
+ };
126
+ }
127
+ } catch (error) {
128
+ debugLog(`[${blockIdentifier}] Plan 阶段执行失败:${error.message}`);
129
+ return null;
130
+ }
131
+ }
132
+
133
+ /**
134
+ * 将 Plan 结果转换为审查指导文本
135
+ * @param {Object} planResult Plan 结果对象
136
+ * @returns {string} 审查指导文本(用于注入 Main 阶段 prompt)
137
+ */
138
+ function buildPlanGuidanceText(planResult) {
139
+ if (!planResult || !planResult.risk_areas || planResult.risk_areas.length === 0) {
140
+ return '';
141
+ }
142
+
143
+ const riskItems = planResult.risk_areas
144
+ .sort((a, b) => {
145
+ // 按严重程度排序:high > medium > low
146
+ const order = { high: 0, medium: 1, low: 2 };
147
+ return (order[a.severity] || 2) - (order[b.severity] || 2);
148
+ })
149
+ .map((r, i) => {
150
+ const severityIcon = r.severity === 'high' ? '🔴' : (r.severity === 'medium' ? '🟡' : '🔵');
151
+ return `${i + 1}. **${severityIcon} [${r.severity.toUpperCase()}] ${r.area}**
152
+ - 位置提示:${r.location_hint || '需自行定位'}
153
+ - 风险描述:${r.description}
154
+ - 重点检查:${r.focus_hint}`;
155
+ })
156
+ .join('\n');
157
+
158
+ return `
159
+ ### 🎯 审查指导(基于风险分析)
160
+
161
+ **改动摘要**:${planResult.change_summary}
162
+
163
+ **重点关注区域**:
164
+ ${riskItems}
165
+
166
+ **整体建议**:${planResult.review_guidance || '请针对性审查上述风险点,不要泛泛而谈'}
167
+
168
+ **⚠️ 重要**:上述风险点是预先分析得出的,请重点审查这些区域,确认问题是否真实存在。
169
+ `;
170
+ }
171
+
8
172
  class GitLabCodeReviewer {
9
173
  constructor(gitlabToken, gitlabUrl = null) {
10
174
  debugLog(`GitLab客户端初始化: ${gitlabUrl}`);
@@ -312,6 +476,181 @@ class GitLabCodeReviewer {
312
476
  return false;
313
477
  }
314
478
 
479
+ /**
480
+ * 快速扫描判断 diff 内容是否值得审查(本地分析,不调用 AI)
481
+ * 用于过滤低价值变更,减少 token 消耗
482
+ * @param {string} diffContent diff 内容
483
+ * @param {string} filePath 文件路径(用于日志)
484
+ * @returns {{ worthReviewing: boolean, reason: string }} 是否值得审查及原因
485
+ */
486
+ quickScanDiffContent(diffContent, filePath = '') {
487
+ if (!diffContent || diffContent.trim() === '') {
488
+ return { worthReviewing: false, reason: 'diff 内容为空' };
489
+ }
490
+
491
+ // 解析 diff 行
492
+ const lines = diffContent.split('\n');
493
+ // 只关注新增行(+ 开头)和删除行(- 开头)
494
+ const changedLines = lines.filter(line =>
495
+ line.startsWith('+') || line.startsWith('-')
496
+ );
497
+
498
+ // 如果没有实际变更行,跳过
499
+ if (changedLines.length === 0) {
500
+ return { worthReviewing: false, reason: '没有实际变更行' };
501
+ }
502
+
503
+ // 统计各类变更
504
+ const stats = {
505
+ importChanges: 0, // import 语句变更
506
+ commentChanges: 0, // 注释变更
507
+ whitespaceChanges: 0, // 空白/格式变更
508
+ getterSetterChanges: 0, // getter/setter 变更
509
+ logChanges: 0, // 日志打印变更
510
+ constantChanges: 0, // 常量定义变更
511
+ annotationChanges: 0, // 注解变更(如 @Override, @Autowired)
512
+ meaningfulChanges: 0, // 有意义的代码变更
513
+ };
514
+
515
+ // 分析每行变更
516
+ for (const line of changedLines) {
517
+ // 跳过 diff 头信息行(如 +++ b/file.txt)
518
+ if (line.startsWith('+++') || line.startsWith('---')) {
519
+ continue;
520
+ }
521
+
522
+ // 获取实际内容(去掉 +/- 前缀)
523
+ const content = line.substring(1).trim();
524
+
525
+ // 空行或纯空白
526
+ if (content === '' || /^\s*$/.test(content)) {
527
+ stats.whitespaceChanges++;
528
+ continue;
529
+ }
530
+
531
+ // import 语句(Java/JavaScript/Python 等)
532
+ if (/^import\s/.test(content) ||
533
+ /^from\s+['"]/.test(content) ||
534
+ /^require\s*\(/.test(content)) {
535
+ stats.importChanges++;
536
+ continue;
537
+ }
538
+
539
+ // 注释(单行或多行注释开始)
540
+ if (/^\/\/.*/.test(content) ||
541
+ /^\/\*.*\*\/$/.test(content) ||
542
+ /^\/\*[\s\S]*/.test(content) ||
543
+ /^\*[\s\S]*/.test(content) ||
544
+ /^#.*$/.test(content) ||
545
+ content.startsWith('*') ||
546
+ content.startsWith('//')) {
547
+ stats.commentChanges++;
548
+ continue;
549
+ }
550
+
551
+ // 注解(Java/Kotlin)
552
+ if (/^@\w+/.test(content) ||
553
+ /^@\w+\(/.test(content) ||
554
+ /^@Override/.test(content) ||
555
+ /^@Autowired/.test(content) ||
556
+ /^@Resource/.test(content) ||
557
+ /^@Inject/.test(content)) {
558
+ stats.annotationChanges++;
559
+ continue;
560
+ }
561
+
562
+ // getter/setter 方法
563
+ if (/^\s*(public|private|protected)?\s*\w+\s+get\w+\s*\(/.test(content) ||
564
+ /^\s*(public|private|protected)?\s*\w+\s+set\w+\s*\(/.test(content) ||
565
+ /^\s*public\s+\w+\s+is\w+\s*\(/.test(content) ||
566
+ /\.get\w+\(\)/.test(content) ||
567
+ /\.set\w+\(/.test(content)) {
568
+ // 只有 getter/setter 声明才算,调用不算
569
+ if (/^\s*(public|private|protected)?\s*\w+\s+(get|set|is)\w+\s*\(/.test(content)) {
570
+ stats.getterSetterChanges++;
571
+ continue;
572
+ }
573
+ }
574
+
575
+ // 日志打印语句
576
+ if (/^\s*(log|logger|Log|Logger|console)\.\w+\s*\(/.test(content) ||
577
+ /^\s*System\.out\.print/.test(content) ||
578
+ /^\s*System\.err\.print/.test(content) ||
579
+ /^\s*print\(/.test(content) ||
580
+ /^\s*println\(/.test(content) ||
581
+ /^\s*printf\(/.test(content) ||
582
+ /^\s*debugLog\(/.test(content) ||
583
+ /^\s*infoLog\(/.test(content) ||
584
+ /^\s*warnLog\(/.test(content) ||
585
+ /^\s*errorLog\(/.test(content)) {
586
+ stats.logChanges++;
587
+ continue;
588
+ }
589
+
590
+ // 常量定义(简单的静态常量)
591
+ if (/^\s*(public|private|protected)?\s*(static)?\s*(final)?\s*\w+\s+\w+\s*=\s*[^;]+;/.test(content) ||
592
+ /^\s*const\s+\w+\s*=/.test(content) ||
593
+ /^\s*static\s+final\s+\w+\s+\w+\s*=/.test(content)) {
594
+ stats.constantChanges++;
595
+ continue;
596
+ }
597
+
598
+ // 其他变更视为有意义的代码变更
599
+ stats.meaningfulChanges++;
600
+ }
601
+
602
+ // 判断是否值得审查
603
+ const totalChanges = changedLines.length;
604
+ const lowValueChanges = stats.importChanges + stats.commentChanges +
605
+ stats.whitespaceChanges + stats.annotationChanges;
606
+
607
+ // 统计低价值变更比例
608
+ const lowValueRatio = totalChanges > 0 ? lowValueChanges / totalChanges : 0;
609
+
610
+ debugLog(`[${filePath}] 快速扫描统计: 总变更=${totalChanges}, import=${stats.importChanges}, 注释=${stats.commentChanges}, 空白=${stats.whitespaceChanges}, 注解=${stats.annotationChanges}, getter/setter=${stats.getterSetterChanges}, 日志=${stats.logChanges}, 常量=${stats.constantChanges}, 有意义=${stats.meaningfulChanges}, 低价值比例=${lowValueRatio.toFixed(2)}`);
611
+
612
+ // 决策规则
613
+ // 1. 如果没有任何有意义变更,跳过
614
+ if (stats.meaningfulChanges === 0) {
615
+ // 进一步检查:如果只有 getter/setter、日志、常量变更,也跳过
616
+ const onlyHelperChanges = stats.getterSetterChanges + stats.logChanges + stats.constantChanges;
617
+ if (onlyHelperChanges === totalChanges - stats.importChanges - stats.whitespaceChanges) {
618
+ return {
619
+ worthReviewing: false,
620
+ reason: `仅有辅助性变更(getter/setter=${stats.getterSetterChanges}, 日志=${stats.logChanges}, 常量=${stats.constantChanges})`
621
+ };
622
+ }
623
+
624
+ return {
625
+ worthReviewing: false,
626
+ reason: `无有意义代码变更(import=${stats.importChanges}, 注释=${stats.commentChanges}, 空白=${stats.whitespaceChanges}, 注解=${stats.annotationChanges})`
627
+ };
628
+ }
629
+
630
+ // 2. 如果低价值变更占比超过 90%,且有意义变更少于 3 行,跳过
631
+ if (lowValueRatio > 0.9 && stats.meaningfulChanges < 3) {
632
+ return {
633
+ worthReviewing: false,
634
+ reason: `低价值变更占比过高(${(lowValueRatio * 100).toFixed(1)}%),有意义变更仅 ${stats.meaningfulChanges} 行`
635
+ };
636
+ }
637
+
638
+ // 3. 如果有意义变更少于 2 行,且主要是辅助性代码,跳过
639
+ if (stats.meaningfulChanges < 2 &&
640
+ (stats.getterSetterChanges > 0 || stats.logChanges > 0 || stats.constantChanges > 0)) {
641
+ return {
642
+ worthReviewing: false,
643
+ reason: `有意义变更太少(${stats.meaningfulChanges} 行),且包含辅助性代码`
644
+ };
645
+ }
646
+
647
+ // 其他情况值得审查
648
+ return {
649
+ worthReviewing: true,
650
+ reason: `有 ${stats.meaningfulChanges} 行有意义代码变更,值得审查`
651
+ };
652
+ }
653
+
315
654
  /**
316
655
  * 获取合并请求的diff信息
317
656
  * @param {number} projectId GitLab项目ID
@@ -497,6 +836,25 @@ class GitLabCodeReviewer {
497
836
  const diffLines = diffObject.diff.split('\n');
498
837
  const codeLines = diffLines.filter(line => !line.startsWith('@@')).join('\n');
499
838
 
839
+ // ========== 阶段1:快速扫描(本地分析,不调用 AI)==========
840
+ const quickScanResult = this.quickScanDiffContent(diffObject.diff, diffObject.new_path || diffObject.old_path);
841
+ if (!quickScanResult.worthReviewing) {
842
+ infoLog(`[跳过审查] ${diffObject.new_path || diffObject.old_path}#${blockIndex}: ${quickScanResult.reason}`);
843
+ // 记录快速扫描跳过统计
844
+ this.metrics.recordQuickScanSkip(quickScanResult.reason);
845
+ return {
846
+ diff_info: diffObject,
847
+ block_index: blockIndex,
848
+ review_result: { reportContent: '<REPORT>\n## 🤖 AI 代码审查结果\n\n该变更无需审查。\n</REPORT>', lineInfo: '[]' },
849
+ temp_file_path: null,
850
+ hallucination_detected: false,
851
+ skipped_by_quick_scan: true,
852
+ skip_reason: quickScanResult.reason,
853
+ };
854
+ }
855
+ debugLog(`[快速扫描通过] ${diffObject.new_path || diffObject.old_path}#${blockIndex}: ${quickScanResult.reason}`);
856
+
857
+ // ========== 阶段2:完整审查(调用 AI)==========
500
858
  // 构造临时文件内容:纯代码在前,元数据在后
501
859
  const diffContentWithMetadata = `${codeLines}
502
860
 
@@ -664,10 +1022,15 @@ class GitLabCodeReviewer {
664
1022
  collectAllReviewReports(results) {
665
1023
  let allReportsText = '';
666
1024
 
667
- // 遍历所有审查结果,过滤掉检测到幻觉的结果
668
- const validResults = results.filter(result => !result.hallucination_detected);
1025
+ // 遍历所有审查结果,过滤掉检测到幻觉的结果和快速扫描跳过的结果
1026
+ const validResults = results.filter(result =>
1027
+ !result.hallucination_detected && !result.skipped_by_quick_scan
1028
+ );
1029
+
1030
+ const skippedCount = results.filter(r => r.skipped_by_quick_scan).length;
1031
+ const hallucinationCount = results.filter(r => r.hallucination_detected).length;
669
1032
 
670
- debugLog(`汇总报告过滤:总共 ${results.length} 个结果,过滤掉 ${results.length - validResults.length} 个幻觉检测结果,保留 ${validResults.length} 个有效结果`);
1033
+ debugLog(`汇总报告过滤:总共 ${results.length} 个结果,快速扫描跳过 ${skippedCount} 个,幻觉检测跳过 ${hallucinationCount} 个,保留 ${validResults.length} 个有效结果`);
671
1034
 
672
1035
  for (let i = 0; i < validResults.length; i++) {
673
1036
  const result = validResults[i];
@@ -895,7 +1258,7 @@ ${allReportsText}
895
1258
  }
896
1259
 
897
1260
  /**
898
- * 使用Claude对单个diff文件进行代码审核
1261
+ * 使用Claude对单个diff文件进行代码审核(两阶段:Plan + Main)
899
1262
  * @param {string} filePath 临时文件路径
900
1263
  * @param {string} blockIdentifier 块标识(格式:文件路径#块索引),用于日志追踪
901
1264
  * @param {boolean} isLargeFile 是否为大文件审查模式
@@ -905,6 +1268,42 @@ ${allReportsText}
905
1268
  debugLog(`[${blockIdentifier}] 开始审核文件: ${filePath}, isLargeFile: ${isLargeFile}`);
906
1269
  const startTime = Date.now();
907
1270
 
1271
+ // ========== 读取临时文件内容,计算改动行数 ==========
1272
+ let diffContent = '';
1273
+ try {
1274
+ diffContent = fs.readFileSync(filePath, 'utf-8');
1275
+ } catch (readError) {
1276
+ debugLog(`[${blockIdentifier}] 无法读取临时文件:${readError.message}`);
1277
+ diffContent = '';
1278
+ }
1279
+
1280
+ // 计算改动行数(排除元数据部分)
1281
+ const codeContent = diffContent.split('\n')
1282
+ .filter(line => !line.startsWith('#')) // 排除元数据行
1283
+ .join('\n');
1284
+ const changeLines = countChangeLines(codeContent);
1285
+
1286
+ debugLog(`[${blockIdentifier}] 改动行数统计:${changeLines} 行`);
1287
+
1288
+ // ========== Phase 1: Plan 阶段(风险分析)==========
1289
+ // 触发条件:改动行数超过阈值,且不是大文件模式
1290
+ let planGuidanceText = '';
1291
+
1292
+ if (changeLines >= PLAN_LINE_THRESHOLD && !isLargeFile) {
1293
+ debugLog(`[${blockIdentifier}] 改动 ${changeLines} 行 >= ${PLAN_LINE_THRESHOLD},启动 Plan 阶段`);
1294
+
1295
+ const planResult = await runPlanPhase(filePath, codeContent, blockIdentifier);
1296
+ if (planResult) {
1297
+ planGuidanceText = buildPlanGuidanceText(planResult);
1298
+ debugLog(`[${blockIdentifier}] Plan 阶段生成审查指导,长度 ${planGuidanceText.length}`);
1299
+ } else {
1300
+ debugLog(`[${blockIdentifier}] Plan 阶段无有效结果,跳过`);
1301
+ }
1302
+ } else {
1303
+ debugLog(`[${blockIdentifier}] 改动 ${changeLines} 行 < ${PLAN_LINE_THRESHOLD}(或大文件模式),跳过 Plan 阶段`);
1304
+ }
1305
+
1306
+ // ========== Phase 2: Main 阶段(执行审查)==========
908
1307
  // 大文件模式下的特殊提示
909
1308
  const largeFilePrompt = isLargeFile ? `
910
1309
  **⚠️ 大文件审查模式 - 强制格式要求**:
@@ -914,11 +1313,22 @@ ${allReportsText}
914
1313
  3. 行号基于完整文件实际行号计算
915
1314
  4. **严禁在输出中写 "Let me format" 等中间过程文本**
916
1315
  5. **必须在 </REPORT> 后立即输出 <LINE_INFO> 标签,包含每个问题的行号**
1316
+ ` : '';
1317
+
1318
+ // 根据是否有 Plan 指导,构建不同的 prompt
1319
+ const planPromptSection = planGuidanceText ? `
1320
+ ${planGuidanceText}
1321
+
1322
+ **⚠️ 上述风险分析是基于代码结构预判的,请重点验证这些风险点是否真实存在**:
1323
+ - 如果风险点确实存在问题 → 按正常格式报告
1324
+ - 如果风险点经验证不存在 → 不报告该问题(不要为了"回应"而编造问题)
1325
+ - 如果发现其他未预判的问题 → 正常报告
917
1326
  ` : '';
918
1327
 
919
1328
  const prompt = `请调用 simple-code-review 技能审核代码变更。
920
1329
  文件路径:${filePath}
921
1330
  ${largeFilePrompt}
1331
+ ${planPromptSection}
922
1332
  **⚠️ 强制输出格式要求(违反会导致审查结果丢失)**:
923
1333
 
924
1334
  必须以规定格式输出,不要输出任何分析过程、思考过程或中间文本:
@@ -927,8 +1337,9 @@ ${largeFilePrompt}
927
1337
  - 有严重问题时,LINE_INFO 必须包含对应行号,否则结果被丢弃
928
1338
  - 无问题时输出 <LINE_INFO>[]</LINE_INFO>
929
1339
  - 不要输出 "Let me analyze"、"Let me format" 等文本`;
1340
+
930
1341
  //打印
931
- debugLog(`[${blockIdentifier}] Claude命令: ${prompt}`);
1342
+ debugLog(`[${blockIdentifier}] Main 阶段 Claude 命令 prompt 长度: ${prompt.length}`);
932
1343
  // 最多重试5次,直到结果包含"🤖 AI 代码审查结果"或达到最大重试次数
933
1344
  let attempts = 0;
934
1345
  const maxAttempts = 5;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job51-gitlab-cr-node-jt-1",
3
- "version": "3.1.2",
3
+ "version": "3.1.4",
4
4
  "description": "GitLab merge request code review tool with AI-powered analysis and project context support",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -0,0 +1,332 @@
1
+ /**
2
+ * 规则加载器 - 支持四层优先级链配置
3
+ *
4
+ * 优先级顺序(从高到低):
5
+ * 1. CLI 覆盖 (--rule <path>)
6
+ * 2. 项目配置 (<repoDir>/.opencodereview/rule.json)
7
+ * 3. 用户配置 (~/.opencodereview/rule.json)
8
+ * 4. 内置规则 (system_rules.json + rule_docs/*.md)
9
+ */
10
+
11
+ const fs = require('fs');
12
+ const path = require('path');
13
+ const { debugLog, warnLog } = require('./utils');
14
+
15
+ // 使用 minimatch 进行 glob 匹配(内置支持,无需额外依赖)
16
+ function minimatch(filePath, pattern, options = {}) {
17
+ // 简化的 glob 匹配实现
18
+ // 支持 ** 和 * 通配符
19
+ const nocase = options.nocase !== false;
20
+
21
+ // 转换 pattern 为正则表达式
22
+ let regexPattern = pattern;
23
+
24
+ // 先处理 **(匹配任意深度目录)
25
+ regexPattern = regexPattern.replace(/\*\*/g, '{{DOUBLESTAR}}');
26
+
27
+ // 处理 *(匹配单层任意字符,不包括路径分隔符)
28
+ regexPattern = regexPattern.replace(/\*/g, '[^/]*');
29
+
30
+ // 恢复 **
31
+ regexPattern = regexPattern.replace(/\{\{DOUBLESTAR\}\}/g, '.*');
32
+
33
+ // 处理 ?(匹配单个字符)
34
+ regexPattern = regexPattern.replace(/\?/g, '[^/]');
35
+
36
+ // 处理 {a,b,c} 扩展
37
+ const braceMatch = regexPattern.match(/\{([^}]+)\}/);
38
+ if (braceMatch) {
39
+ const optionsList = braceMatch[1].split(',');
40
+ // 返回多个 pattern 的匹配结果
41
+ const patterns = optionsList.map(opt =>
42
+ regexPattern.replace(/\{[^}]+\}/, opt)
43
+ );
44
+ return patterns.some(p => matchRegex(filePath, p, nocase));
45
+ }
46
+
47
+ return matchRegex(filePath, regexPattern, nocase);
48
+ }
49
+
50
+ function matchRegex(filePath, regexPattern, nocase) {
51
+ // 确保从头匹配
52
+ if (!regexPattern.startsWith('^')) {
53
+ regexPattern = '^' + regexPattern;
54
+ }
55
+ // 确保匹配到结尾
56
+ if (!regexPattern.endsWith('$')) {
57
+ regexPattern = regexPattern + '$';
58
+ }
59
+
60
+ const flags = nocase ? 'i' : '';
61
+ const regex = new RegExp(regexPattern, flags);
62
+
63
+ // 标准化路径
64
+ const normalizedPath = filePath.replace(/\\/g, '/');
65
+ return regex.test(normalizedPath);
66
+ }
67
+
68
+ /**
69
+ * 扩展 brace pattern(如 {ts,tsx})
70
+ * @param {string} pattern 包含 brace 的 pattern
71
+ * @returns {string[]} 扩展后的 pattern 数组
72
+ */
73
+ function expandBraces(pattern) {
74
+ const braceMatch = pattern.match(/\{([^}]+)\}/);
75
+ if (!braceMatch) {
76
+ return [pattern];
77
+ }
78
+
79
+ const prefix = pattern.substring(0, braceMatch.index);
80
+ const suffix = pattern.substring(braceMatch.index + braceMatch[0].length);
81
+ const options = braceMatch[1].split(',');
82
+
83
+ return options.map(opt => prefix + opt + suffix);
84
+ }
85
+
86
+ class RulesLoader {
87
+ constructor(options = {}) {
88
+ // 规则目录(默认为 .claude/rules)
89
+ this.rulesDir = options.rulesDir || path.join(__dirname, '.claude', 'rules');
90
+
91
+ // CLI 覆盖路径
92
+ this.cliOverride = options.cliOverride || null;
93
+
94
+ // 加载各级配置
95
+ this.systemRules = this.loadSystemRules();
96
+ this.projectRules = this.loadProjectRules(options.projectDir || process.cwd());
97
+ this.userRules = this.loadUserRules();
98
+
99
+ debugLog(`RulesLoader 初始化完成: rulesDir=${this.rulesDir}`);
100
+ }
101
+
102
+ /**
103
+ * 加载内置系统规则配置
104
+ * @returns {Object} 系统规则配置
105
+ */
106
+ loadSystemRules() {
107
+ const configPath = path.join(this.rulesDir, 'system_rules.json');
108
+
109
+ if (fs.existsSync(configPath)) {
110
+ try {
111
+ const content = fs.readFileSync(configPath, 'utf-8');
112
+ return JSON.parse(content);
113
+ } catch (error) {
114
+ warnLog(`加载系统规则配置失败: ${error.message}`);
115
+ }
116
+ }
117
+
118
+ // 默认配置
119
+ return {
120
+ default_rule: 'code-review-rules.md',
121
+ path_rule_map: {}
122
+ };
123
+ }
124
+
125
+ /**
126
+ * 加载项目级规则配置
127
+ * @param {string} projectDir 项目目录
128
+ * @returns {Object|null} 项目规则配置
129
+ */
130
+ loadProjectRules(projectDir) {
131
+ // 支持多种配置文件位置
132
+ const configPaths = [
133
+ path.join(projectDir, '.opencodereview', 'rule.json'),
134
+ path.join(projectDir, '.code-review', 'rule.json'),
135
+ path.join(projectDir, '.cr-node', 'rule.json')
136
+ ];
137
+
138
+ for (const configPath of configPaths) {
139
+ if (fs.existsSync(configPath)) {
140
+ try {
141
+ const content = fs.readFileSync(configPath, 'utf-8');
142
+ debugLog(`加载项目规则配置: ${configPath}`);
143
+ return JSON.parse(content);
144
+ } catch (error) {
145
+ warnLog(`加载项目规则配置失败 (${configPath}): ${error.message}`);
146
+ }
147
+ }
148
+ }
149
+
150
+ return null;
151
+ }
152
+
153
+ /**
154
+ * 加载用户级规则配置
155
+ * @returns {Object|null} 用户规则配置
156
+ */
157
+ loadUserRules() {
158
+ const userHome = process.env.HOME || process.env.USERPROFILE || '';
159
+ const configPaths = [
160
+ path.join(userHome, '.opencodereview', 'rule.json'),
161
+ path.join(userHome, '.code-review', 'rule.json')
162
+ ];
163
+
164
+ for (const configPath of configPaths) {
165
+ if (fs.existsSync(configPath)) {
166
+ try {
167
+ const content = fs.readFileSync(configPath, 'utf-8');
168
+ debugLog(`加载用户规则配置: ${configPath}`);
169
+ return JSON.parse(content);
170
+ } catch (error) {
171
+ warnLog(`加载用户规则配置失败 (${configPath}): ${error.message}`);
172
+ }
173
+ }
174
+ }
175
+
176
+ return null;
177
+ }
178
+
179
+ /**
180
+ * 四层优先级链解析规则
181
+ * @param {string} filePath 待审查的文件路径
182
+ * @returns {{rule: string, source: string, pattern: string}} 规则详情
183
+ */
184
+ resolve(filePath) {
185
+ // 优先级1:CLI 覆盖
186
+ if (this.cliOverride) {
187
+ try {
188
+ const ruleContent = fs.readFileSync(this.cliOverride, 'utf-8');
189
+ debugLog(`使用 CLI 覆盖规则: ${this.cliOverride}`);
190
+ return {
191
+ rule: ruleContent,
192
+ source: 'cli',
193
+ pattern: 'override'
194
+ };
195
+ } catch (error) {
196
+ warnLog(`CLI 规则文件读取失败: ${error.message}`);
197
+ }
198
+ }
199
+
200
+ // 优先级2:项目配置
201
+ if (this.projectRules) {
202
+ const matched = this.matchRule(filePath, this.projectRules, 'project');
203
+ if (matched) return matched;
204
+ }
205
+
206
+ // 优先级3:用户配置
207
+ if (this.userRules) {
208
+ const matched = this.matchRule(filePath, this.userRules, 'user');
209
+ if (matched) return matched;
210
+ }
211
+
212
+ // 优先级4:内置规则(按语言匹配)
213
+ const systemMatched = this.matchRule(filePath, this.systemRules, 'system');
214
+ if (systemMatched) return systemMatched;
215
+
216
+ // 最终 fallback:默认规则
217
+ const defaultRulePath = path.join(this.rulesDir, this.systemRules.default_rule);
218
+ try {
219
+ const ruleContent = fs.readFileSync(defaultRulePath, 'utf-8');
220
+ debugLog(`使用默认规则: ${this.systemRules.default_rule}`);
221
+ return {
222
+ rule: ruleContent,
223
+ source: 'default',
224
+ pattern: 'default'
225
+ };
226
+ } catch (error) {
227
+ warnLog(`默认规则文件读取失败: ${error.message}`);
228
+ return {
229
+ rule: '',
230
+ source: 'none',
231
+ pattern: 'none'
232
+ };
233
+ }
234
+ }
235
+
236
+ /**
237
+ * 根据 glob pattern 匹配规则
238
+ * @param {string} filePath 文件路径
239
+ * @param {Object} rulesConfig 规则配置对象
240
+ * @param {string} source 来源标识
241
+ * @returns {{rule: string, source: string, pattern: string}|null}
242
+ */
243
+ matchRule(filePath, rulesConfig, source) {
244
+ const pathRuleMap = rulesConfig.path_rule_map || {};
245
+
246
+ // 按顺序匹配(第一个匹配生效)
247
+ for (const [pattern, ruleFile] of Object.entries(pathRuleMap)) {
248
+ // 扩展 brace pattern
249
+ const expandedPatterns = expandBraces(pattern);
250
+
251
+ for (const expandedPattern of expandedPatterns) {
252
+ if (minimatch(filePath, expandedPattern, { nocase: true })) {
253
+ // 查找规则文件
254
+ const rulePath = this.findRuleFile(ruleFile);
255
+ if (rulePath && fs.existsSync(rulePath)) {
256
+ try {
257
+ const ruleContent = fs.readFileSync(rulePath, 'utf-8');
258
+ debugLog(`匹配规则: pattern=${expandedPattern}, file=${ruleFile}, source=${source}`);
259
+ return {
260
+ rule: ruleContent,
261
+ source: source,
262
+ pattern: expandedPattern
263
+ };
264
+ } catch (error) {
265
+ warnLog(`规则文件读取失败 (${rulePath}): ${error.message}`);
266
+ }
267
+ }
268
+ }
269
+ }
270
+ }
271
+
272
+ return null;
273
+ }
274
+
275
+ /**
276
+ * 查找规则文件路径(支持多个位置)
277
+ * @param {string} ruleFile 规则文件名
278
+ * @returns {string|null} 规则文件完整路径
279
+ */
280
+ findRuleFile(ruleFile) {
281
+ // 搜索位置优先级
282
+ const searchPaths = [
283
+ // 项目规则目录
284
+ path.join(process.cwd(), '.opencodereview', 'rules', ruleFile),
285
+ path.join(process.cwd(), '.code-review', 'rules', ruleFile),
286
+ // 用户规则目录
287
+ path.join(process.env.HOME || process.env.USERPROFILE || '', '.opencodereview', 'rules', ruleFile),
288
+ // 内置规则目录
289
+ path.join(this.rulesDir, ruleFile)
290
+ ];
291
+
292
+ for (const searchPath of searchPaths) {
293
+ if (fs.existsSync(searchPath)) {
294
+ return searchPath;
295
+ }
296
+ }
297
+
298
+ return null;
299
+ }
300
+
301
+ /**
302
+ * 设置 CLI 覆盖规则路径
303
+ * @param {string} rulePath 规则文件路径
304
+ */
305
+ setCliOverride(rulePath) {
306
+ this.cliOverride = rulePath;
307
+ debugLog(`设置 CLI 覆盖规则: ${rulePath}`);
308
+ }
309
+
310
+ /**
311
+ * 获取当前配置的摘要信息
312
+ * @returns {Object} 配置摘要
313
+ */
314
+ getConfigSummary() {
315
+ return {
316
+ cliOverride: this.cliOverride,
317
+ projectRules: this.projectRules ? 'loaded' : 'none',
318
+ userRules: this.userRules ? 'loaded' : 'none',
319
+ systemRules: {
320
+ default: this.systemRules.default_rule,
321
+ patterns: Object.keys(this.systemRules.path_rule_map || {}).length
322
+ },
323
+ rulesDir: this.rulesDir
324
+ };
325
+ }
326
+ }
327
+
328
+ module.exports = {
329
+ RulesLoader,
330
+ minimatch,
331
+ expandBraces
332
+ };
package/utils.js CHANGED
@@ -116,6 +116,10 @@ class MetricsCollector {
116
116
  totalProblemsFound: 0,
117
117
  seriousProblemsFound: 0,
118
118
 
119
+ // 快速扫描跳过统计
120
+ quickScanSkipped: 0, // 快速扫描跳过的 diff 块数
121
+ quickScanSkipReasons: {}, // 跳过原因统计
122
+
119
123
  // 耗时统计(毫秒)
120
124
  reviewStartTime: 0,
121
125
  reviewEndTime: 0,
@@ -271,11 +275,31 @@ class MetricsCollector {
271
275
  this.metrics.commentsPublished++;
272
276
  }
273
277
 
278
+ // 记录快速扫描跳过
279
+ recordQuickScanSkip(reason = '') {
280
+ this.metrics.quickScanSkipped++;
281
+ if (reason) {
282
+ // 简化原因,只保留关键信息
283
+ const simplifiedReason = reason.split('(')[0] || reason;
284
+ if (!this.metrics.quickScanSkipReasons[simplifiedReason]) {
285
+ this.metrics.quickScanSkipReasons[simplifiedReason] = 0;
286
+ }
287
+ this.metrics.quickScanSkipReasons[simplifiedReason]++;
288
+ }
289
+ }
290
+
274
291
  // 打印统计摘要
275
292
  printSummary() {
276
293
  console.log('\n========== 审查统计 ==========');
277
294
  console.log(`审查文件数:${this.metrics.totalFilesReviewed}`);
278
295
  console.log(`审查 diff 块数:${this.metrics.totalBlocksReviewed}`);
296
+ console.log(`快速扫描跳过:${this.metrics.quickScanSkipped} 个 diff 块(节省 AI 调用)`);
297
+ if (Object.keys(this.metrics.quickScanSkipReasons).length > 0) {
298
+ console.log(' 跳过原因统计:');
299
+ for (const [reason, count] of Object.entries(this.metrics.quickScanSkipReasons)) {
300
+ console.log(` - ${reason}: ${count} 个`);
301
+ }
302
+ }
279
303
  console.log(`发现问题总数:${this.metrics.totalProblemsFound}`);
280
304
  console.log(`严重问题数:${this.metrics.seriousProblemsFound}`);
281
305
  console.log(`发布评论数:${this.metrics.commentsPublished}`);