feat(ai-review 對話收斂): 讀 PR review 留言判斷解決狀態並收斂 findings #45

Merged
admin merged 69 commits from develop into master 2026-06-23 08:30:28 +00:00
Showing only changes of commit 0cb6ffde3e - Show all commits
+25 -18
View File
@@ -150,9 +150,10 @@ function isSafeRepoPath(p) {
}
/**
* 對話收斂主流程:取得 PR 所有行內 review comment、收斂成對話、跳過已 resolve 的
* 對所有「未解決」對話一律呼叫 Gitea resolve API 關閉findings.json 為唯一待辦來源),
* 再取最新程式碼交 AI 判斷每個對話的狀態並決定其在 findings 的去向:
* 對話收斂主流程:取得 PR 所有行內 review comment
* 先把**每一個未解決的 comment**(依 comment id 去重,含無 pathposition 者)一律呼叫 Gitea resolve API 關閉
* findings.json 為唯一待辦來源,下次 review 依其重貼 comment);
* 再以「檔案路徑+行號」收斂成對話、取最新程式碼交 AI 判斷,決定每個對話在 findings 的去向:
* - 'resolved'(程式碼已修復)→ 從舊問題移除(resolvedFindings);
* - 'false_positive'(誤報)→ 寫入 exclusions 並從舊問題移除(excludedFindings);
* - 'open'(仍成立)→ 加入舊問題集合(carriedFindings)。
@@ -177,8 +178,26 @@ export async function reconcileConversations(deps = {}) {
const conversations = groupConversations(comments);
const open = conversations.filter(c => !c.resolved && c.commentIds.length > 0);
const alreadyResolved = conversations.length - open.length;
line(`對話收斂: 對話總數=${conversations.length} 已解決/不可處理=${alreadyResolved} 待處理=${open.length}`);
if (open.length === 0) return { ...EMPTY };
// 要關閉的 comment:有 id 且尚未被 resolve(不依賴 path|line 分組,確保每個獨立 thread 都關到,含無 pathposition 者)
const unresolvedCommentIds = [...new Set(
(comments || []).filter(c => c?.id != null && !c?.resolver).map(c => c.id),
)];
line(`對話收斂: 對話總數=${conversations.length} 已解決/不可處理=${alreadyResolved} 待判斷=${open.length} 待關閉 comment=${unresolvedCommentIds.length}`);
// 關閉所有未解決 commentallSettled:個別失敗不中斷其他)
const settled = await Promise.allSettled(unresolvedCommentIds.map(id => resolveComment(id)));
let closedCount = 0;
settled.forEach((s, i) => {
if (s.status === 'fulfilled') closedCount += 1;
else warn(`resolve comment 失敗: id=${unresolvedCommentIds[i]} error=${s.reason?.message}`);
});
if (unresolvedCommentIds.length > 0) ok(`已關閉 ${closedCount}/${unresolvedCommentIds.length} 個未解決 comment`);
if (open.length === 0) {
ok(`對話收斂完成: 關閉 comment=${closedCount} 已修復=0 誤報=0 仍成立=0`);
return { ...EMPTY, closedCount };
}
// 並行取得各檔案最新內容;單一檔案失敗時視為空字串,不中斷整體流程
const fileCache = new Map();
@@ -214,18 +233,6 @@ export async function reconcileConversations(deps = {}) {
}
const verdictByIdx = new Map(verdicts.map(v => [v.idx, v.verdict]));
// 全部先關閉:對所有未解決對話一律呼叫 resolve APIallSettled:個別失敗不中斷其他)
const settled = await Promise.allSettled(open.map(c => resolveComment(c.commentIds[0])));
let closedCount = 0;
open.forEach((c, i) => {
if (settled[i].status === 'fulfilled') {
closedCount += 1;
ok(`對話已關閉: ${c.path}:${c.line}`);
} else {
warn(`resolve 對話失敗: ${c.path}:${c.line} error=${settled[i].reason?.message}`);
}
});
// 依 AI 判斷決定每個對話在 findings 的去向
const resolvedFindings = []; // 已修復 → 從舊問題移除
const excludedFindings = []; // 誤報 → 寫入 exclusions 並從舊問題移除
@@ -248,7 +255,7 @@ export async function reconcileConversations(deps = {}) {
}
}
ok(`對話收斂完成: 關閉對話=${closedCount}/${open.length} 已修復=${resolvedCount} 誤報=${falsePositiveCount} 仍成立=${openCount}`);
ok(`對話收斂完成: 關閉 comment=${closedCount}/${unresolvedCommentIds.length} 已修復=${resolvedCount} 誤報=${falsePositiveCount} 仍成立=${openCount}`);
return {
resolvedFindings, excludedFindings, carriedFindings,
resolvedCount, falsePositiveCount, openCount, closedCount,