175 lines
6.3 KiB
JavaScript
175 lines
6.3 KiB
JavaScript
import fs from 'fs';
|
||
import path from 'path';
|
||
import { postComment, postPullReviewComment, postPullReview } from './gitea.js';
|
||
import { FINDINGS_PATH } from './config.js';
|
||
import { ok, line, warn } from './log.js';
|
||
|
||
const LEVEL_EMOJI = { critical: '🔴', warning: '🟡', info: '🔵' };
|
||
const LEVEL_LABEL = { critical: '嚴重', warning: '警告', info: '建議' };
|
||
const LEVEL_ORDER = ['critical', 'warning', 'info'];
|
||
|
||
function findingRow(f) {
|
||
return `| ${LEVEL_EMOJI[f.level] || ''} ${LEVEL_LABEL[f.level] || f.level} | ${f.role} | ${f.location} | ${f.suggestion} |`;
|
||
}
|
||
|
||
function buildTable(findings) {
|
||
const rows = findings.map(findingRow).join('\n');
|
||
return `| 等級 | 審查員 | 位置 | 建議 |\n|------|--------|------|------|\n${rows}`;
|
||
}
|
||
|
||
const levelText = f => `${LEVEL_EMOJI[f.level] || ''} ${LEVEL_LABEL[f.level] || f.level}`.trim();
|
||
const bySeverity = (a, b) => {
|
||
const aLevel = LEVEL_ORDER.includes(a.level) ? LEVEL_ORDER.indexOf(a.level) : LEVEL_ORDER.length;
|
||
const bLevel = LEVEL_ORDER.includes(b.level) ? LEVEL_ORDER.indexOf(b.level) : LEVEL_ORDER.length;
|
||
if (aLevel !== bLevel) return aLevel - bLevel;
|
||
return String(a.location || '').localeCompare(String(b.location || ''));
|
||
};
|
||
|
||
/**
|
||
* 解析 finding 的 location 取出檔案與行號,供行內 comment 標註使用。
|
||
* 支援 "file:19" 與 "file:70-82"(取起始行);無行號或含多個檔案(逗號)時回傳 null。
|
||
*/
|
||
export function parseLocation(location) {
|
||
if (typeof location !== 'string') return null;
|
||
const trimmed = location.trim();
|
||
if (trimmed.includes(',')) return null;
|
||
const match = trimmed.match(/^(.+?):(\d+)(?:-\d+)?$/);
|
||
if (!match) return null;
|
||
return { file: match[1], line: Number(match[2]) };
|
||
}
|
||
|
||
/** 行內 comment 內容:等級/審查員/建議 */
|
||
function inlineCommentBody(f) {
|
||
return `**等級**:${levelText(f)}\n**審查員**:${f.role}\n**建議**:${f.suggestion}`;
|
||
}
|
||
|
||
function problemText(f) {
|
||
return f.problem || f.reason || f.description || f.detail || f.title || f.message || '未提供問題原因';
|
||
}
|
||
|
||
function reviewCommentBody(f) {
|
||
return [
|
||
`**嚴重等級**:${levelText(f)}`,
|
||
`**審查員**:${f.role}`,
|
||
`**問題**:${problemText(f)}`,
|
||
`**建議**:${f.suggestion}`,
|
||
].join('\n');
|
||
}
|
||
|
||
function countBy(findings, predicate) {
|
||
return findings.filter(predicate).length;
|
||
}
|
||
|
||
function buildReviewSummary(findings) {
|
||
const criticalCount = countBy(findings, f => f.level === 'critical');
|
||
const warningCount = countBy(findings, f => f.level === 'warning');
|
||
const infoCount = countBy(findings, f => f.level === 'info');
|
||
return [
|
||
'## AI Code Review 統計',
|
||
'',
|
||
'| 🔴 嚴重 | 🟡 警告 | 🔵 建議 |',
|
||
'| --- | --- | --- |',
|
||
`| ${criticalCount} 筆 | ${warningCount} 筆 | ${infoCount} 筆 |`,
|
||
].join('\n');
|
||
}
|
||
|
||
function toReviewComment(f) {
|
||
const loc = parseLocation(f.location);
|
||
if (!loc) return null;
|
||
return {
|
||
path: loc.file,
|
||
body: reviewCommentBody(f),
|
||
new_position: loc.line,
|
||
};
|
||
}
|
||
|
||
/**
|
||
* 發布單一 Gitea review:
|
||
* - summaryFindings 只用來統計本文數字
|
||
* - commentFindings 用來產生 review comments,並依嚴重等級排序
|
||
*/
|
||
export async function postFindingsReview(findings, deps = {}) {
|
||
const {
|
||
postReview = postPullReview,
|
||
summaryFindings = findings,
|
||
commentFindings = findings,
|
||
} = deps;
|
||
const sortedComments = [...commentFindings].sort(bySeverity);
|
||
const comments = sortedComments.map(toReviewComment).filter(Boolean);
|
||
const body = buildReviewSummary(summaryFindings);
|
||
await postReview({ body, comments });
|
||
ok(`review 發布: summary=${summaryFindings.length} total=${sortedComments.length} commentable=${comments.length}`);
|
||
}
|
||
|
||
/**
|
||
* 寫入 findings.json。
|
||
* 預設寫到 workspace;若提供 mirrorDir,則同步寫入另一份供 repo commit 使用。
|
||
*/
|
||
export function saveFindings(workspace, findings, mirrorDir = null) {
|
||
const targets = [workspace];
|
||
if (mirrorDir && mirrorDir !== workspace) targets.push(mirrorDir);
|
||
|
||
for (const targetDir of targets) {
|
||
const fullPath = path.join(targetDir, FINDINGS_PATH);
|
||
fs.mkdirSync(path.dirname(fullPath), { recursive: true });
|
||
fs.writeFileSync(fullPath, JSON.stringify(findings, null, 2) + '\n', 'utf8');
|
||
ok(`findings 寫入: ${fullPath} (${findings.length} 筆)`);
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 發布所有舊問題 comment(一次發布,依等級排序)
|
||
*/
|
||
export async function postOldFindingsComment(findings) {
|
||
const old = findings.filter(f => !f.is_new);
|
||
if (old.length === 0) {
|
||
line('無舊問題,跳過');
|
||
return;
|
||
}
|
||
const body = `## 📋 舊有未解決問題(${old.length} 筆)\n\n${buildTable(old)}`;
|
||
await postComment(body);
|
||
ok(`舊問題 comment 發布 (${old.length} 筆)`);
|
||
}
|
||
|
||
/**
|
||
* 發布新問題中非 critical 的 comment(一次發布)
|
||
*/
|
||
export async function postNewNonCriticalComment(findings) {
|
||
const items = findings.filter(f => f.is_new && f.level !== 'critical');
|
||
if (items.length === 0) {
|
||
line('無新的非嚴重問題,跳過');
|
||
return;
|
||
}
|
||
const body = `## 🔍 新發現問題(${items.length} 筆)\n\n${buildTable(items)}`;
|
||
await postComment(body);
|
||
ok(`新問題(非嚴重)comment 發布 (${items.length} 筆)`);
|
||
}
|
||
|
||
/**
|
||
* 每個新 critical 問題各發一個 comment。
|
||
* 優先用 Gitea 行內 review comment 標註問題檔案與行數(內容為等級/審查員/建議);
|
||
* 若 location 無法解析出行號,或行內發布失敗(例如該行不在 diff 範圍),則降級為一般 comment。
|
||
*/
|
||
export async function postNewCriticalComments(findings, deps = {}) {
|
||
const { postInline = postPullReviewComment, postIssue = postComment } = deps;
|
||
const criticals = findings.filter(f => f.is_new && f.level === 'critical');
|
||
if (criticals.length === 0) {
|
||
line('無新的嚴重問題,跳過');
|
||
return;
|
||
}
|
||
for (const f of criticals) {
|
||
const loc = parseLocation(f.location);
|
||
if (loc) {
|
||
try {
|
||
await postInline({ path: loc.file, line: loc.line, body: inlineCommentBody(f) });
|
||
ok(`嚴重問題 行內 comment 發布: [${f.role}] ${loc.file}:${loc.line}`);
|
||
continue;
|
||
} catch (e) {
|
||
warn(`行內 comment 發布失敗,改用一般 comment: [${f.role}] ${f.location} error=${e.message}`);
|
||
}
|
||
}
|
||
await postIssue(`## 🚨 嚴重問題\n\n${buildTable([f])}`);
|
||
ok(`嚴重問題 comment 發布: [${f.role}] ${f.location}`);
|
||
}
|
||
}
|