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 5bee498e4b - Show all commits
+31 -13
View File
@@ -49,6 +49,7 @@ export function groupConversations(comments) {
const groups = new Map(); const groups = new Map();
for (const c of comments || []) { for (const c of comments || []) {
const filePath = typeof c?.path === 'string' ? c.path : ''; const filePath = typeof c?.path === 'string' ? c.path : '';
if (!filePath) continue; // 無檔案路徑的留言無法定位,跳過以免併入共用群組
const lineNum = Number(c?.position) || Number(c?.original_position) || 0; const lineNum = Number(c?.position) || Number(c?.original_position) || 0;
const key = `${filePath}|${lineNum}`; const key = `${filePath}|${lineNum}`;
if (!groups.has(key)) { if (!groups.has(key)) {
@@ -129,10 +130,17 @@ export async function reconcileConversations(deps = {}) {
line(`對話收斂: 對話總數=${conversations.length} 已解決/不可處理=${alreadyResolved} 待判斷=${open.length}`); line(`對話收斂: 對話總數=${conversations.length} 已解決/不可處理=${alreadyResolved} 待判斷=${open.length}`);
if (open.length === 0) return { ...EMPTY }; if (open.length === 0) return { ...EMPTY };
// 並行取得各檔案最新內容;單一檔案失敗時視為空字串,不中斷整體流程
const fileCache = new Map(); const fileCache = new Map();
for (const filePath of [...new Set(open.map(c => c.path).filter(Boolean))]) { const filePaths = [...new Set(open.map(c => c.path).filter(Boolean))];
fileCache.set(filePath, await getFileContent(filePath)); await Promise.all(filePaths.map(async (filePath) => {
} try {
fileCache.set(filePath, await getFileContent(filePath));
} catch (e) {
warn(`取得檔案內容失敗(視為空): ${filePath} error=${e.message}`);
fileCache.set(filePath, '');
}
}));
const items = open.map((c, idx) => ({ const items = open.map((c, idx) => ({
idx, idx,
@@ -153,19 +161,29 @@ export async function reconcileConversations(deps = {}) {
const resolvedFindings = []; const resolvedFindings = [];
const carriedFindings = []; const carriedFindings = [];
// 並行 resolve 所有 AI 判定已解決的對話(allSettled:個別失敗不中斷其他)
const resolveTargets = open
.map((c, i) => ({ c, i }))
.filter(({ i }) => resolvedSet.has(i));
const settled = await Promise.allSettled(
resolveTargets.map(({ c }) => resolveComment(c.commentIds[0])),
);
const resolveOutcome = new Map();
resolveTargets.forEach(({ i }, j) => resolveOutcome.set(i, settled[j]));
let resolvedCount = 0; let resolvedCount = 0;
for (let i = 0; i < open.length; i++) { for (let i = 0; i < open.length; i++) {
const c = open[i]; const c = open[i];
if (resolvedSet.has(i)) { const outcome = resolveOutcome.get(i);
try { if (outcome?.status === 'fulfilled') {
await resolveComment(c.commentIds[0]); resolvedCount += 1;
resolvedCount += 1; if (c.botFinding) resolvedFindings.push({ ...c.botFinding, is_new: false });
if (c.botFinding) resolvedFindings.push({ ...c.botFinding, is_new: false }); ok(`對話已解決並 resolve: ${c.path}:${c.line}`);
ok(`對話已解決並 resolve: ${c.path}:${c.line}`); continue;
continue; }
} catch (e) { if (outcome?.status === 'rejected') {
warn(`resolve 對話失敗(保留為未解決): ${c.path}:${c.line} error=${e.message}`); warn(`resolve 對話失敗(保留為未解決): ${c.path}:${c.line} error=${outcome.reason?.message}`);
}
} }
pushCarried(carriedFindings, c); pushCarried(carriedFindings, c);
} }