job51-gitlab-cr-node-jt-1 3.1.3 → 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.
- package/.claude/rules/system_rules.json +25 -0
- package/.claude/rules/typescript.md +229 -0
- package/index.js +214 -2
- package/package.json +1 -1
- package/rules-loader.js +332 -0
|
@@ -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/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}`);
|
|
@@ -1094,7 +1258,7 @@ ${allReportsText}
|
|
|
1094
1258
|
}
|
|
1095
1259
|
|
|
1096
1260
|
/**
|
|
1097
|
-
* 使用Claude对单个diff
|
|
1261
|
+
* 使用Claude对单个diff文件进行代码审核(两阶段:Plan + Main)
|
|
1098
1262
|
* @param {string} filePath 临时文件路径
|
|
1099
1263
|
* @param {string} blockIdentifier 块标识(格式:文件路径#块索引),用于日志追踪
|
|
1100
1264
|
* @param {boolean} isLargeFile 是否为大文件审查模式
|
|
@@ -1104,6 +1268,42 @@ ${allReportsText}
|
|
|
1104
1268
|
debugLog(`[${blockIdentifier}] 开始审核文件: ${filePath}, isLargeFile: ${isLargeFile}`);
|
|
1105
1269
|
const startTime = Date.now();
|
|
1106
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 阶段(执行审查)==========
|
|
1107
1307
|
// 大文件模式下的特殊提示
|
|
1108
1308
|
const largeFilePrompt = isLargeFile ? `
|
|
1109
1309
|
**⚠️ 大文件审查模式 - 强制格式要求**:
|
|
@@ -1113,11 +1313,22 @@ ${allReportsText}
|
|
|
1113
1313
|
3. 行号基于完整文件实际行号计算
|
|
1114
1314
|
4. **严禁在输出中写 "Let me format" 等中间过程文本**
|
|
1115
1315
|
5. **必须在 </REPORT> 后立即输出 <LINE_INFO> 标签,包含每个问题的行号**
|
|
1316
|
+
` : '';
|
|
1317
|
+
|
|
1318
|
+
// 根据是否有 Plan 指导,构建不同的 prompt
|
|
1319
|
+
const planPromptSection = planGuidanceText ? `
|
|
1320
|
+
${planGuidanceText}
|
|
1321
|
+
|
|
1322
|
+
**⚠️ 上述风险分析是基于代码结构预判的,请重点验证这些风险点是否真实存在**:
|
|
1323
|
+
- 如果风险点确实存在问题 → 按正常格式报告
|
|
1324
|
+
- 如果风险点经验证不存在 → 不报告该问题(不要为了"回应"而编造问题)
|
|
1325
|
+
- 如果发现其他未预判的问题 → 正常报告
|
|
1116
1326
|
` : '';
|
|
1117
1327
|
|
|
1118
1328
|
const prompt = `请调用 simple-code-review 技能审核代码变更。
|
|
1119
1329
|
文件路径:${filePath}
|
|
1120
1330
|
${largeFilePrompt}
|
|
1331
|
+
${planPromptSection}
|
|
1121
1332
|
**⚠️ 强制输出格式要求(违反会导致审查结果丢失)**:
|
|
1122
1333
|
|
|
1123
1334
|
必须以规定格式输出,不要输出任何分析过程、思考过程或中间文本:
|
|
@@ -1126,8 +1337,9 @@ ${largeFilePrompt}
|
|
|
1126
1337
|
- 有严重问题时,LINE_INFO 必须包含对应行号,否则结果被丢弃
|
|
1127
1338
|
- 无问题时输出 <LINE_INFO>[]</LINE_INFO>
|
|
1128
1339
|
- 不要输出 "Let me analyze"、"Let me format" 等文本`;
|
|
1340
|
+
|
|
1129
1341
|
//打印
|
|
1130
|
-
debugLog(`[${blockIdentifier}] Claude
|
|
1342
|
+
debugLog(`[${blockIdentifier}] Main 阶段 Claude 命令 prompt 长度: ${prompt.length}`);
|
|
1131
1343
|
// 最多重试5次,直到结果包含"🤖 AI 代码审查结果"或达到最大重试次数
|
|
1132
1344
|
let attempts = 0;
|
|
1133
1345
|
const maxAttempts = 5;
|
package/package.json
CHANGED
package/rules-loader.js
ADDED
|
@@ -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
|
+
};
|