feat(ai-review 行號): 角色 prompt 強制行號,缺行號時反問原角色依 diff 定位(重試上限)
This commit is contained in:
+60
-1
@@ -1,7 +1,7 @@
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
import { chatJSON } from './llm.js';
|
||||
import { buildAnalysisPrompt, loadRole, buildVerdictPrompt } from './roles.js';
|
||||
import { buildAnalysisPrompt, loadRole, buildVerdictPrompt, buildLocateLinePrompt } from './roles.js';
|
||||
import { FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js';
|
||||
import { line, ok, warn } from './log.js';
|
||||
|
||||
@@ -244,6 +244,65 @@ function fallback(label, findings, e) {
|
||||
return findings;
|
||||
}
|
||||
|
||||
const MAX_LOCATE_ATTEMPTS = 3;
|
||||
|
||||
/** 從 location 取出行號;無 `檔案:行號`(或多檔逗號)時回 null。 */
|
||||
function findingLine(location) {
|
||||
const s = String(location || '').trim();
|
||||
if (!s || s.includes(',')) return null;
|
||||
const m = /^(.+?):(\d+)(?:-\d+)?$/.exec(s);
|
||||
return m ? Number(m[2]) : null;
|
||||
}
|
||||
|
||||
/** 從整份 unified diff 擷取指定檔案的區段,找不到時回退整份 diff。 */
|
||||
function extractFileDiff(diff, file) {
|
||||
const lines = String(diff || '').split('\n');
|
||||
const out = [];
|
||||
let capturing = false;
|
||||
for (const l of lines) {
|
||||
if (l.startsWith('diff --git ')) capturing = l.includes(`b/${file}`) || l.includes(`a/${file}`);
|
||||
if (capturing) out.push(l);
|
||||
}
|
||||
return out.length ? out.join('\n') : String(diff || '');
|
||||
}
|
||||
|
||||
/**
|
||||
* 對「只有檔名、缺行號」的 findings,反問原角色依該檔 diff 找出行號,
|
||||
* 重複嘗試直到取得有效行號(每條最多 maxAttempts 次,避免無限迴圈);
|
||||
* 成功則把 location 補成 `檔案:行號`,否則保留原檔名。
|
||||
*/
|
||||
export async function resolveMissingLineNumbers(findings, diff, deps = {}) {
|
||||
const { chatFn = chatJSON, getRole = loadRole, maxAttempts = MAX_LOCATE_ATTEMPTS } = deps;
|
||||
let resolved = 0;
|
||||
let pending = 0;
|
||||
for (const f of findings) {
|
||||
if (findingLine(f.location) != null) continue; // 已有行號
|
||||
const file = String(f.location || '').split(',')[0].split(':')[0].trim();
|
||||
if (!file) continue;
|
||||
pending += 1;
|
||||
const systemPrompt = buildLocateLinePrompt(getRole(f.role) || { name: f.role });
|
||||
const userContent = `${JSON.stringify({ file, problem: f.problem, suggestion: f.suggestion })}\n\n--- ${file} Git Diff ---\n${extractFileDiff(diff, file)}`;
|
||||
let located = null;
|
||||
for (let attempt = 1; attempt <= maxAttempts && located == null; attempt++) {
|
||||
try {
|
||||
const res = await chatFn(systemPrompt, userContent);
|
||||
const ln = Number(res?.line);
|
||||
if (Number.isInteger(ln) && ln > 0) located = ln;
|
||||
} catch (e) {
|
||||
warn(`[${f.role}] 行號定位失敗(第 ${attempt}/${maxAttempts} 次): ${e.message}`);
|
||||
}
|
||||
}
|
||||
if (located != null) {
|
||||
f.location = `${file}:${located}`;
|
||||
resolved += 1;
|
||||
} else {
|
||||
warn(`[${f.role}] ${maxAttempts} 次嘗試後仍無法定位行號,保留檔名: ${file}`);
|
||||
}
|
||||
}
|
||||
if (pending > 0) ok(`補行號: ${resolved}/${pending} 筆成功定位`);
|
||||
return findings;
|
||||
}
|
||||
|
||||
/** 只保留 AI 需要的欄位,減少 token 用量 */
|
||||
function toAIPayload(findings) {
|
||||
return findings.map(({ level, role, location, problem, suggestion }) => ({ level, role, location, problem, suggestion }));
|
||||
|
||||
Reference in New Issue
Block a user