smart-review 1.0.1 → 1.0.3
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.en-US.md +580 -0
- package/README.md +93 -54
- package/bin/install.js +419 -280
- package/bin/review.js +42 -47
- package/index.js +0 -1
- package/lib/ai-client-pool.js +63 -25
- package/lib/ai-client.js +262 -415
- package/lib/config-loader.js +35 -7
- package/lib/default-config.js +42 -32
- package/lib/reviewer.js +289 -97
- package/lib/segmented-analyzer.js +102 -126
- package/lib/utils/git-diff-parser.js +9 -8
- package/lib/utils/i18n.js +980 -0
- package/package.json +2 -10
- package/templates/rules/en-US/best-practices.js +123 -0
- package/templates/rules/en-US/performance.js +136 -0
- package/templates/rules/en-US/security.js +345 -0
- package/templates/rules/zh-CN/best-practices.js +123 -0
- package/templates/rules/zh-CN/performance.js +136 -0
- package/templates/rules/zh-CN/security.js +345 -0
- package/templates/smart-review.json +5 -2
package/lib/ai-client.js
CHANGED
|
@@ -4,6 +4,7 @@ import path from 'path';
|
|
|
4
4
|
import { prepareForAIWithLineMap } from './utils/strip.js';
|
|
5
5
|
import { logger } from './utils/logger.js';
|
|
6
6
|
import { AI_CONSTANTS, HTTP_STATUS } from './utils/constants.js';
|
|
7
|
+
import { buildPrompts, t, getLocale, FIELD_LABELS, displayRisk } from './utils/i18n.js';
|
|
7
8
|
|
|
8
9
|
export class AIClient {
|
|
9
10
|
static nodeVersionWarned = false; // 静态变量,确保只警告一次
|
|
@@ -28,13 +29,13 @@ export class AIClient {
|
|
|
28
29
|
const { apiKey, baseURL } = this.config;
|
|
29
30
|
|
|
30
31
|
if (!apiKey) {
|
|
31
|
-
throw new Error(
|
|
32
|
+
throw new Error(t(this.config, 'no_api_key'));
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
// 环境检测:OpenAI SDK 推荐 Node >=18(内置 fetch)。
|
|
35
36
|
const nodeMajor = Number(process.versions.node.split('.')[0]);
|
|
36
37
|
if ((nodeMajor < 18 || typeof fetch === 'undefined') && !AIClient.nodeVersionWarned) {
|
|
37
|
-
logger.warn(
|
|
38
|
+
logger.warn(t(this.config, 'node_version_warn'));
|
|
38
39
|
AIClient.nodeVersionWarned = true;
|
|
39
40
|
}
|
|
40
41
|
|
|
@@ -51,7 +52,10 @@ export class AIClient {
|
|
|
51
52
|
// 如果是大文件分段批次,改走分段整体分析路径,确保行号为绝对源行号
|
|
52
53
|
if (originalBatch?.isLargeFileSegment) {
|
|
53
54
|
try {
|
|
54
|
-
logger.debug(
|
|
55
|
+
logger.debug(t(this.config, 'detected_segmented_batch_dbg', {
|
|
56
|
+
path: originalBatch.segmentedFile,
|
|
57
|
+
segments: originalBatch.totalSegments
|
|
58
|
+
}));
|
|
55
59
|
} catch (e) {}
|
|
56
60
|
const result = await this.handleSegmentBatch(originalBatch);
|
|
57
61
|
return {
|
|
@@ -69,16 +73,16 @@ export class AIClient {
|
|
|
69
73
|
const cfg = this.config?.ai || {};
|
|
70
74
|
const includeStaticHints = cfg.includeStaticHints === true;
|
|
71
75
|
const customPrompts = await this.readCustomPrompts();
|
|
76
|
+
const loc = getLocale(this.config);
|
|
77
|
+
const L = FIELD_LABELS[loc];
|
|
78
|
+
const batchIntro = t(this.config, 'batch_intro');
|
|
72
79
|
const messages = [
|
|
73
80
|
{ role: 'system', content: this.getSystemPrompt() },
|
|
74
|
-
{
|
|
75
|
-
role: 'user',
|
|
76
|
-
content: `我会发送一个批次的文件进行代码审查。其中可能包含分段文件(大文件被分成多段)。对于分段文件,请在收到所有段后进行整体分析。每个问题用空行分隔,务必包含"文件路径:绝对路径"与代码片段,且禁止任何"第X行/第X-Y行"等行号或行范围描述。`
|
|
77
|
-
}
|
|
81
|
+
{ role: 'user', content: batchIntro }
|
|
78
82
|
];
|
|
79
83
|
|
|
80
84
|
if (customPrompts.length > 0) {
|
|
81
|
-
messages.push({ role: 'user', content: `\n[
|
|
85
|
+
messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
|
|
82
86
|
}
|
|
83
87
|
// 处理每个文件
|
|
84
88
|
const requestPreviews = [];
|
|
@@ -89,7 +93,7 @@ export class AIClient {
|
|
|
89
93
|
const contentForAI = attachLineNumbers ? this.addLineNumberPrefixes(clean, lineMap) : clean;
|
|
90
94
|
messages.push({
|
|
91
95
|
role: 'user',
|
|
92
|
-
content:
|
|
96
|
+
content: `${L.file}${file.filePath}\n${L.content}\n\`\`\`\n${contentForAI}\n\`\`\``
|
|
93
97
|
});
|
|
94
98
|
requestPreviews.push({ filePath: file.filePath, contentForAI });
|
|
95
99
|
}
|
|
@@ -99,8 +103,19 @@ export class AIClient {
|
|
|
99
103
|
for (const file of batchData.files) {
|
|
100
104
|
const staticIssues = file.staticIssues || [];
|
|
101
105
|
if (staticIssues.length === 0) continue;
|
|
102
|
-
const lines = staticIssues.map((i, idx) =>
|
|
103
|
-
|
|
106
|
+
const lines = staticIssues.map((i, idx) => {
|
|
107
|
+
const riskDisp = displayRisk(i.risk || 'suggestion', this.config);
|
|
108
|
+
const suggestPart = i.suggestion ? t(this.config, 'inline_suggestion', { suggestion: i.suggestion }) : '';
|
|
109
|
+
return t(this.config, 'local_rule_hint_line', {
|
|
110
|
+
index: idx + 1,
|
|
111
|
+
risk: riskDisp,
|
|
112
|
+
message: i.message,
|
|
113
|
+
suggest: suggestPart,
|
|
114
|
+
snippet: i.snippet || ''
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
const header = t(this.config, 'local_rule_findings_header', { file: file.filePath });
|
|
118
|
+
hintsParts.push(`${header}\n${lines.join('\n')}`);
|
|
104
119
|
}
|
|
105
120
|
if (hintsParts.length > 0) {
|
|
106
121
|
messages.push({ role: 'user', content: hintsParts.join('\n\n') });
|
|
@@ -108,10 +123,10 @@ export class AIClient {
|
|
|
108
123
|
}
|
|
109
124
|
|
|
110
125
|
// 添加最终指令,确保包含片段并保留行号前缀
|
|
111
|
-
const finalInstructionBatch =
|
|
126
|
+
const finalInstructionBatch = t(this.config, 'final_instruction_batch');
|
|
112
127
|
messages.push({ role: 'user', content: finalInstructionBatch });
|
|
113
128
|
// 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
|
|
114
|
-
const finalIgnoreRule =
|
|
129
|
+
const finalIgnoreRule = t(this.config, 'ignore_rule');
|
|
115
130
|
messages.push({ role: 'user', content: finalIgnoreRule });
|
|
116
131
|
|
|
117
132
|
// 使用分段响应处理(携带可读的请求ID,便于日志关联)
|
|
@@ -131,10 +146,10 @@ export class AIClient {
|
|
|
131
146
|
}
|
|
132
147
|
};
|
|
133
148
|
} catch (error) {
|
|
134
|
-
logger.error(
|
|
149
|
+
logger.error(t(this.config, 'ai_batch_failed', { error: error.message }));
|
|
135
150
|
// 如果是AI请求失败,应该终止程序而不是继续处理
|
|
136
151
|
if (error.message.includes('Connection error') || error.message.includes('API') || error.message.includes('请求失败')) {
|
|
137
|
-
logger.error('
|
|
152
|
+
logger.error(t(this.config, 'ai_connection_failed'));
|
|
138
153
|
process.exit(1);
|
|
139
154
|
}
|
|
140
155
|
throw error; // 重新抛出错误,让上层调用者处理
|
|
@@ -190,16 +205,24 @@ export class AIClient {
|
|
|
190
205
|
const cfg = this.config?.ai || this.config || {};
|
|
191
206
|
const includeStaticHints = cfg.includeStaticHints === true;
|
|
192
207
|
const customPrompts = await this.readCustomPrompts();
|
|
208
|
+
const loc = getLocale(this.config);
|
|
209
|
+
const L = FIELD_LABELS[loc];
|
|
193
210
|
|
|
194
211
|
// 并发设置:从配置读取,<=1 则保持串行(兼容顶层/嵌套两种配置形态)
|
|
195
212
|
const segConcurrency = Math.max(1, Number((this.config?.ai?.concurrency ?? this.config?.concurrency) || 1));
|
|
196
213
|
const effectiveTotal = Array.isArray(file.chunks) ? file.chunks.length : (file.totalChunks || 1);
|
|
197
214
|
const availableSlots = this.concurrencyLimiter ? this.concurrencyLimiter.getAvailable() : segConcurrency;
|
|
198
215
|
const workersHead = Math.max(1, Math.min(availableSlots, effectiveTotal));
|
|
199
|
-
const
|
|
200
|
-
|
|
216
|
+
const concurrencyNote = workersHead > 1 ? t(this.config, 'segment_concurrency_note', { workers: workersHead }) : '';
|
|
217
|
+
const totalNoteText = (file.totalChunks && file.totalChunks !== effectiveTotal)
|
|
218
|
+
? t(this.config, 'segment_total_note', { totalChunks: file.totalChunks, effectiveTotal })
|
|
201
219
|
: '';
|
|
202
|
-
logger.progress(
|
|
220
|
+
logger.progress(t(this.config, 'segment_overall_start', {
|
|
221
|
+
file: file.filePath,
|
|
222
|
+
total: effectiveTotal,
|
|
223
|
+
concurrency: concurrencyNote,
|
|
224
|
+
totalNote: totalNoteText
|
|
225
|
+
}));
|
|
203
226
|
|
|
204
227
|
const allIssues = [];
|
|
205
228
|
|
|
@@ -209,7 +232,7 @@ export class AIClient {
|
|
|
209
232
|
|
|
210
233
|
// 提前让出事件循环,允许并发协程启动
|
|
211
234
|
// 注意:真正的“开始分析第X/段”提示将在取得并发许可后输出
|
|
212
|
-
logger.debug(
|
|
235
|
+
logger.debug(t(this.config, 'segment_wait_start_dbg', { index: i + 1, total: effectiveTotal, start: chunk.startLine, end: chunk.endLine }));
|
|
213
236
|
|
|
214
237
|
// 立即让出事件循环,让其他并发协程尽快启动打印日志
|
|
215
238
|
await Promise.resolve();
|
|
@@ -243,44 +266,25 @@ export class AIClient {
|
|
|
243
266
|
|
|
244
267
|
// 添加自定义提示词
|
|
245
268
|
if (customPrompts && customPrompts.length > 0) {
|
|
246
|
-
messages.push({ role: 'user', content: `\n[
|
|
269
|
+
messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
|
|
247
270
|
}
|
|
248
271
|
|
|
249
272
|
// 构建分段分析提示
|
|
250
273
|
const attachLineNumbers = (this.config?.ai?.attachLineNumbersInBatch ?? this.config?.attachLineNumbersInBatch) !== false;
|
|
251
274
|
const contentForAI = attachLineNumbers ? this.addLineNumberPrefixes(clean, lineMapAbs) : clean;
|
|
252
275
|
|
|
253
|
-
const segmentPrompt =
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
- 代码质量问题
|
|
266
|
-
- 最佳实践违反
|
|
267
|
-
|
|
268
|
-
重要:请立即开始分析,不要只是确认收到。必须按以下格式输出每个发现的问题:
|
|
269
|
-
|
|
270
|
-
**-----代码分析结果-----**
|
|
271
|
-
文件路径:${file.filePath}
|
|
272
|
-
代码片段:[具体的问题代码]
|
|
273
|
-
风险等级:[高/中/低]
|
|
274
|
-
风险原因:[问题描述]
|
|
275
|
-
修改建议:[具体的修改建议]
|
|
276
|
-
|
|
277
|
-
如果发现多个问题,每个问题都要用 **-----代码分析结果-----** 开头。
|
|
278
|
-
如果没有发现问题,请回复:
|
|
279
|
-
|
|
280
|
-
**-----代码分析结果-----**
|
|
281
|
-
本段代码无明显问题
|
|
282
|
-
|
|
283
|
-
注意:如片段中每行包含形如 [n] 的源行号前缀,请在你的输出的代码片段中原样保留这些前缀,以便后续定位。`;
|
|
276
|
+
const segmentPrompt = t(this.config, 'segment_prompt_template', {
|
|
277
|
+
index: i + 1,
|
|
278
|
+
total: file.totalChunks,
|
|
279
|
+
Lfile: L.file,
|
|
280
|
+
Lcontent: L.content,
|
|
281
|
+
Lsnippet: L.snippet,
|
|
282
|
+
Lrisk: L.risk,
|
|
283
|
+
Lreason: L.reason,
|
|
284
|
+
Lsuggestion: L.suggestion,
|
|
285
|
+
file: file.filePath,
|
|
286
|
+
content: contentForAI
|
|
287
|
+
});
|
|
284
288
|
|
|
285
289
|
messages.push({
|
|
286
290
|
role: 'user',
|
|
@@ -293,44 +297,57 @@ ${contentForAI}
|
|
|
293
297
|
issue.line >= chunk.startLine && issue.line <= chunk.endLine
|
|
294
298
|
);
|
|
295
299
|
if (segmentStaticIssues.length > 0) {
|
|
296
|
-
const lines = segmentStaticIssues.map((si, idx) =>
|
|
297
|
-
|
|
300
|
+
const lines = segmentStaticIssues.map((si, idx) => {
|
|
301
|
+
const riskDisp = displayRisk(si.risk || 'suggestion', this.config);
|
|
302
|
+
const suggestPart = si.suggestion ? t(this.config, 'inline_suggestion', { suggestion: si.suggestion }) : '';
|
|
303
|
+
return t(this.config, 'segment_static_issue_line', {
|
|
304
|
+
index: idx + 1,
|
|
305
|
+
risk: riskDisp,
|
|
306
|
+
message: si.message,
|
|
307
|
+
suggest: suggestPart,
|
|
308
|
+
snippetLabel: L.snippet,
|
|
309
|
+
snippet: si.snippet || ''
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
messages.push({ role: 'user', content: `${t(this.config, 'segment_static_issues_header', { index: i + 1 })}\n${lines.join('\n')}` });
|
|
298
313
|
}
|
|
299
314
|
}
|
|
300
315
|
|
|
301
316
|
// 发送分段分析请求
|
|
302
317
|
const segReqId = `segment_${path.basename(file.filePath)}_${i + 1}of${file.totalChunks}`;
|
|
303
|
-
const startLabel =
|
|
318
|
+
const startLabel = t(this.config, 'segment_start_label', { file: file.filePath, index: i + 1, total: effectiveTotal, start: chunk.startLine, end: chunk.endLine });
|
|
304
319
|
const responseContent = await this.handleChunkedResponse(messages, segReqId, { onStart: () => logger.info(startLabel) });
|
|
305
320
|
|
|
306
321
|
// 解析分段响应
|
|
307
322
|
const segmentResult = this.parseAIResponse(responseContent, file.filePath, {});
|
|
308
323
|
|
|
309
324
|
const batchPrefix = (typeof file.batchIndex === 'number' && typeof file.batchTotal === 'number')
|
|
310
|
-
?
|
|
325
|
+
? t(this.config, 'segment_batch_prefix', { index: file.batchIndex + 1, total: file.batchTotal })
|
|
311
326
|
: '';
|
|
312
327
|
if (Array.isArray(segmentResult)) {
|
|
313
328
|
allIssues.push(...segmentResult);
|
|
314
|
-
logger.success(
|
|
329
|
+
logger.success(t(this.config, 'segment_analysis_done_n_issues', { batch: batchPrefix, file: file.filePath, index: i + 1, count: segmentResult.length }));
|
|
315
330
|
} else if (segmentResult && segmentResult.issues) {
|
|
316
331
|
allIssues.push(...segmentResult.issues);
|
|
317
|
-
logger.success(
|
|
332
|
+
logger.success(t(this.config, 'segment_analysis_done_n_issues', { batch: batchPrefix, file: file.filePath, index: i + 1, count: segmentResult.issues.length }));
|
|
318
333
|
} else {
|
|
319
|
-
logger.success(
|
|
334
|
+
logger.success(t(this.config, 'segment_analysis_done_zero', { batch: batchPrefix, file: file.filePath, index: i + 1 }));
|
|
320
335
|
}
|
|
321
336
|
} catch (error) {
|
|
322
|
-
logger.error(
|
|
337
|
+
logger.error(t(this.config, 'segment_analysis_failed', { index: i + 1, error: error.message }));
|
|
323
338
|
}
|
|
324
339
|
};
|
|
325
340
|
|
|
326
341
|
// 按配置执行并发或串行
|
|
327
342
|
const total = effectiveTotal;
|
|
328
343
|
const workers = Math.max(1, Math.min((this.concurrencyLimiter ? this.concurrencyLimiter.getAvailable() : segConcurrency), total));
|
|
329
|
-
|
|
330
|
-
? `(总段数 ${file.totalChunks},本批次处理 ${total} 段)`
|
|
331
|
-
: '';
|
|
344
|
+
|
|
332
345
|
// 调度细节降为调试级别,避免扰乱终端主要进度
|
|
333
|
-
logger.debug(
|
|
346
|
+
logger.debug(t(this.config, 'segment_schedule_dbg', {
|
|
347
|
+
workers,
|
|
348
|
+
total,
|
|
349
|
+
note: (file.totalChunks && file.totalChunks !== total) ? t(this.config, 'segment_total_note', { totalChunks: file.totalChunks, effectiveTotal: total }) : ''
|
|
350
|
+
}));
|
|
334
351
|
if (workers <= 1) {
|
|
335
352
|
for (let i = 0; i < total; i++) {
|
|
336
353
|
// eslint-disable-next-line no-await-in-loop
|
|
@@ -340,7 +357,7 @@ ${contentForAI}
|
|
|
340
357
|
let cursor = 0;
|
|
341
358
|
const runWorker = async (workerId) => {
|
|
342
359
|
// 并发协程启动提示降为调试级别
|
|
343
|
-
logger.debug(
|
|
360
|
+
logger.debug(t(this.config, 'segment_worker_start_dbg', { id: workerId }));
|
|
344
361
|
while (true) {
|
|
345
362
|
const i = cursor++;
|
|
346
363
|
if (i >= total) break;
|
|
@@ -349,7 +366,7 @@ ${contentForAI}
|
|
|
349
366
|
}
|
|
350
367
|
};
|
|
351
368
|
await Promise.all(Array.from({ length: workers }, (_, idx) => runWorker(idx + 1)));
|
|
352
|
-
logger.debug(
|
|
369
|
+
logger.debug(t(this.config, 'segment_concurrency_done_dbg', { total, extra: (file.totalChunks && file.totalChunks !== total) ? `/${file.totalChunks}` : '' }));
|
|
353
370
|
}
|
|
354
371
|
|
|
355
372
|
return {
|
|
@@ -361,10 +378,10 @@ ${contentForAI}
|
|
|
361
378
|
};
|
|
362
379
|
|
|
363
380
|
} catch (error) {
|
|
364
|
-
logger.error(
|
|
381
|
+
logger.error(t(this.config, 'segment_file_failed', { error: error.message }));
|
|
365
382
|
// 如果是AI请求失败,应该终止程序而不是继续处理
|
|
366
383
|
if (error.message.includes('Connection error') || error.message.includes('API') || error.message.includes('请求失败')) {
|
|
367
|
-
logger.error('
|
|
384
|
+
logger.error(t(this.config, 'ai_connection_failed'));
|
|
368
385
|
process.exit(1);
|
|
369
386
|
}
|
|
370
387
|
throw error; // 重新抛出错误,让上层调用者处理
|
|
@@ -383,8 +400,9 @@ ${contentForAI}
|
|
|
383
400
|
const includeStaticHints = cfg.includeStaticHints === true;
|
|
384
401
|
const customPrompts = await this.readCustomPrompts();
|
|
385
402
|
const staticIssues = options.staticIssues || [];
|
|
386
|
-
|
|
387
|
-
|
|
403
|
+
const loc = getLocale(this.config);
|
|
404
|
+
const L = FIELD_LABELS[loc];
|
|
405
|
+
logger.debug(t(this.config, 'ai_diff_start_dbg', { file: fileData.filePath, added: fileData.totalAddedLines }));
|
|
388
406
|
|
|
389
407
|
// 构建diff专用的系统提示词
|
|
390
408
|
const diffSystemPrompt = this.getDiffSystemPrompt();
|
|
@@ -395,67 +413,72 @@ ${contentForAI}
|
|
|
395
413
|
|
|
396
414
|
// 添加自定义提示词
|
|
397
415
|
if (customPrompts && customPrompts.length > 0) {
|
|
398
|
-
messages.push({ role: 'user', content: `\n[
|
|
416
|
+
messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
|
|
399
417
|
}
|
|
400
418
|
|
|
401
419
|
// 构建diff分析提示
|
|
402
|
-
const
|
|
403
|
-
|
|
404
|
-
文件路径:${fileData.filePath}
|
|
405
|
-
新增代码行数:${fileData.totalAddedLines}
|
|
406
|
-
智能分段数:${fileData.segments.length}
|
|
407
|
-
|
|
408
|
-
变更内容:`;
|
|
420
|
+
const intro = t(this.config, 'diff_intro');
|
|
421
|
+
const diffPrompt = `${intro}\n\n${L.file}${fileData.filePath}\n${t(this.config, 'diff_added_lines_label')}${fileData.totalAddedLines}\n${t(this.config, 'diff_smart_segments_label')}${fileData.segments.length}\n\n${t(this.config, 'diff_changes_label')}`;
|
|
409
422
|
|
|
410
423
|
messages.push({ role: 'user', content: diffPrompt });
|
|
411
424
|
|
|
412
425
|
// 添加每个智能分段
|
|
413
426
|
for (let i = 0; i < fileData.segments.length; i++) {
|
|
414
427
|
const segment = fileData.segments[i];
|
|
415
|
-
const
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
428
|
+
const segTitle = t(this.config, 'diff_segment_title', { index: i + 1, total: fileData.segments.length });
|
|
429
|
+
const segMeta = t(this.config, 'diff_segment_meta', {
|
|
430
|
+
start: segment.startLine,
|
|
431
|
+
end: segment.endLine,
|
|
432
|
+
added: segment.addedLinesCount,
|
|
433
|
+
tokens: segment.estimatedTokens
|
|
434
|
+
});
|
|
435
|
+
const segmentPrompt = `\n[${segTitle}] (${segMeta})\n\`\`\`diff\n${segment.content}\n\`\`\``;
|
|
420
436
|
|
|
421
437
|
messages.push({ role: 'user', content: segmentPrompt });
|
|
422
438
|
// 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
|
|
423
|
-
messages.push({ role: 'user', content:
|
|
439
|
+
messages.push({ role: 'user', content: t(this.config, 'ignore_rule') });
|
|
424
440
|
}
|
|
425
441
|
|
|
426
442
|
// 添加静态分析提示(如果有)
|
|
427
443
|
if (includeStaticHints && staticIssues.length > 0) {
|
|
428
|
-
const hintLines = staticIssues.map((issue, idx) =>
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
444
|
+
const hintLines = staticIssues.map((issue, idx) => {
|
|
445
|
+
const riskDisp = displayRisk(issue.risk || 'suggestion', this.config);
|
|
446
|
+
const suggestPart = issue.suggestion ? t(this.config, 'inline_suggestion', { suggestion: issue.suggestion }) : '';
|
|
447
|
+
return t(this.config, 'local_rule_hint_line', {
|
|
448
|
+
index: idx + 1,
|
|
449
|
+
risk: riskDisp,
|
|
450
|
+
message: issue.message,
|
|
451
|
+
suggest: suggestPart,
|
|
452
|
+
snippet: issue.snippet || ''
|
|
453
|
+
});
|
|
454
|
+
});
|
|
455
|
+
const hintsPrompt = `\n[${t(this.config, 'local_rule_findings')}]\n${hintLines.join('\n')}`;
|
|
432
456
|
messages.push({ role: 'user', content: hintsPrompt });
|
|
433
457
|
}
|
|
434
458
|
|
|
435
459
|
// 添加最终指令
|
|
436
|
-
const finalInstruction =
|
|
437
|
-
请仅对标记为"+"的新增代码行进行审查,忽略删除行(-)和上下文行。每个问题用空行分隔,必须包含"文件路径:${fileData.filePath}"和具体的代码片段。禁止使用文字行号或行范围描述(如“第X行/第X-Y行”);如片段中存在每行的[行号]前缀请原样保留。`;
|
|
460
|
+
const finalInstruction = t(this.config, 'diff_final_instruction', { file: fileData.filePath });
|
|
438
461
|
|
|
439
462
|
messages.push({ role: 'user', content: finalInstruction });
|
|
440
463
|
// 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
|
|
441
|
-
messages.push({ role: 'user', content:
|
|
464
|
+
messages.push({ role: 'user', content: t(this.config, 'ignore_rule') });
|
|
442
465
|
|
|
443
466
|
// 记录请求信息
|
|
444
|
-
logger.debug(
|
|
467
|
+
logger.debug(t(this.config, 'ai_diff_send_dbg', { model: this.config.model ?? 'gpt-3.5-turbo', messages: messages.length }));
|
|
445
468
|
|
|
446
469
|
// 发送请求并处理响应
|
|
447
470
|
const diffReqId = `diff_${path.basename(fileData.filePath)}`;
|
|
448
471
|
const responseContent = await this.handleChunkedResponse(messages, diffReqId);
|
|
449
472
|
const issues = this.parseAIResponse(responseContent, fileData.filePath);
|
|
450
473
|
|
|
451
|
-
logger.debug(
|
|
474
|
+
logger.debug(t(this.config, 'ai_diff_done_dbg', { file: fileData.filePath, issues: issues.length }));
|
|
452
475
|
|
|
453
476
|
return issues || [];
|
|
454
477
|
|
|
455
478
|
} catch (error) {
|
|
456
|
-
logger.error(
|
|
479
|
+
logger.error(t(this.config, 'ai_diff_failed', { path: fileData.filePath, error: error.message }));
|
|
457
480
|
if (error.message.includes('Connection error') || error.message.includes('API') || error.message.includes('请求失败')) {
|
|
458
|
-
logger.error('
|
|
481
|
+
logger.error(t(this.config, 'ai_connection_failed'));
|
|
459
482
|
process.exit(1);
|
|
460
483
|
}
|
|
461
484
|
throw error;
|
|
@@ -468,17 +491,20 @@ ${segment.content}
|
|
|
468
491
|
const cfg = this.config?.ai || {};
|
|
469
492
|
const includeStaticHints = cfg.includeStaticHints === true;
|
|
470
493
|
const customPrompts = await this.readCustomPrompts();
|
|
494
|
+
const loc = getLocale(this.config);
|
|
495
|
+
const L = FIELD_LABELS[loc];
|
|
496
|
+
const { systemPrompt } = buildPrompts(this.config);
|
|
471
497
|
|
|
472
498
|
const messages = [
|
|
473
|
-
{ role: 'system', content:
|
|
499
|
+
{ role: 'system', content: systemPrompt },
|
|
474
500
|
{
|
|
475
501
|
role: 'user',
|
|
476
|
-
content:
|
|
502
|
+
content: t(this.config, 'batch_files_intro')
|
|
477
503
|
}
|
|
478
504
|
];
|
|
479
505
|
|
|
480
506
|
if (customPrompts.length > 0) {
|
|
481
|
-
messages.push({ role: 'user', content: `\n[
|
|
507
|
+
messages.push({ role: 'user', content: `\n[${t(this.config, 'custom_prompts_label')}]\n${customPrompts.join('\n\n---\n')}` });
|
|
482
508
|
}
|
|
483
509
|
|
|
484
510
|
// 逐文件添加内容
|
|
@@ -488,9 +514,10 @@ ${segment.content}
|
|
|
488
514
|
const { clean, lineMap } = await prepareForAIWithLineMap(content, filePath);
|
|
489
515
|
const attachLineNumbers = this.config?.ai?.attachLineNumbersInBatch !== false;
|
|
490
516
|
const contentForAI = attachLineNumbers ? this.addLineNumberPrefixes(clean, lineMap) : clean;
|
|
517
|
+
const failedText = failedStatic ? t(this.config, 'file_failed_static_suffix') : '';
|
|
491
518
|
messages.push({
|
|
492
519
|
role: 'user',
|
|
493
|
-
content:
|
|
520
|
+
content: `${L.file}${filePath}${failedText}\n${L.content}\n\`\`\`\n${contentForAI}\n\`\`\``
|
|
494
521
|
});
|
|
495
522
|
requestPreviews.push({ filePath, contentForAI });
|
|
496
523
|
}
|
|
@@ -502,8 +529,19 @@ ${segment.content}
|
|
|
502
529
|
if (e.failedStatic !== true) continue; // 仅针对本地未通过的文件汇总静态提示
|
|
503
530
|
const staticIssues = e.staticIssues || [];
|
|
504
531
|
if (staticIssues.length === 0) continue;
|
|
505
|
-
const lines = staticIssues.map((i, idx) =>
|
|
506
|
-
|
|
532
|
+
const lines = staticIssues.map((i, idx) => {
|
|
533
|
+
const riskDisp = displayRisk(i.risk || 'suggestion', this.config);
|
|
534
|
+
const suggestPart = i.suggestion ? t(this.config, 'inline_suggestion', { suggestion: i.suggestion }) : '';
|
|
535
|
+
return t(this.config, 'local_rule_hint_line', {
|
|
536
|
+
index: idx + 1,
|
|
537
|
+
risk: riskDisp,
|
|
538
|
+
message: i.message,
|
|
539
|
+
suggest: suggestPart,
|
|
540
|
+
snippet: i.snippet || ''
|
|
541
|
+
});
|
|
542
|
+
});
|
|
543
|
+
const header = t(this.config, 'local_rule_findings_header', { file: e.filePath });
|
|
544
|
+
hintsParts.push(`${header}\n${lines.join('\n')}`);
|
|
507
545
|
}
|
|
508
546
|
if (hintsParts.length > 0) {
|
|
509
547
|
messages.push({ role: 'user', content: hintsParts.join('\n\n') });
|
|
@@ -511,8 +549,10 @@ ${segment.content}
|
|
|
511
549
|
}
|
|
512
550
|
|
|
513
551
|
// 添加最终指令,确保包含片段并保留行号前缀
|
|
514
|
-
const finalInstructionBatch =
|
|
552
|
+
const finalInstructionBatch = t(this.config, 'final_instruction_batch');
|
|
515
553
|
messages.push({ role: 'user', content: finalInstructionBatch });
|
|
554
|
+
// 追加严格忽略规则,避免模型输出“行号跳跃/预处理移除”的提示
|
|
555
|
+
messages.push({ role: 'user', content: t(this.config, 'ignore_rule') });
|
|
516
556
|
const useDynamic = (this.config?.ai?.dynamicMaxTokens !== false);
|
|
517
557
|
const contextWindow = (() => {
|
|
518
558
|
const cw = Number(this.config.contextWindow ?? AI_CONSTANTS.DEFAULT_CONTEXT_WINDOW);
|
|
@@ -546,7 +586,7 @@ ${segment.content}
|
|
|
546
586
|
// 批量响应:不传单个 filePath,让解析函数从AI返回的“文件名称:”中读取
|
|
547
587
|
return this.parseAIResponse(response.choices[0].message.content, undefined, {});
|
|
548
588
|
} catch (error) {
|
|
549
|
-
logger.error(
|
|
589
|
+
logger.error(t(this.config, 'ai_batch_failed', { error: error.message }));
|
|
550
590
|
return [];
|
|
551
591
|
}
|
|
552
592
|
}
|
|
@@ -587,7 +627,7 @@ ${segment.content}
|
|
|
587
627
|
throw error;
|
|
588
628
|
}
|
|
589
629
|
const delay = baseDelay * Math.pow(2, attempt - 1);
|
|
590
|
-
logger.warn(
|
|
630
|
+
logger.warn(t(this.config, 'ai_retry_warn', { attempt, retries, delay, error: error?.message || String(error) }));
|
|
591
631
|
await new Promise((r) => setTimeout(r, delay));
|
|
592
632
|
} finally {
|
|
593
633
|
if (release) {
|
|
@@ -603,127 +643,10 @@ ${segment.content}
|
|
|
603
643
|
this.cacheStats.hits++;
|
|
604
644
|
return this.systemPromptCache;
|
|
605
645
|
}
|
|
606
|
-
|
|
607
646
|
this.cacheStats.misses++;
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
如果你的回答内容过长,可能超出单次响应的token限制,请按以下格式进行分段:
|
|
612
|
-
|
|
613
|
-
1. 在每段结尾添加分段标记:
|
|
614
|
-
- 未完成:[CHUNK_CONTINUE]
|
|
615
|
-
- 已完成:[CHUNK_END]
|
|
616
|
-
- 分段索引:[CHUNK_X/Y](X为当前段,Y为总段数)
|
|
617
|
-
|
|
618
|
-
2. 示例格式:
|
|
619
|
-
\`\`\`
|
|
620
|
-
这是第一段分析内容...
|
|
621
|
-
[CHUNK_1/3]
|
|
622
|
-
[CHUNK_CONTINUE]
|
|
623
|
-
\`\`\`
|
|
624
|
-
|
|
625
|
-
3. 当收到"继续"时,请继续输出下一段内容。
|
|
626
|
-
|
|
627
|
-
4. 最后一段必须以 [CHUNK_END] 结尾。
|
|
628
|
-
|
|
629
|
-
**代码分析输出格式**
|
|
630
|
-
请严格按照以下格式返回分析结果,每个问题必须使用开始/结束标记包裹,并以空行分隔:
|
|
631
|
-
|
|
632
|
-
**-----代码分析结果开始-----**
|
|
633
|
-
文件路径:{文件路径}
|
|
634
|
-
代码片段:{具体的给定审查代码的原始片段内容,不允许丢失任何字符,仅当问题属于全局性或架构性问题时才可留空}
|
|
635
|
-
风险等级:{致命/高危/中危/低危/建议}
|
|
636
|
-
风险原因:{详细原因(必须包含证据:代码片段)}
|
|
637
|
-
修改建议:{具体建议(允许多行)}
|
|
638
|
-
**-----代码分析结果结束-----**
|
|
639
|
-
|
|
640
|
-
若输入中包含"[本地规则发现的问题]",请顺便判断本地规则是否是误报,如是误报返回对应规则,并在"风险原因"中说明误报理由;若不是误报可忽略该对应本地规则。
|
|
641
|
-
|
|
642
|
-
风险等级及范围定义:
|
|
643
|
-
|
|
644
|
-
**致命级别**:可能导致系统崩溃、数据丢失、严重安全漏洞
|
|
645
|
-
- 前端:未捕获的异常导致页面崩溃、XSS攻击漏洞、敏感信息泄露到控制台、无限递归导致浏览器卡死、计时器未清理等
|
|
646
|
-
- 后端:空指针异常、数据库连接泄露、SQL注入、未授权的数据访问、死循环导致服务器宕机等
|
|
647
|
-
- 引擎/核心:内存泄漏、缓冲区溢出、竞态条件、资源未释放、关键算法错误等
|
|
648
|
-
|
|
649
|
-
**高危级别**:可能导致安全漏洞、数据泄露、业务逻辑错误
|
|
650
|
-
- 前端:CSRF攻击风险、本地存储敏感数据、不安全的API调用、用户输入未验证等
|
|
651
|
-
- 后端:密码硬编码、权限验证缺失、敏感数据未加密、API接口未鉴权、文件上传漏洞等
|
|
652
|
-
- 引擎/核心:加密算法使用错误、随机数生成不安全、配置文件暴露、日志记录敏感信息等
|
|
653
|
-
|
|
654
|
-
**中危级别**:可能影响系统稳定性、性能问题、用户体验
|
|
655
|
-
- 前端:组件重复渲染、大量DOM操作、未优化的网络请求、内存占用过高、响应式设计问题等
|
|
656
|
-
- 后端:数据库查询未优化、缓存策略不当、线程池配置不合理、API响应时间过长等
|
|
657
|
-
- 引擎/核心:算法复杂度过高、资源使用效率低、并发处理能力不足、错误处理机制不完善等
|
|
658
|
-
|
|
659
|
-
**低危级别**:代码质量问题、不符合最佳实践、可维护性问题
|
|
660
|
-
- 前端:组件职责不清、状态管理混乱、样式代码重复、TypeScript类型定义不准确等
|
|
661
|
-
- 后端:代码重复、函数过长、类设计不合理、异常处理不规范、日志记录不完整等
|
|
662
|
-
- 引擎/核心:模块耦合度高、接口设计不清晰、命名规范不统一、注释缺失等
|
|
663
|
-
|
|
664
|
-
**建议级别**:改进建议,不影响功能,提升代码质量
|
|
665
|
-
- 前端:组件拆分优化、Hook使用优化、CSS样式优化、代码格式化、变量命名改进、未使用变量清理等
|
|
666
|
-
- 后端:方法重构、参数验证完善、返回值优化、代码注释补充、单元测试覆盖等
|
|
667
|
-
- 引擎/核心:性能监控添加、配置参数调优、文档完善、代码结构优化、版本兼容性等
|
|
668
|
-
|
|
669
|
-
**代码片段格式要求:**
|
|
670
|
-
1. 必须提供具体的原始代码片段内容,直接返回纯文本代码,不要使用markdown代码块格式(如\`\`\`typescript等)
|
|
671
|
-
2. 仅在以下全局性或架构性问题时才可留空:
|
|
672
|
-
- 整体项目架构设计问题
|
|
673
|
-
- 跨多个文件的设计模式问题
|
|
674
|
-
- 全局配置或依赖管理问题
|
|
675
|
-
- 整体代码组织结构问题
|
|
676
|
-
3. 对于具体的代码问题(如函数、变量、语句等),必须提供相关的代码片段作为证据
|
|
677
|
-
4. 提供的原始内容,不允许以任何形式导致字符丢失,必须保证提供的内容准确性
|
|
678
|
-
5. 若输入中包含每行的\`[n]\`行号前缀(例如\`[123]\`),请原样保留这些前缀作为片段的一部分,不要自行生成、修改或移除行号
|
|
679
|
-
|
|
680
|
-
禁止使用文字描述的行号或行范围(如“第X行/第X-Y行”);允许片段中的\`[n]\`前缀,它是输入的一部分
|
|
681
|
-
|
|
682
|
-
**行号说明与误报避免:**
|
|
683
|
-
1. 我可能会对代码进行预处理(剥离注释、跳过标记为“无需审查”的片段等),因此片段中的\`[n]\`源行号可能出现不连续(例如从278直接跳到294)。这是正常现象,不代表代码缺失或结构问题。
|
|
684
|
-
2. 严禁将“行号不连续/行号跳跃/注释行号不连续”等现象判定为问题或风险原因;遇到此类情况请直接忽略,不要输出相应的风险或建议。
|
|
685
|
-
|
|
686
|
-
**不在审查范围(必须忽略):**
|
|
687
|
-
1. 预处理造成的元信息差异:行号不连续/跳跃、注释移除、被标记为“无需审查”的片段剥离。
|
|
688
|
-
2. 展示层的占位或截断标记(如“中间省略”)。
|
|
689
|
-
3. 请不要在任何输出字段(风险原因、建议或片段)中提及“行号跳跃”、“行号不连续”、“被预处理移除”等措辞;若遇到此类情况,直接忽略,不要生成问题。
|
|
690
|
-
|
|
691
|
-
**重点分析领域:**
|
|
692
|
-
|
|
693
|
-
1. **安全问题**:
|
|
694
|
-
- 输入验证缺失、SQL注入、XSS攻击、CSRF漏洞等
|
|
695
|
-
- 敏感信息硬编码(密码、API密钥、令牌等)
|
|
696
|
-
- 权限验证缺失、未授权访问、数据泄露风险等
|
|
697
|
-
- 不安全的加密算法、随机数生成、文件操作等
|
|
698
|
-
|
|
699
|
-
2. **性能问题**:
|
|
700
|
-
- 算法复杂度过高、嵌套循环、递归深度等
|
|
701
|
-
- 内存泄漏、资源未释放、缓存策略不当等
|
|
702
|
-
- 数据库查询未优化、N+1查询问题等
|
|
703
|
-
- 前端组件重复渲染、大量DOM操作、网络请求优化等
|
|
704
|
-
|
|
705
|
-
3. **代码质量问题**:
|
|
706
|
-
- 函数过长、类职责不清、代码重复等
|
|
707
|
-
- 复杂的条件判断、深层嵌套、魔法数字等
|
|
708
|
-
- 变量命名不规范、类型定义不准确等
|
|
709
|
-
- 异常处理不完善、错误信息不明确等
|
|
710
|
-
|
|
711
|
-
4. **架构与设计问题**:
|
|
712
|
-
- 模块耦合度过高、依赖关系混乱等
|
|
713
|
-
- 设计模式使用不当、接口设计不合理等
|
|
714
|
-
- 配置管理问题、环境变量使用等
|
|
715
|
-
- 单元测试覆盖率、可测试性设计等
|
|
716
|
-
|
|
717
|
-
**代码片段与行号约束**:
|
|
718
|
-
- 如输入片段存在每行的\`[行号]\`前缀,请原样保留,它是输入的一部分
|
|
719
|
-
- 禁止使用文字描述的行号或行范围(如“第X行/第X-Y行”);必须以片段呈现问题
|
|
720
|
-
|
|
721
|
-
**行号说明与误报避免(必须遵守)**:
|
|
722
|
-
- 由于预处理(剥离注释、跳过无需审查片段等),片段中的\`[n]\`源行号可能不连续。这是正常的,务必严格忽略“行号不连续/行号跳跃/被预处理移除”等现象,不得将其视为问题或风险原因,不得提出“检查代码完整性/补全缺失代码”等建议。
|
|
723
|
-
- 输出中禁止出现相关措辞(如“行号跳跃”、“行号不连续”、“被预处理移除”)。如遇此类情形请直接忽略,不要生成任何问题。
|
|
724
|
-
`;
|
|
725
|
-
|
|
726
|
-
return this.systemPromptCache;
|
|
647
|
+
const { systemPrompt } = buildPrompts(this.config);
|
|
648
|
+
this.systemPromptCache = systemPrompt;
|
|
649
|
+
return this.systemPromptCache;
|
|
727
650
|
}
|
|
728
651
|
|
|
729
652
|
/**
|
|
@@ -731,74 +654,15 @@ ${segment.content}
|
|
|
731
654
|
* @returns {string} diff专用系统提示词
|
|
732
655
|
*/
|
|
733
656
|
getDiffSystemPrompt() {
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
请严格按照以下格式返回分析结果,每个问题必须使用开始/结束标记包裹,并以空行分隔:
|
|
744
|
-
|
|
745
|
-
**-----Git Diff代码分析结果开始-----**
|
|
746
|
-
文件路径:{文件路径}
|
|
747
|
-
代码片段:{具体的新增代码片段(+号标记的内容),不包含+号前缀;如片段中存在每行的[行号]前缀,请原样保留}
|
|
748
|
-
风险等级:{致命/高危/中危/低危/建议}
|
|
749
|
-
风险原因:{详细原因,重点说明新增代码可能引入的问题}
|
|
750
|
-
修改建议:{针对新增代码的具体修改建议(允许多行)}
|
|
751
|
-
**-----Git Diff代码分析结果结束-----**
|
|
752
|
-
|
|
753
|
-
**风险等级定义(专注于新增代码):**
|
|
754
|
-
|
|
755
|
-
**致命级别**:新增代码可能导致系统崩溃、数据丢失、严重安全漏洞
|
|
756
|
-
- 新增的未捕获异常、空指针引用、无限循环
|
|
757
|
-
- 新增的SQL注入、XSS攻击漏洞、敏感信息泄露
|
|
758
|
-
- 新增的内存泄漏、资源未释放、死锁风险
|
|
759
|
-
|
|
760
|
-
**高危级别**:新增代码可能导致安全漏洞、数据泄露、业务逻辑错误
|
|
761
|
-
- 新增的权限验证缺失、未授权访问
|
|
762
|
-
- 新增的密码硬编码、敏感数据未加密
|
|
763
|
-
- 新增的不安全API调用、输入验证缺失
|
|
764
|
-
|
|
765
|
-
**中危级别**:新增代码可能影响系统稳定性、性能问题
|
|
766
|
-
- 新增的性能瓶颈、算法复杂度过高
|
|
767
|
-
- 新增的数据库查询未优化、缓存策略不当
|
|
768
|
-
- 新增的组件重复渲染、内存占用过高
|
|
769
|
-
|
|
770
|
-
**低危级别**:新增代码的质量问题、不符合最佳实践
|
|
771
|
-
- 新增的代码重复、函数过长、命名不规范
|
|
772
|
-
- 新增的异常处理不规范、日志记录不完整
|
|
773
|
-
- 新增的类型定义不准确、注释缺失
|
|
774
|
-
|
|
775
|
-
**建议级别**:新增代码的改进建议,提升代码质量
|
|
776
|
-
- 新增代码的格式化、变量命名改进
|
|
777
|
-
- 新增代码的重构建议、性能优化
|
|
778
|
-
- 新增代码的单元测试覆盖、文档完善
|
|
779
|
-
|
|
780
|
-
**重点关注新增代码的问题:**
|
|
781
|
-
1. **安全风险**:新增代码是否引入安全漏洞
|
|
782
|
-
2. **性能影响**:新增代码是否影响系统性能
|
|
783
|
-
3. **逻辑错误**:新增代码是否存在业务逻辑错误
|
|
784
|
-
4. **兼容性**:新增代码是否与现有代码兼容
|
|
785
|
-
5. **可维护性**:新增代码是否易于维护和扩展
|
|
786
|
-
|
|
787
|
-
**代码片段要求:**
|
|
788
|
-
- 只提供新增的代码片段(去除+号前缀,同时保留每行的\`[行号]\`前缀)
|
|
789
|
-
- 不要包含上下文行或删除行
|
|
790
|
-
- 确保代码片段的准确性,不允许字符丢失
|
|
791
|
-
- 禁止使用文字描述的行号或行范围;允许片段中的\`[n]\`前缀,它是输入的一部分
|
|
792
|
-
|
|
793
|
-
**行号说明与误报避免:**
|
|
794
|
-
- 由于仅审查新增行以及上游可能进行的预处理(如剥离注释、跳过无需审查片段),片段中的\`[n]\`源行号可能出现不连续。这是正常的,请不要将“行号不连续/行号跳跃”等现象视为问题或风险原因,务必忽略。
|
|
795
|
-
|
|
796
|
-
**不在审查范围(必须忽略):**
|
|
797
|
-
- 预处理引起的行号不连续/跳跃、注释或无需审查内容的移除。
|
|
798
|
-
- 展示预览的占位或截断标记(如“中间省略”)。
|
|
799
|
-
- 输出中禁止出现“行号跳跃”、“行号不连续”、“被预处理移除”等措辞;如遇此类情形请忽略,不要生成任何问题。
|
|
800
|
-
|
|
801
|
-
记住:你的任务是审查新增的代码变更,帮助开发者在代码合并前发现和修复潜在问题。`;
|
|
657
|
+
// 使用 i18n 生成并缓存 Diff 专用系统提示词
|
|
658
|
+
if (this.diffSystemPromptCache) {
|
|
659
|
+
this.cacheStats.hits++;
|
|
660
|
+
return this.diffSystemPromptCache;
|
|
661
|
+
}
|
|
662
|
+
this.cacheStats.misses++;
|
|
663
|
+
const { diffSystemPrompt } = buildPrompts(this.config);
|
|
664
|
+
this.diffSystemPromptCache = diffSystemPrompt;
|
|
665
|
+
return this.diffSystemPromptCache;
|
|
802
666
|
}
|
|
803
667
|
|
|
804
668
|
getCacheStats() {
|
|
@@ -844,7 +708,7 @@ ${segment.content}
|
|
|
844
708
|
prompts.push(content.trim());
|
|
845
709
|
}
|
|
846
710
|
} catch (e) {
|
|
847
|
-
logger.warn(
|
|
711
|
+
logger.warn(t(this.config, 'read_ai_prompt_file_failed', { file: filePath, error: e?.message || String(e) }));
|
|
848
712
|
}
|
|
849
713
|
}
|
|
850
714
|
}
|
|
@@ -853,7 +717,7 @@ ${segment.content}
|
|
|
853
717
|
this.promptCache.set(cacheKey, prompts);
|
|
854
718
|
return prompts;
|
|
855
719
|
} catch (error) {
|
|
856
|
-
logger.warn('
|
|
720
|
+
logger.warn(t(this.config, 'read_custom_prompts_failed', { error: error?.message || String(error) }));
|
|
857
721
|
return [];
|
|
858
722
|
}
|
|
859
723
|
}
|
|
@@ -869,7 +733,9 @@ ${segment.content}
|
|
|
869
733
|
// 优先:按开始/结束标记提取块
|
|
870
734
|
const markerRegexes = [
|
|
871
735
|
/\*\*-----代码分析结果开始-----\*\*([\s\S]*?)\*\*-----代码分析结果结束-----\*\*/g,
|
|
872
|
-
/\*\*-----Git Diff代码分析结果开始-----\*\*([\s\S]*?)\*\*-----Git Diff代码分析结果结束-----\*\*/g
|
|
736
|
+
/\*\*-----Git Diff代码分析结果开始-----\*\*([\s\S]*?)\*\*-----Git Diff代码分析结果结束-----\*\*/g,
|
|
737
|
+
/\*\*-----Code Analysis Result Start-----\*\*([\s\S]*?)\*\*-----Code Analysis Result End-----\*\*/g,
|
|
738
|
+
/\*\*-----Git Diff Code Analysis Result Start-----\*\*([\s\S]*?)\*\*-----Git Diff Code Analysis Result End-----\*\*/g
|
|
873
739
|
];
|
|
874
740
|
let hasStandardFormat = false;
|
|
875
741
|
for (const re of markerRegexes) {
|
|
@@ -888,7 +754,12 @@ ${segment.content}
|
|
|
888
754
|
if (!hasStandardFormat) {
|
|
889
755
|
let blocks = response.split('\n\n');
|
|
890
756
|
for (const block of blocks) {
|
|
891
|
-
if (
|
|
757
|
+
if (
|
|
758
|
+
block.includes('**-----代码分析结果-----**') ||
|
|
759
|
+
block.includes('**-----Git Diff代码分析结果-----**') ||
|
|
760
|
+
block.includes('**-----Code Analysis Result-----**') ||
|
|
761
|
+
block.includes('**-----Git Diff Code Analysis Result-----**')
|
|
762
|
+
) {
|
|
892
763
|
hasStandardFormat = true;
|
|
893
764
|
const issue = this.parseIssueBlock(block, filePath, context);
|
|
894
765
|
if (issue) issues.push(issue);
|
|
@@ -913,9 +784,9 @@ ${segment.content}
|
|
|
913
784
|
return context.staticIssues.map((i) => ({
|
|
914
785
|
file: filePath,
|
|
915
786
|
source: 'ai',
|
|
916
|
-
risk: i.risk || 'suggestion',
|
|
917
|
-
message:
|
|
918
|
-
suggestion: i.suggestion || '
|
|
787
|
+
risk: this.normalizeRiskLevel(i.risk || 'suggestion'),
|
|
788
|
+
message: t(this.config, 'fallback_static_reason', { message: i.message }),
|
|
789
|
+
suggestion: i.suggestion || t(this.config, 'fallback_static_suggestion'),
|
|
919
790
|
snippet: i.snippet || ''
|
|
920
791
|
}));
|
|
921
792
|
}
|
|
@@ -931,7 +802,7 @@ ${segment.content}
|
|
|
931
802
|
|
|
932
803
|
for (const line of lines) {
|
|
933
804
|
// 检查是否是新问题的开始(问题1:, 问题2: 等)
|
|
934
|
-
if (/^问题\d+[::]/.test(line.trim())) {
|
|
805
|
+
if (/^问题\d+[::]/.test(line.trim()) || /^Issue\s*\d+[::]?/i.test(line.trim())) {
|
|
935
806
|
// 如果当前块有内容,保存它
|
|
936
807
|
if (currentBlock.length > 0) {
|
|
937
808
|
blocks.push(currentBlock.join('\n'));
|
|
@@ -952,162 +823,132 @@ ${segment.content}
|
|
|
952
823
|
parseIssueBlock(block, filePath, context = {}) {
|
|
953
824
|
const lines = block.split('\n');
|
|
954
825
|
const issue = { source: 'ai' };
|
|
955
|
-
|
|
956
|
-
|
|
826
|
+
|
|
827
|
+
const LCN = FIELD_LABELS['zh-CN'];
|
|
828
|
+
const LEN = FIELD_LABELS['en-US'];
|
|
829
|
+
const FILE_LABELS = [LCN.file, LEN.file];
|
|
830
|
+
const RISK_LABELS = [LCN.risk, LEN.risk];
|
|
831
|
+
const REASON_LABELS = [LCN.reason, LEN.reason];
|
|
832
|
+
const SUGGEST_LABELS = [LCN.suggestion, LEN.suggestion];
|
|
833
|
+
const SNIPPET_LABELS = [LCN.snippet, LEN.snippet];
|
|
834
|
+
|
|
835
|
+
const startsWithAny = (text, labels) => {
|
|
836
|
+
const tline = String(text || '').trim();
|
|
837
|
+
return labels.some(l => tline.startsWith(l));
|
|
838
|
+
};
|
|
839
|
+
const extractAfterAny = (text, labels) => {
|
|
840
|
+
const tline = String(text || '').trim();
|
|
841
|
+
for (const l of labels) {
|
|
842
|
+
if (tline.startsWith(l)) return tline.slice(l.length).trim();
|
|
843
|
+
}
|
|
844
|
+
return null;
|
|
845
|
+
};
|
|
846
|
+
|
|
957
847
|
if (!filePath && context.fileList && context.fileList.length > 0) {
|
|
958
|
-
|
|
959
|
-
const filePathLine = lines.find(line => line.startsWith('文件路径:'));
|
|
848
|
+
const filePathLine = lines.find(line => startsWithAny(line, FILE_LABELS));
|
|
960
849
|
if (filePathLine) {
|
|
961
|
-
const aiPath = filePathLine
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
const
|
|
965
|
-
|
|
966
|
-
return fileName === aiFileName || path.includes(aiPath.replace(/^[A-Z]?:?\\?/, ''));
|
|
850
|
+
const aiPath = extractAfterAny(filePathLine, FILE_LABELS) || '';
|
|
851
|
+
const matchedPath = context.fileList.find(p => {
|
|
852
|
+
const fileName = p.split(/[\\/]/).pop();
|
|
853
|
+
const aiFileName = aiPath.split(/[\\/]/).pop();
|
|
854
|
+
return fileName === aiFileName || p.includes(aiPath.replace(/^[A-Z]?:?\\?/, ''));
|
|
967
855
|
});
|
|
968
856
|
if (matchedPath) {
|
|
969
857
|
issue.file = matchedPath;
|
|
970
858
|
}
|
|
971
859
|
}
|
|
972
860
|
} else {
|
|
973
|
-
// 单文件模式,直接使用传入的filePath
|
|
974
861
|
issue.file = filePath;
|
|
975
862
|
}
|
|
976
|
-
|
|
863
|
+
|
|
977
864
|
let isInCodeBlock = false;
|
|
978
865
|
let codeLines = [];
|
|
979
|
-
let collectPlainSnippet = false;
|
|
980
|
-
let collectSuggestion = false;
|
|
866
|
+
let collectPlainSnippet = false;
|
|
867
|
+
let collectSuggestion = false;
|
|
981
868
|
let suggestionLines = [];
|
|
982
869
|
|
|
983
870
|
const isFieldLine = (text) => {
|
|
984
|
-
const
|
|
985
|
-
if (!
|
|
986
|
-
if (
|
|
987
|
-
if (
|
|
988
|
-
if (
|
|
989
|
-
if (
|
|
990
|
-
if (
|
|
991
|
-
if (/^\*\*-----/.test(
|
|
992
|
-
if (/^问题\d+[::]/.test(
|
|
871
|
+
const tline = String(text || '').trim();
|
|
872
|
+
if (!tline) return false;
|
|
873
|
+
if (startsWithAny(tline, FILE_LABELS)) return true;
|
|
874
|
+
if (startsWithAny(tline, RISK_LABELS)) return true;
|
|
875
|
+
if (startsWithAny(tline, REASON_LABELS)) return true;
|
|
876
|
+
if (startsWithAny(tline, SUGGEST_LABELS)) return true;
|
|
877
|
+
if (startsWithAny(tline, SNIPPET_LABELS)) return true;
|
|
878
|
+
if (/^\*\*-----/.test(tline)) return true;
|
|
879
|
+
if (/^问题\d+[::]/.test(tline) || /^Issue\s*\d+[::]?/i.test(tline)) return true;
|
|
993
880
|
return false;
|
|
994
881
|
};
|
|
995
|
-
|
|
882
|
+
|
|
996
883
|
for (let i = 0; i < lines.length; i++) {
|
|
997
884
|
const line = lines[i];
|
|
998
|
-
|
|
999
|
-
// 若正在收集非围栏代码片段,优先处理
|
|
885
|
+
|
|
1000
886
|
if (collectPlainSnippet) {
|
|
1001
887
|
if (isFieldLine(line)) {
|
|
1002
|
-
// 到达下一个字段,结束收集
|
|
1003
888
|
issue.snippet = (issue.snippet && issue.snippet.length > 0)
|
|
1004
889
|
? issue.snippet
|
|
1005
890
|
: codeLines.join('\n').trim();
|
|
1006
891
|
collectPlainSnippet = false;
|
|
1007
|
-
// 继续让本行按字段规则处理
|
|
1008
892
|
} else {
|
|
1009
893
|
codeLines.push(line);
|
|
1010
894
|
continue;
|
|
1011
895
|
}
|
|
1012
896
|
}
|
|
1013
897
|
|
|
1014
|
-
// 若正在收集修改建议的多行文本
|
|
1015
898
|
if (collectSuggestion) {
|
|
1016
899
|
if (isFieldLine(line)) {
|
|
1017
900
|
issue.suggestion = suggestionLines.join('\n').trim();
|
|
1018
901
|
collectSuggestion = false;
|
|
1019
902
|
suggestionLines = [];
|
|
1020
|
-
// 继续处理当前字段
|
|
1021
903
|
} else {
|
|
1022
904
|
suggestionLines.push(line);
|
|
1023
905
|
continue;
|
|
1024
906
|
}
|
|
1025
907
|
}
|
|
1026
908
|
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
const level = line.replace('风险等级:', '').trim();
|
|
909
|
+
if (startsWithAny(line, FILE_LABELS)) {
|
|
910
|
+
// ignore; handled above
|
|
911
|
+
} else if (startsWithAny(line, RISK_LABELS)) {
|
|
912
|
+
const level = extractAfterAny(line, RISK_LABELS) || '';
|
|
1032
913
|
issue.risk = this.mapRiskLevel(level);
|
|
1033
|
-
} else if (line
|
|
1034
|
-
issue.message = line
|
|
1035
|
-
} else if (line
|
|
1036
|
-
const rest = line
|
|
914
|
+
} else if (startsWithAny(line, REASON_LABELS)) {
|
|
915
|
+
issue.message = extractAfterAny(line, REASON_LABELS) || '';
|
|
916
|
+
} else if (startsWithAny(line, SUGGEST_LABELS)) {
|
|
917
|
+
const rest = extractAfterAny(line, SUGGEST_LABELS) || '';
|
|
1037
918
|
collectSuggestion = true;
|
|
1038
919
|
suggestionLines = rest ? [rest] : [];
|
|
1039
|
-
} else if (line
|
|
1040
|
-
const snippetContent = line
|
|
1041
|
-
// 如果代码片段行后面直接有内容,使用该内容
|
|
1042
|
-
if (snippetContent && !snippetContent.startsWith('```')) {
|
|
1043
|
-
// 改为启动“非围栏片段”模式,收集后续多行直到下一个字段
|
|
1044
|
-
collectPlainSnippet = true;
|
|
1045
|
-
codeLines = [snippetContent];
|
|
1046
|
-
} else {
|
|
1047
|
-
// 检查下一行是否是代码块开始
|
|
1048
|
-
if (i + 1 < lines.length && lines[i + 1].trim().startsWith('```')) {
|
|
1049
|
-
isInCodeBlock = true;
|
|
1050
|
-
i++; // 跳过 ``` 行
|
|
1051
|
-
codeLines = [];
|
|
1052
|
-
} else {
|
|
1053
|
-
// 启动非围栏代码片段收集,直到遇到下一个字段
|
|
1054
|
-
collectPlainSnippet = true;
|
|
1055
|
-
codeLines = [];
|
|
1056
|
-
}
|
|
1057
|
-
}
|
|
1058
|
-
}
|
|
1059
|
-
// 处理实际AI响应格式的字段(没有冒号前缀)
|
|
1060
|
-
else if (line.trim().startsWith('代码片段:')) {
|
|
1061
|
-
const snippetContent = line.replace(/^.*代码片段:/, '').trim();
|
|
920
|
+
} else if (startsWithAny(line, SNIPPET_LABELS)) {
|
|
921
|
+
const snippetContent = extractAfterAny(line, SNIPPET_LABELS) || '';
|
|
1062
922
|
if (snippetContent && !snippetContent.startsWith('```')) {
|
|
1063
|
-
// 改为启动“非围栏片段”模式,收集后续多行直到下一个字段
|
|
1064
923
|
collectPlainSnippet = true;
|
|
1065
924
|
codeLines = [snippetContent];
|
|
1066
925
|
} else {
|
|
1067
|
-
// 检查下一行是否是代码块开始
|
|
1068
926
|
if (i + 1 < lines.length && lines[i + 1].trim().startsWith('```')) {
|
|
1069
927
|
isInCodeBlock = true;
|
|
1070
|
-
i++;
|
|
928
|
+
i++;
|
|
1071
929
|
codeLines = [];
|
|
1072
930
|
} else {
|
|
1073
|
-
// 启动非围栏代码片段收集,直到遇到下一个字段
|
|
1074
931
|
collectPlainSnippet = true;
|
|
1075
932
|
codeLines = [];
|
|
1076
933
|
}
|
|
1077
934
|
}
|
|
1078
|
-
} else if (line.trim().startsWith('风险等级:')) {
|
|
1079
|
-
const level = line.replace(/^.*风险等级:/, '').trim();
|
|
1080
|
-
issue.risk = this.mapRiskLevel(level);
|
|
1081
|
-
} else if (line.trim().startsWith('风险原因:')) {
|
|
1082
|
-
issue.message = line.replace(/^.*风险原因:/, '').trim();
|
|
1083
|
-
} else if (line.trim().startsWith('修改建议:')) {
|
|
1084
|
-
const rest = line.replace(/^.*修改建议:/, '').trim();
|
|
1085
|
-
collectSuggestion = true;
|
|
1086
|
-
suggestionLines = rest ? [rest] : [];
|
|
1087
935
|
} else if (isInCodeBlock) {
|
|
1088
|
-
// 处理代码块内容
|
|
1089
936
|
if (line.trim() === '```') {
|
|
1090
|
-
// 代码块结束
|
|
1091
937
|
isInCodeBlock = false;
|
|
1092
938
|
issue.snippet = codeLines.join('\n').trim();
|
|
1093
939
|
} else {
|
|
1094
|
-
// 收集代码行
|
|
1095
940
|
codeLines.push(line);
|
|
1096
941
|
}
|
|
1097
942
|
}
|
|
1098
943
|
}
|
|
1099
944
|
|
|
1100
|
-
// 若到末尾仍在收集非围栏片段,收尾
|
|
1101
945
|
if (collectPlainSnippet && (!issue.snippet || issue.snippet.length === 0)) {
|
|
1102
946
|
issue.snippet = codeLines.join('\n').trim();
|
|
1103
947
|
}
|
|
1104
|
-
|
|
1105
|
-
// 若到末尾仍在收集多行修改建议,收尾
|
|
1106
948
|
if (collectSuggestion && (!issue.suggestion || issue.suggestion.length === 0)) {
|
|
1107
949
|
issue.suggestion = suggestionLines.join('\n').trim();
|
|
1108
950
|
}
|
|
1109
951
|
|
|
1110
|
-
// 回退:如果未找到“代码片段:”字段但块内存在代码块,则提取第一个代码块内容作为片段
|
|
1111
952
|
if (!issue.snippet) {
|
|
1112
953
|
const fenceMatch = block.match(/```([\s\S]*?)```/);
|
|
1113
954
|
if (fenceMatch && fenceMatch[1]) {
|
|
@@ -1115,12 +956,10 @@ ${segment.content}
|
|
|
1115
956
|
}
|
|
1116
957
|
}
|
|
1117
958
|
|
|
1118
|
-
// 片段规范化:移除“中间省略”占位行,裁剪到合理长度,并避免跨越不相邻的行号簇
|
|
1119
959
|
if (issue.snippet && typeof issue.snippet === 'string') {
|
|
1120
960
|
issue.snippet = this.normalizeSnippet(issue.snippet);
|
|
1121
961
|
}
|
|
1122
962
|
|
|
1123
|
-
// 从代码片段中提取行号范围(支持每行的`[n]`前缀;兼容可选的'+'或空格前缀)
|
|
1124
963
|
if (issue.snippet && typeof issue.snippet === 'string') {
|
|
1125
964
|
let startNum = null;
|
|
1126
965
|
let endNum = null;
|
|
@@ -1141,22 +980,11 @@ ${segment.content}
|
|
|
1141
980
|
issue.line = issue.lineStart; // 兼容旧字段
|
|
1142
981
|
}
|
|
1143
982
|
}
|
|
1144
|
-
|
|
1145
|
-
// 必须有原因才认为是有效问题
|
|
983
|
+
|
|
1146
984
|
if (issue.message) {
|
|
1147
|
-
|
|
1148
|
-
if (!issue.
|
|
1149
|
-
issue.file = filePath;
|
|
1150
|
-
}
|
|
1151
|
-
|
|
1152
|
-
// 确保所有问题都有风险等级,如果没有则设为默认值
|
|
1153
|
-
if (!issue.risk) {
|
|
1154
|
-
issue.risk = 'suggestion'; // 默认为建议等级
|
|
1155
|
-
}
|
|
1156
|
-
|
|
1157
|
-
// 规范化风险等级:确保在有效范围内
|
|
985
|
+
if (!issue.file) issue.file = filePath;
|
|
986
|
+
if (!issue.risk) issue.risk = 'suggestion';
|
|
1158
987
|
issue.risk = this.normalizeRiskLevel(issue.risk);
|
|
1159
|
-
|
|
1160
988
|
return issue;
|
|
1161
989
|
}
|
|
1162
990
|
|
|
@@ -1220,15 +1048,30 @@ ${segment.content}
|
|
|
1220
1048
|
}
|
|
1221
1049
|
|
|
1222
1050
|
mapRiskLevel(chineseLevel) {
|
|
1051
|
+
const raw = String(chineseLevel || '').trim().toLowerCase();
|
|
1223
1052
|
const mapping = {
|
|
1224
1053
|
'致命': 'critical',
|
|
1225
1054
|
'高危': 'high',
|
|
1226
1055
|
'中危': 'medium',
|
|
1227
1056
|
'低危': 'low',
|
|
1228
|
-
'建议': 'suggestion'
|
|
1057
|
+
'建议': 'suggestion',
|
|
1058
|
+
'critical': 'critical',
|
|
1059
|
+
'high': 'high',
|
|
1060
|
+
'medium': 'medium',
|
|
1061
|
+
'low': 'low',
|
|
1062
|
+
'suggestion': 'suggestion',
|
|
1063
|
+
'fatal': 'critical',
|
|
1064
|
+
'blocker': 'critical',
|
|
1065
|
+
'severe': 'high',
|
|
1066
|
+
'major': 'high',
|
|
1067
|
+
'moderate': 'medium',
|
|
1068
|
+
'minor': 'low',
|
|
1069
|
+
'info': 'suggestion',
|
|
1070
|
+
'tip': 'suggestion',
|
|
1071
|
+
'advice': 'suggestion',
|
|
1072
|
+
'recommendation': 'suggestion'
|
|
1229
1073
|
};
|
|
1230
|
-
|
|
1231
|
-
return mapping[chineseLevel] || 'suggestion';
|
|
1074
|
+
return mapping[raw] || 'suggestion';
|
|
1232
1075
|
}
|
|
1233
1076
|
|
|
1234
1077
|
normalizeRiskLevel(riskLevel) {
|
|
@@ -1302,19 +1145,23 @@ ${segment.content}
|
|
|
1302
1145
|
|
|
1303
1146
|
try {
|
|
1304
1147
|
// 记录请求信息
|
|
1305
|
-
logger.debug(
|
|
1148
|
+
logger.debug(t(this.config, 'chunk_req_info_dbg', {
|
|
1149
|
+
model: this.config.model ?? 'gpt-3.5-turbo',
|
|
1150
|
+
count: collector.messages.length
|
|
1151
|
+
}));
|
|
1306
1152
|
// 输出请求消息的预览(限制每条消息长度)
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1153
|
+
try {
|
|
1154
|
+
const preview = collector.messages.map((m, idx) => {
|
|
1155
|
+
const text = String(m.content ?? '');
|
|
1156
|
+
const maxLen = 1500;
|
|
1157
|
+
const truncatedSuffix = t(this.config, 'preview_truncated_suffix', { count: text.length - maxLen });
|
|
1158
|
+
const cut = text.length > maxLen ? `${text.slice(0, maxLen)}\n${truncatedSuffix}` : text;
|
|
1159
|
+
return `#${idx + 1} [${m.role}]\n${cut}`;
|
|
1160
|
+
}).join('\n---\n');
|
|
1161
|
+
logger.debug(t(this.config, 'chunk_req_preview_dbg', { preview }));
|
|
1162
|
+
} catch (e) {
|
|
1163
|
+
logger.debug(t(this.config, 'chunk_req_preview_fail_dbg', { error: e.message }));
|
|
1164
|
+
}
|
|
1318
1165
|
|
|
1319
1166
|
// 发送请求
|
|
1320
1167
|
const isFirstCall = this.chunkedResponseCollector.get(requestId).chunks.length === 0;
|
|
@@ -1326,7 +1173,7 @@ ${segment.content}
|
|
|
1326
1173
|
}, isFirstCall ? meta : null);
|
|
1327
1174
|
|
|
1328
1175
|
const content = response.choices[0].message.content;
|
|
1329
|
-
logger.debug(
|
|
1176
|
+
logger.debug(t(this.config, 'ai_response_len_dbg', { len: content.length }));
|
|
1330
1177
|
// 检查是否是分段响应
|
|
1331
1178
|
if (this.isChunkedResponse(content)) {
|
|
1332
1179
|
const chunkInfo = this.parseChunkInfo(content);
|
|
@@ -1349,9 +1196,9 @@ ${segment.content}
|
|
|
1349
1196
|
collector.messages.push({ role: 'assistant', content: content });
|
|
1350
1197
|
collector.messages.push({
|
|
1351
1198
|
role: 'user',
|
|
1352
|
-
content: '
|
|
1199
|
+
content: t(this.config, 'chunk_continue_prompt')
|
|
1353
1200
|
});
|
|
1354
|
-
logger.debug('
|
|
1201
|
+
logger.debug(t(this.config, 'chunk_continue_needed_dbg'));
|
|
1355
1202
|
|
|
1356
1203
|
// 递归获取下一段
|
|
1357
1204
|
return await this.handleChunkedResponse(null, requestId, null);
|