ai-unit-test-generator 1.4.5 → 2.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/CHANGELOG.md +138 -0
- package/README.md +307 -322
- package/bin/cli.js +100 -202
- package/lib/ai/analyzer-prompt.mjs +120 -0
- package/lib/ai/config-writer.mjs +99 -0
- package/lib/ai/context-builder.mjs +52 -0
- package/lib/ai/index.mjs +11 -0
- package/lib/ai/reviewer.mjs +283 -0
- package/lib/ai/sampler.mjs +97 -0
- package/lib/ai/validator.mjs +101 -0
- package/lib/core/scanner.mjs +112 -2
- package/lib/core/scorer.mjs +184 -4
- package/lib/utils/config-manager.mjs +110 -0
- package/lib/utils/index.mjs +2 -0
- package/lib/utils/scan-manager.mjs +121 -0
- package/lib/workflows/analyze.mjs +157 -0
- package/lib/workflows/generate.mjs +98 -0
- package/lib/workflows/index.mjs +7 -0
- package/lib/workflows/init.mjs +45 -0
- package/lib/workflows/scan.mjs +144 -0
- package/package.json +1 -1
- package/templates/default.config.jsonc +58 -0
@@ -0,0 +1,283 @@
|
|
1
|
+
/**
|
2
|
+
* 交互式 AI 建议审核器
|
3
|
+
* 支持分类审核、逐条选择、批量调整分数
|
4
|
+
*/
|
5
|
+
|
6
|
+
import readline from 'readline'
|
7
|
+
|
8
|
+
/**
|
9
|
+
* 创建 readline 接口
|
10
|
+
*/
|
11
|
+
function createInterface() {
|
12
|
+
return readline.createInterface({
|
13
|
+
input: process.stdin,
|
14
|
+
output: process.stdout
|
15
|
+
})
|
16
|
+
}
|
17
|
+
|
18
|
+
/**
|
19
|
+
* 询问用户输入
|
20
|
+
*/
|
21
|
+
function ask(rl, question) {
|
22
|
+
return new Promise(resolve => rl.question(question, resolve))
|
23
|
+
}
|
24
|
+
|
25
|
+
/**
|
26
|
+
* 获取分类图标
|
27
|
+
*/
|
28
|
+
function getCategoryIcon(category) {
|
29
|
+
const icons = {
|
30
|
+
businessCriticalPaths: '🔴',
|
31
|
+
highRiskModules: '⚠️',
|
32
|
+
testabilityAdjustments: '✅'
|
33
|
+
}
|
34
|
+
return icons[category] || '📝'
|
35
|
+
}
|
36
|
+
|
37
|
+
/**
|
38
|
+
* 获取分类名称
|
39
|
+
*/
|
40
|
+
function getCategoryName(category) {
|
41
|
+
const names = {
|
42
|
+
businessCriticalPaths: 'Business Critical Paths',
|
43
|
+
highRiskModules: 'High Risk Modules',
|
44
|
+
testabilityAdjustments: 'Testability Adjustments'
|
45
|
+
}
|
46
|
+
return names[category] || category
|
47
|
+
}
|
48
|
+
|
49
|
+
/**
|
50
|
+
* 获取分类名称(小写)
|
51
|
+
*/
|
52
|
+
function getCategoryNameLower(category) {
|
53
|
+
const names = {
|
54
|
+
businessCriticalPaths: 'business critical paths',
|
55
|
+
highRiskModules: 'high risk modules',
|
56
|
+
testabilityAdjustments: 'testability adjustments'
|
57
|
+
}
|
58
|
+
return names[category] || category
|
59
|
+
}
|
60
|
+
|
61
|
+
/**
|
62
|
+
* 格式化单个建议
|
63
|
+
*/
|
64
|
+
function formatSuggestion(item, index, category) {
|
65
|
+
let output = `\n [${index}] ${item.pattern}\n`
|
66
|
+
|
67
|
+
if (category === 'businessCriticalPaths') {
|
68
|
+
output += ` BC: ${item.suggestedBC} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
|
69
|
+
} else if (category === 'highRiskModules') {
|
70
|
+
output += ` ER: ${item.suggestedER} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
|
71
|
+
} else if (category === 'testabilityAdjustments') {
|
72
|
+
output += ` Adjustment: ${item.adjustment} | Confidence: ${(item.confidence * 100).toFixed(0)}%\n`
|
73
|
+
}
|
74
|
+
|
75
|
+
output += ` Reason: ${item.reason}\n`
|
76
|
+
output += ` Evidence:\n`
|
77
|
+
item.evidence.forEach(e => {
|
78
|
+
output += ` - ${e}\n`
|
79
|
+
})
|
80
|
+
|
81
|
+
return output
|
82
|
+
}
|
83
|
+
|
84
|
+
/**
|
85
|
+
* 显示建议列表
|
86
|
+
*/
|
87
|
+
function displaySuggestions(category, items) {
|
88
|
+
const icon = getCategoryIcon(category)
|
89
|
+
const name = getCategoryName(category)
|
90
|
+
|
91
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
|
92
|
+
console.log(`${icon} ${name} (${items.length} suggestions):`)
|
93
|
+
|
94
|
+
items.forEach((item, i) => {
|
95
|
+
console.log(formatSuggestion(item, i + 1, category))
|
96
|
+
})
|
97
|
+
|
98
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
|
99
|
+
}
|
100
|
+
|
101
|
+
/**
|
102
|
+
* 解析用户选择
|
103
|
+
*/
|
104
|
+
function parseSelection(input, maxCount) {
|
105
|
+
if (!input || input.trim() === '') {
|
106
|
+
return []
|
107
|
+
}
|
108
|
+
|
109
|
+
return input.split(',')
|
110
|
+
.map(s => parseInt(s.trim()))
|
111
|
+
.filter(n => !isNaN(n) && n >= 1 && n <= maxCount)
|
112
|
+
}
|
113
|
+
|
114
|
+
/**
|
115
|
+
* 批量调整分数
|
116
|
+
*/
|
117
|
+
async function adjustScores(rl, items, category) {
|
118
|
+
console.log(`\nAdjust scores (press Enter to keep original):`)
|
119
|
+
|
120
|
+
const scoreField = category === 'businessCriticalPaths' ? 'suggestedBC' : 'suggestedER'
|
121
|
+
const minScore = category === 'businessCriticalPaths' ? 8 : 7
|
122
|
+
const maxScore = 10
|
123
|
+
|
124
|
+
const adjusted = []
|
125
|
+
|
126
|
+
for (let i = 0; i < items.length; i++) {
|
127
|
+
const item = { ...items[i] }
|
128
|
+
const currentScore = item[scoreField]
|
129
|
+
|
130
|
+
const input = await ask(rl, ` [${i + 1}] ${item.pattern} (${scoreField}: ${currentScore}): `)
|
131
|
+
|
132
|
+
if (input && input.trim() !== '') {
|
133
|
+
const newScore = parseInt(input.trim())
|
134
|
+
if (!isNaN(newScore) && newScore >= minScore && newScore <= maxScore) {
|
135
|
+
item[scoreField] = newScore
|
136
|
+
console.log(` ✅ Updated: ${currentScore} → ${newScore}`)
|
137
|
+
} else {
|
138
|
+
console.log(` ⚠️ Invalid score (must be ${minScore}-${maxScore}), keeping original`)
|
139
|
+
}
|
140
|
+
}
|
141
|
+
|
142
|
+
adjusted.push(item)
|
143
|
+
}
|
144
|
+
|
145
|
+
return adjusted
|
146
|
+
}
|
147
|
+
|
148
|
+
/**
|
149
|
+
* 审核单个分类
|
150
|
+
*/
|
151
|
+
async function reviewCategory(rl, category, items) {
|
152
|
+
if (items.length === 0) {
|
153
|
+
return []
|
154
|
+
}
|
155
|
+
|
156
|
+
// 显示建议
|
157
|
+
displaySuggestions(category, items)
|
158
|
+
|
159
|
+
// 询问操作
|
160
|
+
const categoryName = getCategoryNameLower(category)
|
161
|
+
const action = await ask(rl,
|
162
|
+
`❓ Review ${categoryName}:\n` +
|
163
|
+
` [a] Accept all (${items.length})\n` +
|
164
|
+
` [r] Reject all\n` +
|
165
|
+
` [s] Select individually\n` +
|
166
|
+
` [n] Skip (keep empty)\n` +
|
167
|
+
`> `
|
168
|
+
)
|
169
|
+
|
170
|
+
const actionLower = action.trim().toLowerCase()
|
171
|
+
|
172
|
+
if (actionLower === 'a') {
|
173
|
+
// 接受全部
|
174
|
+
console.log(`✅ Accepted: ${items.length}/${items.length} ${categoryName}`)
|
175
|
+
|
176
|
+
// 询问是否调整分数
|
177
|
+
if (category === 'businessCriticalPaths' || category === 'highRiskModules') {
|
178
|
+
const adjustInput = await ask(rl, `\nAdjust scores? (y/n) `)
|
179
|
+
if (adjustInput.trim().toLowerCase() === 'y') {
|
180
|
+
return await adjustScores(rl, items, category)
|
181
|
+
}
|
182
|
+
}
|
183
|
+
|
184
|
+
return items
|
185
|
+
} else if (actionLower === 's') {
|
186
|
+
// 逐条选择
|
187
|
+
const selection = await ask(rl, `\nSelect which suggestions to accept (comma-separated, e.g. 1,3):\n> `)
|
188
|
+
const indices = parseSelection(selection, items.length)
|
189
|
+
|
190
|
+
if (indices.length === 0) {
|
191
|
+
console.log(`❌ No suggestions selected`)
|
192
|
+
return []
|
193
|
+
}
|
194
|
+
|
195
|
+
const selected = indices.map(i => items[i - 1])
|
196
|
+
console.log(`✅ Selected: ${selected.length}/${items.length} ${categoryName}`)
|
197
|
+
|
198
|
+
// 询问是否调整分数
|
199
|
+
if (category === 'businessCriticalPaths' || category === 'highRiskModules') {
|
200
|
+
const adjustInput = await ask(rl, `\nAdjust scores? (y/n) `)
|
201
|
+
if (adjustInput.trim().toLowerCase() === 'y') {
|
202
|
+
return await adjustScores(rl, selected, category)
|
203
|
+
}
|
204
|
+
}
|
205
|
+
|
206
|
+
return selected
|
207
|
+
} else if (actionLower === 'r') {
|
208
|
+
// 拒绝全部
|
209
|
+
console.log(`❌ Rejected: all ${categoryName}`)
|
210
|
+
return []
|
211
|
+
} else {
|
212
|
+
// 跳过
|
213
|
+
console.log(`⏭️ Skipped ${categoryName}`)
|
214
|
+
return []
|
215
|
+
}
|
216
|
+
}
|
217
|
+
|
218
|
+
/**
|
219
|
+
* 显示总结
|
220
|
+
*/
|
221
|
+
function displaySummary(result, validated) {
|
222
|
+
const totalSuggested = Object.values(validated).reduce((sum, arr) => sum + arr.length, 0)
|
223
|
+
const totalAccepted = Object.values(result).reduce((sum, arr) => sum + arr.length, 0)
|
224
|
+
|
225
|
+
console.log(`\n━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`)
|
226
|
+
console.log(`📊 Summary:`)
|
227
|
+
console.log(` Business Critical Paths: ${result.businessCriticalPaths.length}/${validated.businessCriticalPaths?.length || 0} accepted`)
|
228
|
+
console.log(` High Risk Modules: ${result.highRiskModules.length}/${validated.highRiskModules?.length || 0} accepted`)
|
229
|
+
console.log(` Testability Adjustments: ${result.testabilityAdjustments.length}/${validated.testabilityAdjustments?.length || 0} accepted`)
|
230
|
+
console.log(` Total: ${totalAccepted}/${totalSuggested} suggestions accepted`)
|
231
|
+
|
232
|
+
if (totalAccepted === 0) {
|
233
|
+
console.log(`\n⚠️ No suggestions accepted. Config will not be modified.`)
|
234
|
+
}
|
235
|
+
}
|
236
|
+
|
237
|
+
/**
|
238
|
+
* 交互式审核
|
239
|
+
* @param {Object} validated - 已验证的建议
|
240
|
+
* @returns {Object|null} - 用户批准的建议,或 null(取消)
|
241
|
+
*/
|
242
|
+
export async function interactiveReview(validated) {
|
243
|
+
const rl = createInterface()
|
244
|
+
|
245
|
+
const result = {
|
246
|
+
businessCriticalPaths: [],
|
247
|
+
highRiskModules: [],
|
248
|
+
testabilityAdjustments: []
|
249
|
+
}
|
250
|
+
|
251
|
+
try {
|
252
|
+
// 审核每个分类
|
253
|
+
const categories = ['businessCriticalPaths', 'highRiskModules', 'testabilityAdjustments']
|
254
|
+
|
255
|
+
for (const category of categories) {
|
256
|
+
const items = validated[category] || []
|
257
|
+
if (items.length === 0) continue
|
258
|
+
|
259
|
+
result[category] = await reviewCategory(rl, category, items)
|
260
|
+
}
|
261
|
+
|
262
|
+
// 显示总结
|
263
|
+
displaySummary(result, validated)
|
264
|
+
|
265
|
+
const totalAccepted = Object.values(result).reduce((sum, arr) => sum + arr.length, 0)
|
266
|
+
|
267
|
+
if (totalAccepted === 0) {
|
268
|
+
rl.close()
|
269
|
+
return null
|
270
|
+
}
|
271
|
+
|
272
|
+
// 最终确认
|
273
|
+
const save = await ask(rl, `\n💾 Save these changes to ai-test.config.jsonc? (y/n)\n> `)
|
274
|
+
|
275
|
+
rl.close()
|
276
|
+
|
277
|
+
return save.trim().toLowerCase() === 'y' ? result : null
|
278
|
+
} catch (err) {
|
279
|
+
rl.close()
|
280
|
+
throw err
|
281
|
+
}
|
282
|
+
}
|
283
|
+
|
@@ -0,0 +1,97 @@
|
|
1
|
+
/**
|
2
|
+
* 代码文件采样工具
|
3
|
+
* 智能选择代表性代码文件供 AI 分析
|
4
|
+
*/
|
5
|
+
|
6
|
+
import { readFileSync, statSync } from 'node:fs'
|
7
|
+
import fg from 'fast-glob'
|
8
|
+
|
9
|
+
/**
|
10
|
+
* 智能采样代码文件
|
11
|
+
* @returns {Array} 采样结果
|
12
|
+
*/
|
13
|
+
export async function sampleCodeFiles() {
|
14
|
+
const samples = []
|
15
|
+
|
16
|
+
// 策略 1: 按目录分层采样(确保覆盖各层)
|
17
|
+
const layers = {
|
18
|
+
services: await fg('src/services/**/*.{ts,tsx}'),
|
19
|
+
components: await fg('src/components/**/*.{ts,tsx}'),
|
20
|
+
utils: await fg('src/utils/**/*.{ts,tsx}'),
|
21
|
+
atoms: await fg('src/atoms/**/*.{ts,tsx}'),
|
22
|
+
stores: await fg('src/stores/**/*.{ts,tsx}'),
|
23
|
+
hooks: await fg('src/hooks/**/*.{ts,tsx}'),
|
24
|
+
context: await fg('src/context/**/*.{ts,tsx}')
|
25
|
+
}
|
26
|
+
|
27
|
+
for (const [layer, files] of Object.entries(layers)) {
|
28
|
+
if (files.length === 0) continue
|
29
|
+
|
30
|
+
// 每层选 2-3 个文件
|
31
|
+
const selected = files
|
32
|
+
.map(f => ({
|
33
|
+
path: f,
|
34
|
+
size: statSync(f).size,
|
35
|
+
name: f.split('/').pop()
|
36
|
+
}))
|
37
|
+
.sort((a, b) => b.size - a.size) // 按文件大小排序
|
38
|
+
.slice(0, 3)
|
39
|
+
|
40
|
+
for (const file of selected) {
|
41
|
+
samples.push({
|
42
|
+
path: file.path,
|
43
|
+
layer,
|
44
|
+
reason: `${layer}_representative`,
|
45
|
+
preview: readFileSync(file.path, 'utf-8').slice(0, 1500) // 前1500字符
|
46
|
+
})
|
47
|
+
}
|
48
|
+
}
|
49
|
+
|
50
|
+
// 策略 2: 关键词匹配(业务关键代码)
|
51
|
+
const keywords = ['payment', 'order', 'booking', 'checkout', 'price', 'cart', 'hotel', 'room']
|
52
|
+
const criticalFiles = await fg('src/**/*.{ts,tsx}')
|
53
|
+
|
54
|
+
for (const keyword of keywords) {
|
55
|
+
const matched = criticalFiles.filter(f => f.toLowerCase().includes(keyword))
|
56
|
+
if (matched.length > 0) {
|
57
|
+
const file = matched[0] // 取第一个
|
58
|
+
if (!samples.find(s => s.path === file)) {
|
59
|
+
samples.push({
|
60
|
+
path: file,
|
61
|
+
layer: 'business',
|
62
|
+
reason: `critical_keyword_${keyword}`,
|
63
|
+
preview: readFileSync(file, 'utf-8').slice(0, 1500)
|
64
|
+
})
|
65
|
+
}
|
66
|
+
}
|
67
|
+
}
|
68
|
+
|
69
|
+
// 去重
|
70
|
+
const uniqueSamples = samples.filter((s, i, arr) =>
|
71
|
+
arr.findIndex(x => x.path === s.path) === i
|
72
|
+
)
|
73
|
+
|
74
|
+
return uniqueSamples.slice(0, 25) // 最多 25 个样本
|
75
|
+
}
|
76
|
+
|
77
|
+
/**
|
78
|
+
* 分析项目结构
|
79
|
+
*/
|
80
|
+
export async function analyzeProjectStructure() {
|
81
|
+
const allFiles = await fg('src/**/*.{ts,tsx,js,jsx}')
|
82
|
+
|
83
|
+
let totalLines = 0
|
84
|
+
for (const file of allFiles) {
|
85
|
+
try {
|
86
|
+
const content = readFileSync(file, 'utf-8')
|
87
|
+
totalLines += content.split('\n').length
|
88
|
+
} catch {}
|
89
|
+
}
|
90
|
+
|
91
|
+
return {
|
92
|
+
totalFiles: allFiles.length,
|
93
|
+
totalLines,
|
94
|
+
avgLinesPerFile: Math.round(totalLines / allFiles.length)
|
95
|
+
}
|
96
|
+
}
|
97
|
+
|
@@ -0,0 +1,101 @@
|
|
1
|
+
/**
|
2
|
+
* AI 响应验证器
|
3
|
+
*/
|
4
|
+
|
5
|
+
const SCHEMA = {
|
6
|
+
businessCriticalPaths: {
|
7
|
+
minConfidence: 0.85,
|
8
|
+
maxCount: 10,
|
9
|
+
requiredFields: ['pattern', 'confidence', 'reason', 'suggestedBC', 'evidence'],
|
10
|
+
validators: {
|
11
|
+
pattern: (v) => /^[a-z0-9_/-]+\/?\*?\*?$/.test(v),
|
12
|
+
confidence: (v) => v >= 0.85 && v <= 1.0,
|
13
|
+
suggestedBC: (v) => [8, 9, 10].includes(v),
|
14
|
+
reason: (v) => v.length > 0 && v.length <= 200,
|
15
|
+
evidence: (v) => Array.isArray(v) && v.length >= 2 && v.length <= 3
|
16
|
+
}
|
17
|
+
},
|
18
|
+
highRiskModules: {
|
19
|
+
minConfidence: 0.75,
|
20
|
+
maxCount: 10,
|
21
|
+
requiredFields: ['pattern', 'confidence', 'reason', 'suggestedER', 'evidence'],
|
22
|
+
validators: {
|
23
|
+
pattern: (v) => /^[a-z0-9_/-]+\/?\*?\*?$/.test(v),
|
24
|
+
confidence: (v) => v >= 0.75 && v <= 1.0,
|
25
|
+
suggestedER: (v) => [7, 8, 9, 10].includes(v),
|
26
|
+
reason: (v) => v.length > 0 && v.length <= 200,
|
27
|
+
evidence: (v) => Array.isArray(v) && v.length >= 2 && v.length <= 3
|
28
|
+
}
|
29
|
+
},
|
30
|
+
testabilityAdjustments: {
|
31
|
+
minConfidence: 0.80,
|
32
|
+
maxCount: 10,
|
33
|
+
requiredFields: ['pattern', 'confidence', 'reason', 'adjustment', 'evidence'],
|
34
|
+
validators: {
|
35
|
+
pattern: (v) => /^[a-z0-9_/-]+\/?\*?\*?$/.test(v),
|
36
|
+
confidence: (v) => v >= 0.80 && v <= 1.0,
|
37
|
+
adjustment: (v) => ['-2', '-1', '+1', '+2'].includes(v),
|
38
|
+
reason: (v) => v.length > 0 && v.length <= 200,
|
39
|
+
evidence: (v) => Array.isArray(v) && v.length >= 2 && v.length <= 3
|
40
|
+
}
|
41
|
+
}
|
42
|
+
}
|
43
|
+
|
44
|
+
/**
|
45
|
+
* 验证单个建议
|
46
|
+
*/
|
47
|
+
function validateSuggestion(item, schema) {
|
48
|
+
// 检查必需字段
|
49
|
+
for (const field of schema.requiredFields) {
|
50
|
+
if (!(field in item)) {
|
51
|
+
return false
|
52
|
+
}
|
53
|
+
}
|
54
|
+
|
55
|
+
// 检查置信度
|
56
|
+
if (item.confidence < schema.minConfidence) {
|
57
|
+
return false
|
58
|
+
}
|
59
|
+
|
60
|
+
// 运行字段验证器
|
61
|
+
for (const [field, validator] of Object.entries(schema.validators)) {
|
62
|
+
if (field in item && !validator(item[field])) {
|
63
|
+
return false
|
64
|
+
}
|
65
|
+
}
|
66
|
+
|
67
|
+
return true
|
68
|
+
}
|
69
|
+
|
70
|
+
/**
|
71
|
+
* 验证并清洗 AI 响应
|
72
|
+
*/
|
73
|
+
export function validateAndSanitize(parsed) {
|
74
|
+
const result = {
|
75
|
+
businessCriticalPaths: [],
|
76
|
+
highRiskModules: [],
|
77
|
+
testabilityAdjustments: []
|
78
|
+
}
|
79
|
+
|
80
|
+
if (!parsed || typeof parsed !== 'object' || !parsed.suggestions) {
|
81
|
+
console.warn('⚠️ Invalid AI response structure')
|
82
|
+
return result
|
83
|
+
}
|
84
|
+
|
85
|
+
const suggestions = parsed.suggestions
|
86
|
+
|
87
|
+
for (const [key, items] of Object.entries(suggestions)) {
|
88
|
+
const schema = SCHEMA[key]
|
89
|
+
if (!schema || !Array.isArray(items)) continue
|
90
|
+
|
91
|
+
const validated = items
|
92
|
+
.filter(item => validateSuggestion(item, schema))
|
93
|
+
.sort((a, b) => b.confidence - a.confidence)
|
94
|
+
.slice(0, schema.maxCount)
|
95
|
+
|
96
|
+
result[key] = validated
|
97
|
+
}
|
98
|
+
|
99
|
+
return result
|
100
|
+
}
|
101
|
+
|
package/lib/core/scanner.mjs
CHANGED
@@ -140,6 +140,111 @@ async function extractTargets(files) {
|
|
140
140
|
return false
|
141
141
|
}
|
142
142
|
|
143
|
+
// ✅ 文件级缓存:避免重复扫描每个文件的导入
|
144
|
+
const fileImportsCache = new Map()
|
145
|
+
|
146
|
+
function getCachedFileImports(sf) {
|
147
|
+
const filePath = sf.getFilePath()
|
148
|
+
if (fileImportsCache.has(filePath)) {
|
149
|
+
return fileImportsCache.get(filePath)
|
150
|
+
}
|
151
|
+
|
152
|
+
const imports = sf.getImportDeclarations()
|
153
|
+
const criticalKeywords = ['stripe', 'payment', 'auth', 'axios', 'fetch', 'prisma', 'db', 'api', 'jotai', 'zustand']
|
154
|
+
const criticalImports = imports
|
155
|
+
.map(imp => imp.getModuleSpecifierValue())
|
156
|
+
.filter(mod => criticalKeywords.some(kw => mod.toLowerCase().includes(kw)))
|
157
|
+
.slice(0, 5) // 限制数量
|
158
|
+
|
159
|
+
fileImportsCache.set(filePath, criticalImports)
|
160
|
+
return criticalImports
|
161
|
+
}
|
162
|
+
|
163
|
+
// ✅ 新增:提取函数的 AI 分析元数据
|
164
|
+
function extractMetadata(node, sf) {
|
165
|
+
const metadata = {
|
166
|
+
criticalImports: [],
|
167
|
+
businessEntities: [],
|
168
|
+
hasDocumentation: false,
|
169
|
+
documentation: '',
|
170
|
+
errorHandling: 0,
|
171
|
+
externalCalls: 0,
|
172
|
+
paramCount: 0,
|
173
|
+
returnType: ''
|
174
|
+
}
|
175
|
+
|
176
|
+
if (!node) return metadata
|
177
|
+
|
178
|
+
try {
|
179
|
+
// 1. 提取关键导入(使用缓存)
|
180
|
+
metadata.criticalImports = getCachedFileImports(sf)
|
181
|
+
|
182
|
+
// 2. 提取参数类型(识别业务实体)
|
183
|
+
if (node.getParameters) {
|
184
|
+
const params = node.getParameters()
|
185
|
+
metadata.paramCount = params.length
|
186
|
+
|
187
|
+
// ✅ 从配置读取业务实体关键词(支持项目定制)
|
188
|
+
const entityKeywords = cfg?.aiEnhancement?.entityKeywords ||
|
189
|
+
['Payment', 'Order', 'Booking', 'User', 'Hotel', 'Room', 'Cart', 'Price', 'Guest', 'Request', 'Response']
|
190
|
+
|
191
|
+
metadata.businessEntities = params
|
192
|
+
.map(p => {
|
193
|
+
try {
|
194
|
+
return p.getType().getText()
|
195
|
+
} catch {
|
196
|
+
return ''
|
197
|
+
}
|
198
|
+
})
|
199
|
+
.filter(type => entityKeywords.some(kw => type.includes(kw)))
|
200
|
+
.slice(0, 3) // 限制数量
|
201
|
+
}
|
202
|
+
|
203
|
+
// 3. 提取返回类型
|
204
|
+
if (node.getReturnType) {
|
205
|
+
try {
|
206
|
+
const returnType = node.getReturnType().getText()
|
207
|
+
// 简化类型(避免过长)
|
208
|
+
metadata.returnType = returnType.length > 100 ? returnType.slice(0, 100) + '...' : returnType
|
209
|
+
} catch {}
|
210
|
+
}
|
211
|
+
|
212
|
+
// 4. 提取 JSDoc 注释
|
213
|
+
if (node.getJsDocs) {
|
214
|
+
const jsDocs = node.getJsDocs()
|
215
|
+
if (jsDocs.length > 0) {
|
216
|
+
metadata.hasDocumentation = true
|
217
|
+
metadata.documentation = jsDocs
|
218
|
+
.map(doc => {
|
219
|
+
const comment = doc.getComment()
|
220
|
+
return typeof comment === 'string' ? comment : ''
|
221
|
+
})
|
222
|
+
.filter(Boolean)
|
223
|
+
.join(' ')
|
224
|
+
.slice(0, 200) // 限制长度
|
225
|
+
}
|
226
|
+
}
|
227
|
+
|
228
|
+
// 5. 统计异常处理(try-catch)
|
229
|
+
if (node.getDescendantsOfKind) {
|
230
|
+
metadata.errorHandling = node.getDescendantsOfKind(SyntaxKind.TryStatement).length
|
231
|
+
}
|
232
|
+
|
233
|
+
// 6. 统计外部 API 调用
|
234
|
+
if (node.getDescendantsOfKind) {
|
235
|
+
const callExpressions = node.getDescendantsOfKind(SyntaxKind.CallExpression)
|
236
|
+
metadata.externalCalls = callExpressions.filter(call => {
|
237
|
+
const expr = call.getExpression().getText()
|
238
|
+
return /fetch|axios|\.get\(|\.post\(|\.put\(|\.delete\(/.test(expr)
|
239
|
+
}).length
|
240
|
+
}
|
241
|
+
} catch (err) {
|
242
|
+
// 提取失败不影响主流程
|
243
|
+
}
|
244
|
+
|
245
|
+
return metadata
|
246
|
+
}
|
247
|
+
|
143
248
|
for (const sf of project.getSourceFiles()) {
|
144
249
|
const absPath = sf.getFilePath()
|
145
250
|
const relPath = relativize(absPath)
|
@@ -154,6 +259,7 @@ async function extractTargets(files) {
|
|
154
259
|
if (fn) {
|
155
260
|
const type = decideTypeByPathAndName(relPath, name)
|
156
261
|
const layer = decideLayer(relPath, cfg)
|
262
|
+
const metadata = extractMetadata(fn, sf) // ✅ 提取元数据
|
157
263
|
targets.push({
|
158
264
|
name,
|
159
265
|
path: relPath,
|
@@ -162,7 +268,8 @@ async function extractTargets(files) {
|
|
162
268
|
internal: false,
|
163
269
|
loc: fileLoc,
|
164
270
|
impactHint: buildImpactHint(relPath),
|
165
|
-
roiHint: buildRoiHint(relPath, content)
|
271
|
+
roiHint: buildRoiHint(relPath, content),
|
272
|
+
metadata // ✅ 添加元数据
|
166
273
|
})
|
167
274
|
continue
|
168
275
|
}
|
@@ -172,6 +279,8 @@ async function extractTargets(files) {
|
|
172
279
|
if (v && isTestableVariable(v)) {
|
173
280
|
const type = decideTypeByPathAndName(relPath, name)
|
174
281
|
const layer = decideLayer(relPath, cfg)
|
282
|
+
const init = v.getInitializer()
|
283
|
+
const metadata = extractMetadata(init, sf) // ✅ 提取元数据
|
175
284
|
targets.push({
|
176
285
|
name,
|
177
286
|
path: relPath,
|
@@ -180,7 +289,8 @@ async function extractTargets(files) {
|
|
180
289
|
internal: false,
|
181
290
|
loc: fileLoc,
|
182
291
|
impactHint: buildImpactHint(relPath),
|
183
|
-
roiHint: buildRoiHint(relPath, content)
|
292
|
+
roiHint: buildRoiHint(relPath, content),
|
293
|
+
metadata // ✅ 添加元数据
|
184
294
|
})
|
185
295
|
}
|
186
296
|
// 其他(interface/type/const/enum)直接跳过
|