import fs from 'fs'; import path from 'path'; import { chatJSON } from './llm.js'; import { FINDINGS_PATH } from './config.js'; const LEVELS = ['critical', 'warning', 'info']; /** * 用單一角色分析 diff,回傳 findings 陣列 */ export async function analyzeWithRole(role, diff) { console.log(` [${role.name}] 開始分析...`); const findings = await chatJSON(role.system_prompt, `以下是 Git Diff 內容:\n\n${diff}`); // 確保每筆都有必要欄位,並標記為新問題 const valid = findings.filter(f => f.level && f.role && f.location && f.suggestion) .map(f => ({ ...f, is_new: true })); console.log(` [${role.name}] 找到 ${valid.length} 個問題`); return valid; } /** * 讀取舊 findings(從 workspace 的 FINDINGS_PATH) */ export function loadOldFindings(workspace) { const fullPath = path.join(workspace, FINDINGS_PATH); if (!fs.existsSync(fullPath)) { console.log(' 舊 findings 檔案不存在,視為空'); return []; } try { const data = JSON.parse(fs.readFileSync(fullPath, 'utf8')); const old = (Array.isArray(data) ? data : []).map(f => ({ ...f, is_new: false })); console.log(` 讀取舊 findings: ${old.length} 筆`); return old; } catch (e) { console.log(` ⚠️ 讀取舊 findings 失敗: ${e.message},視為空`); return []; } } /** * 合併新舊 findings,以 (role + location + suggestion前50字) 為 key 去除重複 * 舊問題保留,新問題若與舊問題重複則捨棄 */ export function mergeFindings(oldFindings, newFindings) { const key = f => `${f.role}|${f.location}|${String(f.suggestion).slice(0, 50)}`; const seen = new Set(oldFindings.map(key)); const deduped = newFindings.filter(f => { if (seen.has(key(f))) return false; seen.add(key(f)); return true; }); const merged = [...oldFindings, ...deduped]; console.log(` 合併結果: 舊=${oldFindings.length} 新(去重後)=${deduped.length} 總計=${merged.length}`); return merged; } /** * 依等級排序(critical > warning > info) */ export function sortByLevel(findings) { return [...findings].sort((a, b) => LEVELS.indexOf(a.level) - LEVELS.indexOf(b.level)); } /** * 呼叫 LLM 進行語意去重,回傳去重後的 findings * 失敗時降級回傳原始 findings */ export async function deduplicateWithAI(findings) { if (findings.length === 0) return findings; const systemPrompt = `你是一位程式碼審查問題去重專家。 給你一份問題清單(JSON 陣列),請移除語意重複的問題(即使描述文字不同,但指的是同一個問題)。 保留等級較高的版本,優先保留 critical > warning > info。 只回傳去重後的 JSON 陣列,不要有其他文字。`; const userContent = `以下是問題清單,請去除語意重複的項目:\n\n${JSON.stringify(findings, null, 2)}`; try { const result = await chatJSON(systemPrompt, userContent); if (Array.isArray(result) && result.length > 0) { console.log(` AI 去重: ${findings.length} -> ${result.length} 筆`); return result; } throw new Error('AI 回傳空陣列'); } catch (e) { const status = e.response?.status; if (status === 402 || status === 429) { console.log(` ⚠️ AI 去重失敗(${status} 額度/限流),降級:保留所有問題`); } else { console.log(` ⚠️ AI 去重失敗(${e.message}),降級:保留所有問題`); } return findings; } }