feat(ai-review 行號): 角色 prompt 強制行號,缺行號時反問原角色依 diff 定位(重試上限)

This commit is contained in:
Jeffery
2026-06-23 15:36:35 +08:00
parent 942721009e
commit fbf83f1cbb
3 changed files with 83 additions and 3 deletions
+60 -1
View File
@@ -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 }));