import fs from 'fs'; import path from 'path'; import { postComment, postPullReviewComment } 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: '建議' }; 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(); /** * 解析 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}`; } /** * 寫入 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}`); } }