From 8662e8ca801c3dbf7f74ed7e758a7a55176735b8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 09:46:12 +0800 Subject: [PATCH] =?UTF-8?q?docs(=E5=AF=A9=E6=9F=A5=E6=AD=A5=E9=A9=9F?= =?UTF-8?q?=E7=B7=A8=E8=99=9F):=20=E4=BE=9D=E6=96=B0=E9=A0=86=E5=BA=8F?= =?UTF-8?q?=E9=87=8D=E7=B7=A8=E6=AD=A5=E9=A9=9F=E7=B7=A8=E8=99=9F=E8=88=87?= =?UTF-8?q?=E8=AA=AA=E6=98=8E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 46 +++++++++++------------ src/lib/agents.js | 2 +- src/lib/gitea.js | 8 ++-- src/lib/review.js | 87 ++++++++++++++++++++++---------------------- src/lib/roles.js | 2 +- src/lib/templates.js | 26 ++++++------- 6 files changed, 86 insertions(+), 85 deletions(-) diff --git a/readme.md b/readme.md index 35df151..1d9725d 100644 --- a/readme.md +++ b/readme.md @@ -38,13 +38,13 @@ jobs: ```mermaid flowchart TD S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure] - S1 -->|未命中| S2[2 偵測 AI 工具並留言] - S2 --> S3[3 讀 .reviewignore 整理 diff 並留言] - S3 --> S4[4 攻擊方登場留言] - S4 --> S5[5 攻擊方 sub agent 並行找問題] - S5 --> S6[6 防守方登場留言] - S6 --> S7[7 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] - S7 --> S8[8 舊留言標記解決] + S1 -->|未命中| S2[2 舊留言標記解決] + S2 --> S3[3 偵測 AI 工具並留言] + S3 --> S4[4 讀 .reviewignore 整理 diff 並留言] + S4 --> S5[5 攻擊方登場留言] + S5 --> S6[6 攻擊方 sub agent 並行找問題] + S6 --> S7[7 防守方登場留言] + S7 --> S8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] S8 --> S9[9 嚴重問題逐條掛行留言] S9 --> S10[10 警告+建議彙整表格留言] S10 --> E1[收尾 commit/push + exit code] @@ -104,9 +104,9 @@ flowchart TD | [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | | [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) | | [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) | -| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) | -| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) | -| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 4/6 角色登場留言](#templatesrolescomment) | +| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L88) | [產生步驟 3 審查工具留言](#templatestoolcomment) | +| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L132) | [產生步驟 4 變更摘要留言](#templatesdiffcomment) | +| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L173) | [產生步驟 5/7 角色登場留言](#templatesrolescomment) | | [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | | [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | | [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | @@ -168,8 +168,8 @@ taipeiFromIso(''); // '—' ```js const { log } = require('./src/lib/log'); -log('步驟3', 'INF', '變更檔案 5 個,送審 3 個。'); -// [2026/07/17 16:46:13][步驟3][INF]: 變更檔案 5 個,送審 3 個。 +log('步驟4', 'INF', '變更檔案 5 個,送審 3 個。'); +// [2026/07/17 16:46:13][步驟4][INF]: 變更檔案 5 個,送審 3 個。 ``` @@ -246,7 +246,7 @@ const committed = gitrepo.commitAndPushFindings(cwd, { ### gitea.whoAmI -取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 8 以 `login` 比對留言作者辨識本 action 發過的留言。 +取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 2 以 `login` 比對留言作者辨識本 action 發過的留言。 ```js const gitea = require('./src/lib/gitea'); @@ -269,7 +269,7 @@ await gitea.createCommentOnIssue(ctx, issue.number, '🔴 嚴重|...'); ```js const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...'); -// created.id 記入本回合留言集合,步驟 8 標註過時時跳過 +// created.id 記入本回合留言集合,步驟 2 標註過時時跳過 ``` @@ -294,7 +294,7 @@ const issue = await gitea.createIssue(ctx, { title: 'PR 標題', body: '…', la ### gitea.listIssueComments -列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 8 據此找出 bot 舊留言標註〔已過時〕。 +列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 2 據此找出 bot 舊留言標註〔已過時〕。 ```js const comments = await gitea.listIssueComments(ctx); @@ -303,7 +303,7 @@ const comments = await gitea.listIssueComments(ctx); ### gitea.editIssueComment -以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 8 用來替舊留言加上〔已過時〕前綴。 +以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 2 用來替舊留言加上〔已過時〕前綴。 ```js await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`); @@ -323,7 +323,7 @@ await gitea.createReview(ctx, '## 🔴 嚴重問題(共 2 條)…', [ ### gitea.listReviews -列出 PR 全部 review(自動分頁);步驟 8 據此逐一取出行內留言嘗試解決。 +列出 PR 全部 review(自動分頁);步驟 2 據此逐一取出行內留言嘗試解決。 ```js const reviews = await gitea.listReviews(ctx); @@ -411,7 +411,7 @@ const defenders = defendersOf(roles); // [Paladin] ### templates.toolComment -產生步驟 2 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 8 辨識。 +產生步驟 3 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 2 辨識。 ```js const body = templates.toolComment({ @@ -424,7 +424,7 @@ const body = templates.toolComment({ ### templates.diffComment -產生步驟 3 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。 +產生步驟 4 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。 ```js const body = templates.diffComment(diffRows, ignoredCount); @@ -433,7 +433,7 @@ const body = templates.diffComment(diffRows, ignoredCount); ### templates.rolesComment -產生步驟 4/6 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。 +產生步驟 5/7 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。 ```js const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }); @@ -534,7 +534,7 @@ await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); ### review.runAttackers -步驟 5:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。 +步驟 6:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。 ```js const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); @@ -543,7 +543,7 @@ const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attack ### review.runDefenders -步驟 7:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。 +步驟 8:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。 ```js const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); @@ -600,7 +600,7 @@ if (ctx.createIssue && kept.length > 0) { ### review.resolveOldComments -步驟 8:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。 +步驟 2:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。 ```js await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); diff --git a/src/lib/agents.js b/src/lib/agents.js index acec87d..e7cfe73 100644 --- a/src/lib/agents.js +++ b/src/lib/agents.js @@ -47,7 +47,7 @@ const TOOLS = [ * @returns {{ name: string, buildArgs: Function, resultFrom: string, version: string } | null} * 中選工具的描述物件(TOOLS 項目加上 version 欄位);所有工具皆不可用時回傳 null。 * @remarks - * 使用情境:action 主流程(步驟 2)啟動審查前呼叫一次,取得工具描述後交給 + * 使用情境:action 主流程(步驟 3)啟動審查前呼叫一次,取得工具描述後交給 * runAgent 執行;若回傳 null,主流程會記 ERR 並以失敗收場(無工具即無法審查)。 */ function detectTool() { diff --git a/src/lib/gitea.js b/src/lib/gitea.js index 232c9db..ef71722 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -80,7 +80,7 @@ async function listAll(ctx, apiPath) { * @returns {Promise} Gitea 使用者物件(含 `id`、`login` 等欄位, * 依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx,例如 token 無效時 401)由底層 `api` 丟出。 - * @remarks 使用情境:action 步驟 8 先查出 bot 自己的帳號, + * @remarks 使用情境:action 步驟 2 先查出 bot 自己的帳號, * 之後比對 PR 留言的作者,辨識哪些留言是本 action 先前發出的 * (例如要將舊留言標註為已過時)。 */ @@ -178,7 +178,7 @@ function createIssue(ctx, { title, body, labels }) { * @returns {Promise>} 留言物件陣列(含 `id`、`body`、`user` 等欄位); * 無留言時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:步驟 8 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI` + * @remarks 使用情境:步驟 2 重跑 review 前,先撈出 PR 全部留言並搭配 `whoAmI` * 比對作者,找出本 action(bot)先前發過的留言,以便編輯標註為已過時。 */ function listIssueComments(ctx) { @@ -237,7 +237,7 @@ function createReview(ctx, body, comments) { * @returns {Promise>} review 物件陣列(含 `id`、`user`、`body` 等欄位); * 無 review 時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:步驟 8 重跑 review 前,先找出 PR 上既有 review, + * @remarks 使用情境:步驟 2 重跑 review 前,先找出 PR 上既有 review, * 再以 `listReviewComments` 取出其行內留言做後續解決標記。 */ function listReviews(ctx) { @@ -280,7 +280,7 @@ function listReviewComments(ctx, reviewId) { * @param {number|string} commentId - 要標記為已解決的行內留言 id。 * @returns {Promise} 標記成功回傳 `true`;任何失敗 * (版本不支援、權限不足、留言不存在等)一律回傳 `false`,不丟出例外。 - * @remarks 使用情境:步驟 8 嘗試把舊回合的行內留言標記為已解決;若回傳 `false` + * @remarks 使用情境:步驟 2 嘗試把舊回合的行內留言標記為已解決;若回傳 `false` * (例如目標 Gitea 版本無此 API),呼叫端應停止嘗試並記 WRN * (由 `resolveOldComments` 實作此降級)。 */ diff --git a/src/lib/review.js b/src/lib/review.js index da0086e..cf504f8 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -22,7 +22,7 @@ const TOTAL_DIFF_LIMIT = 160_000; * @param {string} workspace - 工作目錄絕對路徑(`.reviewignore` 所在的 repo 根目錄)。 * @returns {string[]} 忽略用的路徑前綴陣列;檔案不存在時為空陣列。 * @remarks - * 使用情境:審查流程「步驟 3」開頭由 `src/index.js` 呼叫, + * 使用情境:審查流程「步驟 4」開頭由 `src/index.js` 呼叫, * 取得前綴清單後搭配 {@link isIgnored} 過濾 `gitrepo.changedFiles` 的結果, * 決定哪些變更檔案要納入送審。 */ @@ -46,7 +46,7 @@ function loadReviewIgnore(workspace) { * @param {string[]} prefixes - 忽略路徑前綴清單(通常來自 {@link loadReviewIgnore})。 * @returns {boolean} `true` 表示忽略、不納入審查;`false` 表示送審。 * @remarks - * 使用情境:審查流程「步驟 3」中,`src/index.js` 以 + * 使用情境:審查流程「步驟 4」中,`src/index.js` 以 * `allFiles.filter((file) => !review.isIgnored(file, ignores))` * 過濾變更檔案清單,被排除的檔案數量會反映在變更摘要留言的排除統計。 */ @@ -70,7 +70,7 @@ function isIgnored(file, prefixes) { * @returns {Array<{file: string, purpose: string, lines: number, chars: number, truncated: boolean, lastUpdated: string, diffForPrompt: string}>} * 每檔一列的 diff 資料列;`purpose` 初始為「—」,由 {@link fillPurposes} 補齊。 * @remarks - * 使用情境:審查流程「步驟 3」由 `src/index.js` 呼叫,產出的 rows 同時餵給 + * 使用情境:審查流程「步驟 4」由 `src/index.js` 呼叫,產出的 rows 同時餵給 * {@link fillPurposes}(補用途)、`templates.diffComment`(變更摘要留言)與 * {@link buildAttackPrompt}(攻擊方提示的變更內容區塊)。 */ @@ -86,12 +86,12 @@ function collectDiffRows({ cwd, files, base, gitrepo }) { if (diffForPrompt.length > PER_FILE_DIFF_LIMIT) { diffForPrompt = `${diffForPrompt.slice(0, PER_FILE_DIFF_LIMIT)}\n...(diff 過長,其餘截斷未送審)`; truncated = true; - log('步驟3', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`); + log('步驟4', 'WRN', `${file} 的 diff 超過單檔上限(${chars} 字元),已截斷送審。`); } if (totalChars + diffForPrompt.length > TOTAL_DIFF_LIMIT) { diffForPrompt = '(全部 diff 總量超過送審上限,本檔內容未送審,僅列出檔名)'; truncated = true; - log('步驟3', 'WRN', `${file} 因總量上限未送審 diff 內容。`); + log('步驟4', 'WRN', `${file} 因總量上限未送審 diff 內容。`); } else { totalChars += diffForPrompt.length; } @@ -121,7 +121,7 @@ function collectDiffRows({ cwd, files, base, gitrepo }) { * @param {Array} params.diffRows - {@link collectDiffRows} 產出的資料列;本函式會就地更新其 `purpose` 欄位。 * @returns {Promise} 無回傳值;結果反映在 `diffRows` 的 `purpose` 欄位。 * @remarks - * 使用情境:審查流程「步驟 3」在 `collectDiffRows` 之後、發布 + * 使用情境:審查流程「步驟 4」在 `collectDiffRows` 之後、發布 * `templates.diffComment` 變更摘要留言之前呼叫,讓摘要表格的「用途」欄有內容。 */ async function fillPurposes({ tool, model, cwd, diffRows }) { @@ -140,12 +140,12 @@ ${sections} - 不得輸出個資(PII)。`; const res = await runAgent(tool, { model, prompt, cwd, timeoutMs: 300_000 }); if (!res.ok) { - log('步驟3', 'WRN', '檔案用途摘要產生失敗,以「—」代替。'); + log('步驟4', 'WRN', '檔案用途摘要產生失敗,以「—」代替。'); return; } const parsed = extractJson(res.output); if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) { - log('步驟3', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。'); + log('步驟4', 'WRN', '檔案用途摘要回覆無法解析,以「—」代替。'); return; } for (const row of diffRows) { @@ -163,7 +163,7 @@ ${sections} * @param {*} value - 攻擊方回覆的 severity 原始值(可能是任何型別;非字串會先轉字串)。 * @returns {'嚴重'|'警告'|'建議'} 收斂後的等級字串。 * @remarks - * 使用情境:審查流程「步驟 5」中 {@link normalizeFinding} 檢核每條 finding 時呼叫, + * 使用情境:審查流程「步驟 6」中 {@link normalizeFinding} 檢核每條 finding 時呼叫, * 確保後續 {@link sortFindings} 的 `templates.SEVERITY_ORDER` 排序、 * 「嚴重」分組(步驟 9 逐條留言 vs 步驟 10 彙整表格)都能以固定用詞比對。 * 本函式未匯出,僅供模組內部使用。 @@ -188,7 +188,7 @@ function normalizeSeverity(value) { * @param {Array} diffRows - {@link collectDiffRows} 產出的送審資料列(file/purpose/lastUpdated/diffForPrompt)。 * @returns {string} 可直接餵給 `runAgent` stdin 的完整提示字串。 * @remarks - * 使用情境:審查流程「步驟 5」{@link runAttackers} 為每個攻擊方角色各組一份提示, + * 使用情境:審查流程「步驟 6」{@link runAttackers} 為每個攻擊方角色各組一份提示, * 並行送入 sub agent 找問題。本函式未匯出,僅供模組內部使用。 */ function buildAttackPrompt(role, diffRows) { @@ -233,7 +233,7 @@ ${sections} * @returns {?{reviewer: string, focus: string, badge: string, severity: string, file: string, startLine: number, endLine: number, problem: string, suggestion: string, suggestedCode: string}} * 標準化後的 finding;輸入不合格時為 `null`。 * @remarks - * 使用情境:審查流程「步驟 5」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後, + * 使用情境:審查流程「步驟 6」{@link runAttackers} 解析每個攻擊方的 JSON 回覆後, * 逐條經本函式檢核,通過者才進入合併列表並編派 id,供防守方裁決與留言使用。 * 本函式未匯出,僅供模組內部使用。 */ @@ -256,7 +256,7 @@ function normalizeFinding(fromAgent, role) { } /** - * 步驟 5:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。 + * 步驟 6:每個攻擊方角色一個 sub agent 並行分析送審 diff,合併為單一問題列表並編派 id。 * * 單一角色失敗(執行失敗或回覆無法解析為 JSON 陣列)只記 WRN 並以空結果代替, * 不阻斷其他角色(失敗降級行為);每條回覆先經 {@link normalizeFinding} 檢核, @@ -270,25 +270,25 @@ function normalizeFinding(fromAgent, role) { * @param {Array} params.diffRows - {@link collectDiffRows} 產出的送審資料列。 * @returns {Promise>} 合併後的標準化 finding 列表(每條含 `id`);全部失敗或無問題時為空陣列。 * @remarks - * 使用情境:審查流程「步驟 5」由 `src/index.js` 在攻擊方登場留言後呼叫, - * 結果直接交給步驟 7 的 {@link runDefenders} 裁決。 + * 使用情境:審查流程「步驟 6」由 `src/index.js` 在攻擊方登場留言後呼叫, + * 結果直接交給步驟 8 的 {@link runDefenders} 裁決。 */ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { const results = await Promise.all( attackers.map(async (role) => { - log('步驟5', 'INF', `攻擊方 ${role.meta.name} 開始分析。`); + log('步驟6', 'INF', `攻擊方 ${role.meta.name} 開始分析。`); const res = await runAgent(tool, { model, prompt: buildAttackPrompt(role, diffRows), cwd }); if (!res.ok) { - log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}。`); + log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 執行失敗:${(res.error && res.error.message) || '未知錯誤'}。`); return []; } const parsed = extractJson(res.output); if (!Array.isArray(parsed)) { - log('步驟5', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`); + log('步驟6', 'WRN', `攻擊方 ${role.meta.name} 回覆無法解析為 JSON 陣列,略過該角色結果。`); return []; } const list = parsed.map((f) => normalizeFinding(f, role)).filter(Boolean); - log('步驟5', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`); + log('步驟6', 'INF', `攻擊方 ${role.meta.name} 完成:${list.length} 條問題。`); return list; }), ); @@ -296,7 +296,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { merged.forEach((finding, index) => { finding.id = `F${String(index + 1).padStart(3, '0')}`; }); - log('步驟5', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`); + log('步驟6', 'INF', `全部攻擊方完成,合併後共 ${merged.length} 條問題。`); return merged; } @@ -309,7 +309,7 @@ async function runAttackers({ tool, model, cwd, attackers, diffRows }) { * @param {number} limit - 保留的最大字元數(超過即截斷)。 * @returns {string} 截斷後的檔案內容;檔案不存在時為空字串。 * @remarks - * 使用情境:審查流程「步驟 7」{@link runDefenders} 以 + * 使用情境:審查流程「步驟 8」{@link runDefenders} 以 * `readCapped(/.gitea/ai-review/exclusions.json, 20_000)` * 讀取已知排除事項,嵌入 {@link buildDefendPrompt} 的防守方提示, * 避免排除清單過長撐爆提示。本函式未匯出,僅供模組內部使用。 @@ -331,7 +331,7 @@ function readCapped(filePath, limit) { * @param {string} cwd - 工作目錄(repo 根目錄,findings 目錄位於其下 `.gitea/ai-review/findings`)。 * @returns {string} 歷史 findings 摘要文字(Markdown 區段 + JSON);無歷史時為空字串。 * @remarks - * 使用情境:審查流程「步驟 7」{@link runDefenders} 呼叫本函式取得歷史摘要, + * 使用情境:審查流程「步驟 8」{@link runDefenders} 呼叫本函式取得歷史摘要, * 嵌入 {@link buildDefendPrompt},讓防守方能以「與歷史 findings 重複」為由裁決排除。 * 本函式未匯出,僅供模組內部使用。 */ @@ -379,7 +379,7 @@ function loadHistory(cwd) { * @param {string} historyText - {@link loadHistory} 產出的歷史 findings 摘要;空字串時提示顯示「(無)」。 * @returns {string} 可直接餵給 `runAgent` stdin 的完整裁決提示字串。 * @remarks - * 使用情境:審查流程「步驟 7」{@link runDefenders} 為每個防守方角色各組一份提示, + * 使用情境:審查流程「步驟 8」{@link runDefenders} 為每個防守方角色各組一份提示, * 並行送入 sub agent 逐條裁決是否可排除(重複或誤判)。本函式未匯出,僅供模組內部使用。 */ function buildDefendPrompt(role, findings, exclusionsText, historyText) { @@ -424,7 +424,7 @@ ${JSON.stringify(minimal, null, 2)} } /** - * 步驟 7:每個防守方角色一個 sub agent 並行裁決 findings; + * 步驟 8:每個防守方角色一個 sub agent 並行裁決 findings; * 「全部防守方都判可排除」才移除該條,其餘一律保留(保守原則)。 * * 失敗降級:某防守方執行失敗或回覆無法解析 → 該角色視為全部保留; @@ -441,7 +441,7 @@ ${JSON.stringify(minimal, null, 2)} * `kept`=保留(至少一位防守方不同意排除)、`excluded`=移除(全數防守方判可排除); * 兩邊元素都已附 `verdicts`。 * @remarks - * 使用情境:審查流程「步驟 7」由 `src/index.js` 呼叫;`kept` 隨後經 + * 使用情境:審查流程「步驟 8」由 `src/index.js` 呼叫;`kept` 隨後經 * {@link sortFindings} 排序、依「嚴重」分組發留言(步驟 9/10), * `kept` 與 `excluded` 一併保存進 `.gitea/ai-review/findings/*.json`。 */ @@ -451,7 +451,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { const historyText = loadHistory(cwd); const verdictsPerDefender = await Promise.all( defenders.map(async (role) => { - log('步驟7', 'INF', `防守方 ${role.meta.name} 開始裁決。`); + log('步驟8', 'INF', `防守方 ${role.meta.name} 開始裁決。`); const res = await runAgent(tool, { model, prompt: buildDefendPrompt(role, findings, exclusionsText, historyText), @@ -459,7 +459,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { }); const verdicts = new Map(); if (!res.ok) { - log('步驟7', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`); + log('步驟8', 'WRN', `防守方 ${role.meta.name} 執行失敗,該角色視為全部保留。`); return { role: role.meta.name, verdicts }; } const parsed = extractJson(res.output); @@ -473,9 +473,9 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { } } } else { - log('步驟7', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`); + log('步驟8', 'WRN', `防守方 ${role.meta.name} 回覆無法解析,該角色視為全部保留。`); } - log('步驟7', 'INF', `防守方 ${role.meta.name} 完成裁決。`); + log('步驟8', 'INF', `防守方 ${role.meta.name} 完成裁決。`); return { role: role.meta.name, verdicts }; }), ); @@ -493,7 +493,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { finding.verdicts = verdicts; (allExclude ? excluded : kept).push(finding); } - log('步驟7', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`); + log('步驟8', 'INF', `裁決完成:保留 ${kept.length} 條、排除 ${excluded.length} 條。`); return { kept, excluded }; } @@ -514,7 +514,7 @@ async function runDefenders({ tool, model, cwd, defenders, findings }) { * false=無排除問題、或既有檔案壞損/非陣列而略過寫入。 * @throws {Error} 檔案系統寫入失敗(如權限不足)時由 fs 拋出,未攔截。 * @remarks - * 使用情境:`main()`(src/index.js)於步驟 7 防守方裁決後呼叫本函式, + * 使用情境:`main()`(src/index.js)於步驟 8 防守方裁決後呼叫本函式, * 並以回傳值決定收尾時是否把 exclusions.json 一併 commit * (一般模式:findings+exclusions.json;建問題模式:只 commit exclusions.json)。 */ @@ -527,11 +527,11 @@ function appendExclusions({ cwd, excluded, prNumber }) { try { entries = JSON.parse(fs.readFileSync(filePath, 'utf8')); } catch { - log('步驟7', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); + log('步驟8', 'WRN', 'exclusions.json 無法解析,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); return false; } if (!Array.isArray(entries)) { - log('步驟7', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); + log('步驟8', 'WRN', 'exclusions.json 非 JSON 陣列,為避免破壞既有內容不附加誤判紀錄(需人工確認)。'); return false; } } @@ -552,7 +552,7 @@ function appendExclusions({ cwd, excluded, prNumber }) { } fs.mkdirSync(dir, { recursive: true }); fs.writeFileSync(filePath, `${JSON.stringify(entries, null, 2)}\n`, 'utf8'); - log('步驟7', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`); + log('步驟8', 'INF', `已將 ${excluded.length} 條誤判/重複問題附加到 exclusions.json。`); return true; } @@ -565,7 +565,7 @@ function appendExclusions({ cwd, excluded, prNumber }) { * @param {Array<{severity: string, file: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為 {@link runDefenders} 回傳的 `kept`)。 * @returns {void} 無回傳值;排序結果反映在傳入陣列本身。 * @remarks - * 使用情境:審查流程「步驟 7」裁決完成後、保存 findings 與分組發留言之前, + * 使用情境:審查流程「步驟 8」裁決完成後、保存 findings 與分組發留言之前, * `src/index.js` 對 `kept` 呼叫本函式,確保步驟 9 逐條留言與步驟 10 彙整表格 * 都以「嚴重度優先、同檔集中、行號遞增」的穩定順序呈現。 */ @@ -758,7 +758,7 @@ function readSnippet(cwd, finding) { } /** - * 步驟 8:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。 + * 步驟 2:將 PR 既有的 bot 留言標記為已解決,本回合剛發的留言除外。 * * 兩類處理: * - 一般留言(bot 發、含隱藏標記、非本回合、尚未標註)→ 編輯加上「〔已過時〕」前綴。 @@ -772,15 +772,16 @@ function readSnippet(cwd, finding) { * @param {Set} params.currentRunCommentIds - 本回合發出的一般留言 id 集合;這些留言不標註過時。 * @returns {Promise} 無回傳值;結果反映在 PR 留言狀態與日誌。 * @remarks - * 使用情境:審查流程「步驟 8」在防守方裁決、保存 findings 之後、 - * 發布本回合嚴重問題留言(步驟 9)之前呼叫,確保 PR 上只有最新回合的審查結果醒目可見。 + * 使用情境:審查流程「步驟 2」在步驟 1 快速回報與前置檢查之後、偵測工具(步驟 3) + * 與所有本回合留言之前呼叫;此時本回合尚未發出任何留言(currentRunCommentIds 為空), + * 之後發出的留言自然不受影響,確保 PR 上只有最新回合的審查結果醒目可見。 */ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { let botLogin = ''; try { botLogin = (await gitea.whoAmI(ctx)).login || ''; } catch (err) { - log('步驟8', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`); + log('步驟2', 'WRN', `無法取得 bot 身分(${err.message}),略過留言解決。`); return; } @@ -797,9 +798,9 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { await gitea.editIssueComment(ctx, comment.id, `${templates.OUTDATED_PREFIX}${comment.body}`); outdatedCount += 1; } - log('步驟8', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`); + log('步驟2', 'INF', `一般留言已標註〔已過時〕:${outdatedCount} 則。`); } catch (err) { - log('步驟8', 'WRN', `標註一般留言失敗:${err.message}。`); + log('步驟2', 'WRN', `標註一般留言失敗:${err.message}。`); } // review 程式碼留言:盡力 resolve;API 不支援(第一次就失敗)即停止嘗試。 @@ -825,12 +826,12 @@ async function resolveOldComments({ ctx, gitea, currentRunCommentIds }) { } } if (resolveSupported) { - log('步驟8', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`); + log('步驟2', 'INF', `review 程式碼留言已解決:${resolvedCount} 則。`); } else { - log('步驟8', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。'); + log('步驟2', 'WRN', 'Gitea 版本不支援 resolve API,review 程式碼留言維持原狀(已解決 ' + resolvedCount + ' 則)。'); } } catch (err) { - log('步驟8', 'WRN', `解決 review 留言失敗:${err.message}。`); + log('步驟2', 'WRN', `解決 review 留言失敗:${err.message}。`); } } diff --git a/src/lib/roles.js b/src/lib/roles.js index f120b67..16eac2b 100644 --- a/src/lib/roles.js +++ b/src/lib/roles.js @@ -23,7 +23,7 @@ const path = require('path'); * @throws {Error} 當 `rolesDir` 不存在、無法讀取,或個別檔案讀取失敗時, * 由 `fs.readdirSync` / `fs.readFileSync` 直接拋出(未在函式內捕捉)。 * @remarks - * 使用情境:`src/index.js` 於審查流程步驟 4 呼叫 + * 使用情境:`src/index.js` 於審查流程步驟 5 呼叫 * `loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles'))` 載入全部角色, * 再以 {@link attackersOf} / {@link defendersOf} 依 frontmatter 的 `side` 欄位 * 分出攻擊方(Mage/Assassin/Rogue/Bard/Leo/Maya)與防守方(Paladin), diff --git a/src/lib/templates.js b/src/lib/templates.js index 89ab109..6219b0f 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -2,10 +2,10 @@ // 固定留言模板:本 action 發到 PR 的留言一律由此產生(繁體中文、UTF-8、表格優先)。 -// 隱藏標記:辨識哪些留言是本 action 發的(步驟 8 標註過時時使用)。 +// 隱藏標記:辨識哪些留言是本 action 發的(步驟 2 標註過時時使用)。 const MARK = ''; -// 舊留言標註前綴(步驟 8 的降級做法:無 resolve API 時編輯加註)。 +// 舊留言標註前綴(步驟 2 的降級做法:無 resolve API 時編輯加註)。 const OUTDATED_PREFIX = '> 〔已過時〕本留言屬於較舊的審查回合。\n\n'; // 嚴重等級對應的 emoji 與排序權重。 @@ -33,7 +33,7 @@ const FOCUS_LABEL = { * @param {*} text - 任意待處理內容;非字串會先以 `String()` 轉型,null/undefined 視為空字串。 * @returns {string} 已逸出、單行化的儲存格內容;若結果為空則回傳 `'—'`。 * @remarks - * 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 3 的 + * 使用情境:審查流程中所有表格型留言的共用防呆——例如步驟 4 的 * `diffComment()` 產生變更摘要表格時,檔名與用途欄位都經本函式處理, * 避免檔名或 AI 產生的描述含 `|` 或換行而撐破 Markdown 表格。 * 本函式未匯出,僅供模組內部使用。 @@ -55,7 +55,7 @@ function cell(text) { * @param {string} focus - 審查面向代碼(例如 `'logic'`、`'security'`);可為 undefined。 * @returns {string} 顯示字串:命中時如 `'邏輯(logic)'`;未命中時原樣回傳 `focus`;falsy 時回傳 `'—'`。 * @remarks - * 使用情境:審查流程步驟 4/6 的角色登場留言——`rolesComment()` + * 使用情境:審查流程步驟 5/6 的角色登場留言——`rolesComment()` * 產生「角色|面向|個性」表格時,以本函式把每位審查員 * (攻擊方/防守方)的 focus 代碼轉成中英並列的面向欄位內容。 * 本函式未匯出,僅供模組內部使用。 @@ -66,7 +66,7 @@ function focusLabel(focus) { } /** - * 產生審查流程步驟 2 的「審查工具」PR 留言內容。 + * 產生審查流程步驟 3 的「審查工具」PR 留言內容。 * * 留言以隱藏標記 `MARK` 開頭,包含工具資訊表格(工具/版本/模型/ * 審查 commit/Run Job 連結)與一張 mermaid 流程圖,說明整條審查管線 @@ -81,9 +81,9 @@ function focusLabel(focus) { * @param {string} params.runLink - CI run 的網址,直接內插為 Markdown 連結目標。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記,結尾帶換行)。 * @remarks - * 使用情境:審查流程步驟 2——每回合審查開始時,先把工具身分與 + * 使用情境:審查流程步驟 3——每回合審查開始時,先把工具身分與 * 管線流程圖留言到 PR,讓開發者知道這回合由哪個版本/模型執行; - * 留言開頭的 MARK 讓步驟 8 能辨識並將舊回合留言標註為過時。 + * 留言開頭的 MARK 讓步驟 2 能辨識並將舊回合留言標註為過時。 */ function toolComment({ toolName, version, model, sha, runNumber, runLink }) { return `${MARK} @@ -108,7 +108,7 @@ flowchart LR } /** - * 產生審查流程步驟 3 的「變更摘要(送審 git diff)」PR 留言內容。 + * 產生審查流程步驟 4 的「變更摘要(送審 git diff)」PR 留言內容。 * * 以四欄表格(檔案/用途/git diff 長度/最後更新時間)列出本回合 * 送審的每個檔案;diff 過長被截斷送審的檔案會加註「(過長截斷送審)」, @@ -125,7 +125,7 @@ flowchart LR * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示排除註記。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:審查流程步驟 3——整理完 git diff 後,把「哪些檔案、多長、 + * 使用情境:審查流程步驟 4——整理完 git diff 後,把「哪些檔案、多長、 * 是否截斷、哪些被 .reviewignore 排除」留言到 PR,讓開發者確認送審範圍 * 與 AI 實際看到的內容一致。 */ @@ -150,10 +150,10 @@ function diffComment(rows, ignoredCount) { } /** - * 產生審查流程步驟 4/6 共用的「角色登場」PR 留言內容。 + * 產生審查流程步驟 5/6 共用的「角色登場」PR 留言內容。 * * 以三欄表格(角色/面向/個性)列出本回合登場的審查員; - * 攻擊方(步驟 4)與防守方(步驟 6)共用本模板,僅標題不同。 + * 攻擊方(步驟 5)與防守方(步驟 7)共用本模板,僅標題不同。 * 面向欄位經 focusLabel() 轉成「中文(原文)」並列格式。 * * @param {Object} params - 留言內容(解構參數)。 @@ -166,7 +166,7 @@ function diffComment(rows, ignoredCount) { * @param {string} params.roles[].meta.personality - 角色個性描述;經 cell() 防呆。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:審查流程步驟 4(攻擊方登場)與步驟 6(防守方登場)—— + * 使用情境:審查流程步驟 5(攻擊方登場)與步驟 7(防守方登場)—— * 在各階段開始審查前,把該回合參與的審查員角色、負責面向與個性 * 留言到 PR,讓開發者理解後續 findings 是由哪些視角產出的。 */ @@ -371,7 +371,7 @@ function issueFindingComment(finding) { * @param {number} ignoredCount - 依 `.reviewignore` 排除的檔案數;大於 0 才顯示「(N 個檔案被排除)」註記。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:審查流程步驟 3 的替代路徑——整理 git diff 時發現 + * 使用情境:審查流程步驟 4 的替代路徑——整理 git diff 時發現 * 過濾後送審清單為空(例如整包變更都被 .reviewignore 排除), * 直接以本留言告知開發者本回合視為審查通過,不再進入 * 攻擊方/防守方審查階段。