smart-review 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +713 -0
- package/bin/install.js +280 -0
- package/bin/review.js +256 -0
- package/index.js +5 -0
- package/lib/ai-client-pool.js +434 -0
- package/lib/ai-client.js +1413 -0
- package/lib/config-loader.js +223 -0
- package/lib/default-config.js +203 -0
- package/lib/reviewer.js +1340 -0
- package/lib/segmented-analyzer.js +490 -0
- package/lib/smart-batching.js +1671 -0
- package/lib/utils/concurrency-limiter.js +46 -0
- package/lib/utils/constants.js +117 -0
- package/lib/utils/git-diff-parser.js +624 -0
- package/lib/utils/logger.js +66 -0
- package/lib/utils/strip.js +221 -0
- package/package.json +44 -0
- package/templates/rules/best-practices.js +111 -0
- package/templates/rules/performance.js +123 -0
- package/templates/rules/security.js +311 -0
- package/templates/smart-review.json +80 -0
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import { createRequire } from 'module';
|
|
4
|
+
import { defaultConfig, defaultRules } from './default-config.js';
|
|
5
|
+
import { logger } from './utils/logger.js';
|
|
6
|
+
|
|
7
|
+
export class ConfigLoader {
|
|
8
|
+
constructor(projectRoot) {
|
|
9
|
+
this.projectRoot = projectRoot;
|
|
10
|
+
this.reviewDir = path.join(projectRoot, '.smart-review');
|
|
11
|
+
|
|
12
|
+
// 启动时清理遗留的临时文件
|
|
13
|
+
this.cleanupTempFiles();
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
cleanupTempFiles() {
|
|
17
|
+
try {
|
|
18
|
+
const tempDir = path.join(this.reviewDir, 'local-rules', '.temp-smart-review');
|
|
19
|
+
if (fs.existsSync(tempDir)) {
|
|
20
|
+
const files = fs.readdirSync(tempDir);
|
|
21
|
+
for (const file of files) {
|
|
22
|
+
if (file.startsWith('temp-') && file.endsWith('.mjs')) {
|
|
23
|
+
try {
|
|
24
|
+
fs.unlinkSync(path.join(tempDir, file));
|
|
25
|
+
} catch (e) {
|
|
26
|
+
// 忽略单个文件删除失败
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// 尝试删除空的临时目录
|
|
32
|
+
try {
|
|
33
|
+
fs.rmdirSync(tempDir);
|
|
34
|
+
} catch (e) {
|
|
35
|
+
// 忽略目录删除失败(可能不为空)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
} catch (error) {
|
|
39
|
+
// 忽略清理过程中的错误,不影响主要功能
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async loadConfig() {
|
|
44
|
+
let externalConfig = {};
|
|
45
|
+
|
|
46
|
+
// 尝试加载外部配置文件
|
|
47
|
+
const configPath = path.join(this.reviewDir, 'smart-review.json');
|
|
48
|
+
if (fs.existsSync(configPath)) {
|
|
49
|
+
try {
|
|
50
|
+
const configContent = await fs.promises.readFile(configPath, 'utf8');
|
|
51
|
+
externalConfig = JSON.parse(configContent);
|
|
52
|
+
|
|
53
|
+
} catch (error) {
|
|
54
|
+
logger.warn('外部配置文件解析失败,使用默认配置:', error.message);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// 深度合并配置(外部配置覆盖默认配置)
|
|
59
|
+
const mergedConfig = this.deepMerge(defaultConfig, externalConfig);
|
|
60
|
+
|
|
61
|
+
// 设置项目根目录
|
|
62
|
+
mergedConfig.projectRoot = this.projectRoot;
|
|
63
|
+
mergedConfig.reviewDir = this.reviewDir;
|
|
64
|
+
|
|
65
|
+
return mergedConfig;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async loadRules(config = {}) {
|
|
69
|
+
// 2) 加载外部规则
|
|
70
|
+
const externalRules = await this.loadExternalRules();
|
|
71
|
+
|
|
72
|
+
// 根据配置决定规则加载策略
|
|
73
|
+
if (config.useExternalRulesOnly) {
|
|
74
|
+
// 仅使用外部规则模式:只返回外部规则,不加载内置规则
|
|
75
|
+
logger.info('使用外部规则模式:仅加载 local-rules 目录中的规则');
|
|
76
|
+
return externalRules;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// 默认合并模式:内置规则 + 外部规则合并
|
|
80
|
+
// 1) 收集内置规则
|
|
81
|
+
const builtInRules = Object.values(defaultRules).flat();
|
|
82
|
+
|
|
83
|
+
// 3) 根据规则ID进行去重合并(外部规则优先生效)
|
|
84
|
+
const ruleMap = new Map();
|
|
85
|
+
|
|
86
|
+
for (const rule of builtInRules) {
|
|
87
|
+
const key = rule.id || `${rule.pattern}__${rule.risk}`;
|
|
88
|
+
ruleMap.set(key, rule);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
for (const rule of externalRules) {
|
|
92
|
+
const key = rule.id || `${rule.pattern}__${rule.risk}`;
|
|
93
|
+
ruleMap.set(key, rule); // 外部规则覆盖同ID的内置规则
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const allRules = Array.from(ruleMap.values());
|
|
97
|
+
|
|
98
|
+
return allRules;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async loadExternalRules() {
|
|
102
|
+
const externalRules = [];
|
|
103
|
+
// 改为读取 .smart-review/local-rules 作为静态规则目录,避免与AI提示目录冲突
|
|
104
|
+
const rulesDir = path.join(this.reviewDir, 'local-rules');
|
|
105
|
+
|
|
106
|
+
if (!fs.existsSync(rulesDir)) {
|
|
107
|
+
return externalRules;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
try {
|
|
111
|
+
const ruleFiles = fs.readdirSync(rulesDir);
|
|
112
|
+
|
|
113
|
+
for (const file of ruleFiles) {
|
|
114
|
+
if (file.endsWith('.js') || file.endsWith('.mjs') || file.endsWith('.json')) {
|
|
115
|
+
const filePath = path.join(rulesDir, file);
|
|
116
|
+
const rules = await this.loadRuleFile(filePath);
|
|
117
|
+
externalRules.push(...rules);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
logger.warn('加载外部规则失败:', error.message);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return externalRules;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
async loadRuleFile(filePath) {
|
|
128
|
+
try {
|
|
129
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
130
|
+
|
|
131
|
+
if (ext === '.js' || ext === '.mjs') {
|
|
132
|
+
let mod;
|
|
133
|
+
|
|
134
|
+
if (ext === '.mjs') {
|
|
135
|
+
// .mjs 文件肯定是 ES 模块,直接导入
|
|
136
|
+
const fileUrl = `file://${filePath.replace(/\\/g, '/')}`;
|
|
137
|
+
mod = await import(fileUrl);
|
|
138
|
+
} else {
|
|
139
|
+
// .js 文件需要判断 ESM/CommonJS
|
|
140
|
+
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
141
|
+
const looksESM = /\bexport\b|^\s*import\b/m.test(content);
|
|
142
|
+
|
|
143
|
+
if (looksESM) {
|
|
144
|
+
// 为了避免 base64 编码导致转义字符丢失,我们创建一个临时的 .mjs 文件
|
|
145
|
+
const tempDir = path.join(path.dirname(filePath), '.temp-smart-review');
|
|
146
|
+
const tempFile = path.join(tempDir, `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}.mjs`);
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
// 确保临时目录存在
|
|
150
|
+
if (!fs.existsSync(tempDir)) {
|
|
151
|
+
await fs.promises.mkdir(tempDir, { recursive: true });
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 写入临时文件
|
|
155
|
+
await fs.promises.writeFile(tempFile, content, 'utf8');
|
|
156
|
+
|
|
157
|
+
// 导入临时文件
|
|
158
|
+
const fileUrl = `file://${tempFile.replace(/\\/g, '/')}`;
|
|
159
|
+
mod = await import(fileUrl);
|
|
160
|
+
|
|
161
|
+
// 清理临时文件
|
|
162
|
+
fs.unlinkSync(tempFile);
|
|
163
|
+
|
|
164
|
+
// 如果临时目录为空,删除它
|
|
165
|
+
try {
|
|
166
|
+
fs.rmdirSync(tempDir);
|
|
167
|
+
} catch (e) {
|
|
168
|
+
// 忽略删除目录失败的错误(可能不为空)
|
|
169
|
+
}
|
|
170
|
+
} catch (tempError) {
|
|
171
|
+
// 如果临时文件方法失败,回退到原来的 base64 方法
|
|
172
|
+
logger.warn(`临时文件方法失败,回退到 base64 方法: ${tempError.message}`);
|
|
173
|
+
|
|
174
|
+
// 清理可能已创建的临时文件
|
|
175
|
+
try {
|
|
176
|
+
if (fs.existsSync(tempFile)) {
|
|
177
|
+
fs.unlinkSync(tempFile);
|
|
178
|
+
}
|
|
179
|
+
} catch (cleanupError) {
|
|
180
|
+
// 忽略清理错误
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const base64 = Buffer.from(content, 'utf8').toString('base64');
|
|
184
|
+
mod = await import(`data:text/javascript;base64,${base64}`);
|
|
185
|
+
}
|
|
186
|
+
} else {
|
|
187
|
+
// CommonJS 规则:使用 createRequire 加载
|
|
188
|
+
const require = createRequire(import.meta.url);
|
|
189
|
+
mod = require(filePath);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
// 兼容多种导出风格
|
|
193
|
+
const candidates = [mod?.rules, mod?.default?.rules, mod?.default, mod];
|
|
194
|
+
for (const c of candidates) {
|
|
195
|
+
if (Array.isArray(c)) return c;
|
|
196
|
+
}
|
|
197
|
+
return [];
|
|
198
|
+
} else if (ext === '.json') {
|
|
199
|
+
const content = await fs.promises.readFile(filePath, 'utf8');
|
|
200
|
+
const config = JSON.parse(content);
|
|
201
|
+
return config.rules || [];
|
|
202
|
+
}
|
|
203
|
+
} catch (error) {
|
|
204
|
+
logger.warn(`加载规则文件失败 ${filePath}:`, error.message);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return [];
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
deepMerge(target, source) {
|
|
211
|
+
const result = { ...target };
|
|
212
|
+
|
|
213
|
+
for (const key in source) {
|
|
214
|
+
if (source[key] && typeof source[key] === 'object' && !Array.isArray(source[key])) {
|
|
215
|
+
result[key] = this.deepMerge(result[key] || {}, source[key]);
|
|
216
|
+
} else {
|
|
217
|
+
result[key] = source[key];
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return result;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
export const defaultConfig = {
|
|
2
|
+
// AI配置
|
|
3
|
+
ai: {
|
|
4
|
+
enabled: true,
|
|
5
|
+
model: 'gpt-4o-mini',
|
|
6
|
+
apiKey: process.env.OPENAI_API_KEY || '',
|
|
7
|
+
baseURL: '',
|
|
8
|
+
temperature: 0.1,
|
|
9
|
+
maxResponseTokens: 4096,
|
|
10
|
+
maxFileSizeKB: 100,
|
|
11
|
+
enabledFor: ['.js', '.ts', '.jsx', '.tsx', '.vue', '.py', '.java', '.cpp', '.c', '.cs', '.php', '.rb', '.go', '.rs', '.swift', '.kt'],
|
|
12
|
+
// 当本地规则存在时,是否将本地规则提示作为上下文提供给AI
|
|
13
|
+
useStaticHints: true,
|
|
14
|
+
// 在存在阻断级别本地问题时是否仍运行AI
|
|
15
|
+
runWhenBlocked: false,
|
|
16
|
+
maxRequestTokens: 8000,
|
|
17
|
+
minFilesPerBatch: 1,
|
|
18
|
+
maxFilesPerBatch: 10,
|
|
19
|
+
tokenRatio: 4,
|
|
20
|
+
chunkOverlapLines: 5,
|
|
21
|
+
includeStaticHints: true,
|
|
22
|
+
// 并发处理配置
|
|
23
|
+
concurrency: 3, // 并发AI请求数量,默认3个,<=1时串行,>1时并发
|
|
24
|
+
// Git Diff 增量审查配置
|
|
25
|
+
reviewOnlyChanges: true, // 是否仅审查暂存区变动内容(git diff),而非全文件
|
|
26
|
+
contextMergeLines: 10 // 上下文合并行长度(大概值),用于在diff审查时提供足够的上下文
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
// 风险等级配置
|
|
30
|
+
riskLevels: {
|
|
31
|
+
critical: { block: true },
|
|
32
|
+
high: { block: false },
|
|
33
|
+
medium: { block: false },
|
|
34
|
+
low: { block: false },
|
|
35
|
+
suggestion: { block: false }
|
|
36
|
+
},
|
|
37
|
+
|
|
38
|
+
// 是否抑制低等级问题的输出(仅输出阻断等级的问题)
|
|
39
|
+
suppressLowLevelOutput: false,
|
|
40
|
+
|
|
41
|
+
// 规则加载策略:true=仅使用外部规则,false=内部和外部规则合并(默认)
|
|
42
|
+
useExternalRulesOnly: false,
|
|
43
|
+
|
|
44
|
+
// 文件处理配置
|
|
45
|
+
fileExtensions: [
|
|
46
|
+
'.js', '.jsx', '.ts', '.tsx', '.vue', '.svelte',
|
|
47
|
+
'.py', '.java', '.go', '.rs', '.cpp', '.c', '.h',
|
|
48
|
+
'.php', '.rb', '.html', '.css', '.scss', '.less'
|
|
49
|
+
],
|
|
50
|
+
|
|
51
|
+
// 统一的忽略文件配置:支持相对路径、绝对路径、glob模式和正则表达式
|
|
52
|
+
// 示例:["./test/src/test-file.js", "test/*", "**/node_modules/**", "/.*\\.generated\\./"]
|
|
53
|
+
ignoreFiles: [
|
|
54
|
+
// 依赖目录
|
|
55
|
+
'**/node_modules/**',
|
|
56
|
+
'**/vendor/**',
|
|
57
|
+
'**/.pnpm/**',
|
|
58
|
+
'**/bower_components/**',
|
|
59
|
+
|
|
60
|
+
// 测试覆盖率和报告
|
|
61
|
+
'**/coverage/**',
|
|
62
|
+
'**/test-results/**',
|
|
63
|
+
'**/reports/**',
|
|
64
|
+
|
|
65
|
+
// 压缩和打包文件
|
|
66
|
+
'**/*.min.js',
|
|
67
|
+
'**/*.min.css',
|
|
68
|
+
'**/*.bundle.js',
|
|
69
|
+
'**/*.chunk.js',
|
|
70
|
+
'**/*.umd.js',
|
|
71
|
+
|
|
72
|
+
// 生成的文件
|
|
73
|
+
'**/*.generated.*',
|
|
74
|
+
'**/*.auto.*',
|
|
75
|
+
'**/generated/**',
|
|
76
|
+
|
|
77
|
+
// 版本控制和临时文件
|
|
78
|
+
'**/.git/**',
|
|
79
|
+
'**/.svn/**',
|
|
80
|
+
'**/.hg/**',
|
|
81
|
+
'**/tmp/**',
|
|
82
|
+
'**/temp/**',
|
|
83
|
+
'**/*.tmp',
|
|
84
|
+
'**/*.temp',
|
|
85
|
+
'**/*.swp',
|
|
86
|
+
'**/*.swo',
|
|
87
|
+
'**/*~',
|
|
88
|
+
|
|
89
|
+
// IDE和编辑器文件
|
|
90
|
+
'**/.vscode/**',
|
|
91
|
+
'**/.idea/**',
|
|
92
|
+
'**/*.iml',
|
|
93
|
+
'**/.project',
|
|
94
|
+
'**/.classpath',
|
|
95
|
+
'**/.settings/**',
|
|
96
|
+
|
|
97
|
+
// 日志文件
|
|
98
|
+
'**/*.log',
|
|
99
|
+
'**/logs/**',
|
|
100
|
+
|
|
101
|
+
// 缓存目录
|
|
102
|
+
'**/.cache/**',
|
|
103
|
+
'**/.next/**',
|
|
104
|
+
'**/.nuxt/**',
|
|
105
|
+
'**/.vuepress/**',
|
|
106
|
+
|
|
107
|
+
// 包管理器锁文件
|
|
108
|
+
'**/package-lock.json',
|
|
109
|
+
'**/yarn.lock',
|
|
110
|
+
'**/pnpm-lock.yaml',
|
|
111
|
+
'**/composer.lock',
|
|
112
|
+
'**/Pipfile.lock',
|
|
113
|
+
|
|
114
|
+
// 环境和配置文件(可能包含敏感信息)
|
|
115
|
+
'**/.env',
|
|
116
|
+
'**/.env.*',
|
|
117
|
+
'**/config/secrets.*'
|
|
118
|
+
],
|
|
119
|
+
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export const defaultRules = {
|
|
123
|
+
security: [
|
|
124
|
+
{
|
|
125
|
+
id: 'SEC001',
|
|
126
|
+
name: '硬编码密码',
|
|
127
|
+
pattern: '(password|pwd|pass)\\s*=\\s*[\'"][^\'"]+[\'"]',
|
|
128
|
+
risk: 'high',
|
|
129
|
+
message: '发现硬编码的密码,建议使用环境变量或安全的配置管理',
|
|
130
|
+
suggestion: '使用环境变量或加密的配置存储',
|
|
131
|
+
flags: 'gi'
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
id: 'SEC002',
|
|
135
|
+
name: 'SQL注入风险',
|
|
136
|
+
pattern: '(execute|query)\\s*\\(\\s*[fF]?[\'"][^\']*\\+.*[\'"]',
|
|
137
|
+
risk: 'critical',
|
|
138
|
+
message: '发现可能的SQL注入风险,字符串拼接SQL查询',
|
|
139
|
+
suggestion: '使用参数化查询或ORM的安全方法',
|
|
140
|
+
flags: 'gi'
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
id: 'SEC003',
|
|
144
|
+
name: 'XSS风险',
|
|
145
|
+
pattern: 'innerHTML\\s*=|document\\.write\\s*\\(',
|
|
146
|
+
risk: 'high',
|
|
147
|
+
message: '发现直接操作HTML内容,可能存在XSS风险',
|
|
148
|
+
suggestion: '使用textContent或安全的DOM操作方法',
|
|
149
|
+
flags: 'gi'
|
|
150
|
+
}
|
|
151
|
+
],
|
|
152
|
+
|
|
153
|
+
performance: [
|
|
154
|
+
{
|
|
155
|
+
id: 'PERF001',
|
|
156
|
+
name: '循环内数据库查询',
|
|
157
|
+
pattern: 'for\\s*\\([^)]*\\)\\s*\\{[^}]*\\.(find|query|select)[^}]*\\}',
|
|
158
|
+
risk: 'medium',
|
|
159
|
+
message: '在循环内执行数据库查询,可能导致N+1查询问题',
|
|
160
|
+
suggestion: '使用批量查询或预加载数据',
|
|
161
|
+
flags: 'gi'
|
|
162
|
+
},
|
|
163
|
+
{
|
|
164
|
+
id: 'PERF002',
|
|
165
|
+
name: '内存泄漏风险',
|
|
166
|
+
pattern: 'setInterval\\s*\\([^)]*\\)|setTimeout\\s*\\([^)]*\\)',
|
|
167
|
+
risk: 'medium',
|
|
168
|
+
message: '发现定时器使用,可能存在内存泄漏风险',
|
|
169
|
+
suggestion: '确保在组件卸载时清理定时器',
|
|
170
|
+
flags: 'gi'
|
|
171
|
+
}
|
|
172
|
+
],
|
|
173
|
+
|
|
174
|
+
'best-practices': [
|
|
175
|
+
{
|
|
176
|
+
id: 'BP001',
|
|
177
|
+
name: '调试代码',
|
|
178
|
+
pattern: 'console\\.log|print\\(|alert\\(',
|
|
179
|
+
risk: 'low',
|
|
180
|
+
message: '发现调试代码,建议在提交前移除',
|
|
181
|
+
suggestion: '使用日志系统替代console.log',
|
|
182
|
+
flags: 'gi'
|
|
183
|
+
},
|
|
184
|
+
{
|
|
185
|
+
id: 'BP002',
|
|
186
|
+
name: '魔法数字',
|
|
187
|
+
pattern: '\\b(?<!\\.)(?!(?:0|1|10|12|24|30|60|100|200|201|300|400|401|403|404|500|503|1000|3000|5000|8080|9000)\\b)\\d{3,}(?!\\.\\d)\\b',
|
|
188
|
+
risk: 'low',
|
|
189
|
+
message: '检测到魔法数字,建议使用常量定义',
|
|
190
|
+
suggestion: '将数字定义为有意义的常量',
|
|
191
|
+
flags: 'g'
|
|
192
|
+
},
|
|
193
|
+
{
|
|
194
|
+
id: 'BP013',
|
|
195
|
+
name: '使用var声明',
|
|
196
|
+
pattern: '\\bvar\\s+\\w+',
|
|
197
|
+
risk: 'medium',
|
|
198
|
+
message: '检测到使用var声明变量,可能导致作用域问题',
|
|
199
|
+
suggestion: '使用let或const替代var,提高代码安全性',
|
|
200
|
+
flags: 'gi'
|
|
201
|
+
}
|
|
202
|
+
]
|
|
203
|
+
};
|