From 8e4ea97dacffe848fdff97c3266b054ab4d68ab1 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 22 Jun 2026 09:05:29 +0000 Subject: [PATCH] =?UTF-8?q?feat(ai-review=20comment):=20=E9=9B=86=E4=B8=AD?= =?UTF-8?q?=E6=9B=B4=E6=96=B0=E5=96=AE=E4=B8=80=E5=AF=A9=E6=9F=A5=E7=95=99?= =?UTF-8?q?=E8=A8=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/comments.js | 12 +++++++----- app/gitea.js | 9 +++++++++ app/main.js | 25 +++++++++++++++++++------ 3 files changed, 35 insertions(+), 11 deletions(-) diff --git a/app/comments.js b/app/comments.js index 94a6210..a0e99bc 100644 --- a/app/comments.js +++ b/app/comments.js @@ -55,28 +55,30 @@ export function saveFindings(workspace, findings, mirrorDir = null) { /** * 發布所有舊問題 comment(一次發布,依等級排序) */ -export async function postOldFindingsComment(findings) { +export async function postOldFindingsComment(findings, deps = {}) { + const { postIssue = postComment } = deps; 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); + await postIssue(body); ok(`舊問題 comment 發布 (${old.length} 筆)`); } /** * 發布新問題中非 critical 的 comment(一次發布) */ -export async function postNewNonCriticalComment(findings) { +export async function postNewNonCriticalComment(findings, deps = {}) { + const { postIssue = postComment } = deps; 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); + await postIssue(body); ok(`新問題(非嚴重)comment 發布 (${items.length} 筆)`); } @@ -94,7 +96,7 @@ export async function postNewCriticalComments(findings, deps = {}) { } for (const f of criticals) { const loc = parseLocation(f.location); - if (loc) { + if (postInline && loc) { try { await postInline({ path: loc.file, line: loc.line, body: inlineCommentBody(f) }); ok(`嚴重問題 行內 comment 發布: [${f.role}] ${loc.file}:${loc.line}`); diff --git a/app/gitea.js b/app/gitea.js index 7aeffed..76e7873 100644 --- a/app/gitea.js +++ b/app/gitea.js @@ -118,6 +118,15 @@ export async function postComment(body) { return resp.data; } +export async function updateComment(commentId, body) { + const resp = await axios.patch( + api(`/repos/${GITEA_REPOSITORY}/issues/comments/${commentId}`), + { body }, + { headers: headers(GITEA_COMMENT_TOKEN || GITEA_TOKEN), timeout: 30000, httpsAgent }, + ); + return resp.data; +} + /** * 在 PR 指定檔案的指定行數發布行內 review comment(標註程式碼位置)。 * 透過 Gitea 的 pull reviews API,以 new_position 對應新版檔案的行號。 diff --git a/app/main.js b/app/main.js index b793148..c7c0e80 100644 --- a/app/main.js +++ b/app/main.js @@ -1,7 +1,7 @@ import path from 'path'; import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConfig, FINDINGS_PATH, EXCLUSIONS_PATH } from './config.js'; import { loadRoles, getRoleIntro } from './roles.js'; -import { getPRDiff, postComment, getCommitMessageBySha, getBotReviewOutcome, shouldSkipBotCommit } from './gitea.js'; +import { getPRDiff, postComment, updateComment, getCommitMessageBySha, getBotReviewOutcome, shouldSkipBotCommit } from './gitea.js'; import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js'; import { saveFindings, postOldFindingsComment, postNewNonCriticalComment, postNewCriticalComments } from './comments.js'; import { cloneRepo, commitAndPush, getRepoState } from './git.js'; @@ -63,9 +63,22 @@ async function main() { process.exit(0); } + let reviewComment = null; + let reviewCommentBody = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`; + const appendReviewComment = async (body) => { + if (!body) return null; + if (!reviewComment?.id) { + reviewCommentBody = body; + reviewComment = await postComment(reviewCommentBody); + return reviewComment; + } + reviewCommentBody += `\n\n---\n\n${body}`; + reviewComment = await updateComment(reviewComment.id, reviewCommentBody); + return reviewComment; + }; + try { - const intro = getRoleIntro(roles) + `\n\n> 🔍 服務:${provider} 模型:${model}`; - await postComment(intro); + reviewComment = await postComment(reviewCommentBody); ok('角色介紹 comment 發布成功'); } catch (e) { warn(`comment 發布失敗(繼續執行): ${e.message}`); @@ -111,9 +124,9 @@ async function main() { const reviewDir = repoDir || WORKSPACE; saveFindings(WORKSPACE, filtered, reviewDir); try { - await postOldFindingsComment(filtered); - await postNewNonCriticalComment(filtered); - await postNewCriticalComments(filtered); + await postOldFindingsComment(filtered, { postIssue: appendReviewComment }); + await postNewNonCriticalComment(filtered, { postIssue: appendReviewComment }); + await postNewCriticalComments(filtered, { postInline: null, postIssue: appendReviewComment }); ok('Step5 完成'); } catch (e) { warn(`comment 發布失敗(繼續執行): ${e.message}`);