diff --git a/app/resolve.js b/app/resolve.js index 9920e46..5ac1b68 100644 --- a/app/resolve.js +++ b/app/resolve.js @@ -150,9 +150,10 @@ function isSafeRepoPath(p) { } /** - * 對話收斂主流程:取得 PR 所有行內 review comment、收斂成對話、跳過已 resolve 的, - * 對所有「未解決」對話一律呼叫 Gitea resolve API 關閉(findings.json 為唯一待辦來源), - * 再取最新程式碼交 AI 判斷每個對話的狀態並決定其在 findings 的去向: + * 對話收斂主流程:取得 PR 所有行內 review comment, + * 先把**每一個未解決的 comment**(依 comment id 去重,含無 path/position 者)一律呼叫 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 都關到,含無 path/position 者) + 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}`); + + // 關閉所有未解決 comment(allSettled:個別失敗不中斷其他) + 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 API(allSettled:個別失敗不中斷其他) - 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,