# AI Code Review > 更新時間:2026/07/28 13:57:24 AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。 ## 使用方式 ```yaml # .gitea/workflows/review.yaml(呼叫端範例) name: AI-REVIEW on: pull_request: branches: [master, develop] types: [opened, synchronize] jobs: review: runs-on: ubuntu steps: - uses: actions/checkout@v4 with: fetch-depth: 0 # 需完整歷史以計算 merge-base - uses: https://gitea.jsc.idv.tw/node-actions/ai-code-review@v1 with: token: ${{ secrets.GITHUB_TOKEN }} # 必填:PR 留言與 push findings 用 model: '' # 選填:指定 AI 模型(空=工具預設) create-issue: 'false' # 選填:'true' 時問題另建 issue 追蹤 ``` | input | 必填 | 預設 | 說明 | | --- | --- | --- | --- | | `token` | ✅ | — | Gitea API token(PR 留言與 push findings 用;呼叫端以 secrets 傳入) | | `model` | ❌ | `''` | 指定 AI 工具使用的模型(空值=各工具預設) | | `create-issue` | ❌ | `'false'` | `'true'` 時建立 issue 逐條留言問題明細,收尾只 commit `exclusions.json` | 審查流程(10 步驟): ```mermaid flowchart TD S1[1 判斷 bot commit 標記] -->|命中| E0[直接回報 success/failure] S1 -->|未命中| N3[3 偵測 AI 工具並留言] N3 --> N4[4 讀 .reviewignore 整理 diff 並留言] N4 --> N5[5 攻擊方登場留言] N5 --> N6[6 攻擊方 sub agent 並行找問題] N6 --> N7[7 防守方登場留言] N7 --> N8[8 防守方裁決 → 保存 findings + 誤判回寫 exclusions.json] N8 --> N2[2 延後將舊留言標記解決(成功產生結果後才執行)] N2 --> N9[9 嚴重問題逐條掛行留言] N9 --> N10[10 警告+建議彙整表格留言] N10 --> E1[收尾 commit/push + exit code] ``` ## 專案列表 ### 專案描述表 | 專案名稱 | 專案描述 | | --- | --- | | [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | Gitea node action:提供台北時區日誌工具、runner 上下文載入、git diff/commit 操作、Gitea REST API 客戶端(留言/review/issue/標籤)、AI CLI 工具偵測與 sub agent 執行、角色提示載入、固定留言模板,以及多角色審查編排(攻擊方找問題、防守方裁決、findings 保存、誤判回寫、建問題模式) | ### 參考專案表 | 專案名稱 | 參考專案列表 | | --- | --- | | [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 | ### NuGet 套件表 | 專案名稱 | NuGet 套件列表 | | --- | --- | | [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 | ## 功能列表 ### ai-code-review | 功能名稱 | 功能描述 | | --- | --- | | [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) | | [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) | | [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | | [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js) | [以統一格式輸出一行日誌](#loglog) | | [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | | [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | | [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | | [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | | [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | | [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | | [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | | [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | | [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | | [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | | [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出存取庫可用標籤](#gitealistlabels) | | [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) | | [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) | | [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [編輯既有一般留言](#giteaeditissuecomment) | | [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [建立 code review 並掛行內留言](#giteacreatereview) | | [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) | | [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [列出某 review 的全部行內留言](#gitealistreviewcomments) | | [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) | | [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) | | [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) | | [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) | | [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | | [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js) | [過濾出攻擊方角色](#rolesattackersof) | | [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/roles.js) | [過濾出防守方角色](#rolesdefendersof) | | [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 3 審查工具留言](#templatestoolcomment) | | [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 4 變更摘要留言](#templatesdiffcomment) | | [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 5/7 角色登場留言](#templatesrolescomment) | | [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | | [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | | [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | | [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生建問題模式的 issue 本文](#templatesissuebody) | | [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) | | [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) | | [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) | | [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [判斷檔案是否忽略不送審](#reviewisignored) | | [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) | | [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) | | [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) | | [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) | | [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) | | [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) | | [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) | | [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) | | [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) | | [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) | | [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) | ## 使用範例 ### log.taipeiNow 將指定時間(省略時為現在)轉為台北時區(Asia/Taipei)的 `yyyy/MM/dd HH:mm:ss` 字串,輸出不受主機系統時區影響;供日誌時間戳與 findings 產生時間使用。 ```js const { taipeiNow } = require('./src/lib/log'); taipeiNow(); // '2026/07/17 16:46:13' taipeiNow(new Date('2026-01-01')); // '2026/01/01 08:00:00' ``` ### log.taipeiFileStamp 產生檔名用時間戳 `yyyy-MM-dd-HH:mm:ss`(空白換成 `-`);findings 檔案即以此命名。注意輸出含 `:`,Linux 檔名合法、不可移植到 Windows。 ```js const { taipeiFileStamp } = require('./src/lib/log'); taipeiFileStamp(); // '2026-07-17-16:46:13' → .gitea/ai-review/findings/2026-07-17-16:46:13.json ``` ### log.taipeiFromIso 將 ISO 8601 時間字串轉為台北時區顯示字串;輸入為空或無法解析時回傳佔位符「—」不丟例外,適合直接嵌進留言表格。 ```js const { taipeiFromIso } = require('./src/lib/log'); taipeiFromIso('2026-07-17T06:30:05Z'); // '2026/07/17 14:30:05' taipeiFromIso(''); // '—' ``` ### log.log 以統一格式 `[yyyy/MM/dd HH:mm:ss][階段][等級]: 訊息` 輸出一行日誌到 stdout;stage 為空時省略階段區塊,等級約定限 INF/WRN/ERR/TRC/DBG。 ```js const { log } = require('./src/lib/log'); log('步驟4', 'INF', '變更檔案 5 個,送審 3 個。'); // [2026/07/17 16:46:13][步驟4][INF]: 變更檔案 5 個,送審 3 個。 ``` ### context.loadContext 彙整 runner 注入的 `GITHUB_*` 環境變數、`INPUT_*` 輸入參數與事件 payload,組出審查流程所需的完整上下文(repo、PR 編號/標題/描述、head/base、token、model、createIssue、workspace、actionPath 等)。前置條件:於 Actions runner 環境執行;呼叫端應檢查 `prNumber` 與 `token` 是否有值。 ```js const { loadContext } = require('./src/lib/context'); const ctx = loadContext(); if (!ctx.prNumber || !ctx.token) process.exit(1); // 非 PR 事件或缺 token ``` ### gitrepo.latestCommitSubject 取得目前 HEAD 最新 commit 的訊息標題;主流程步驟 1 以此比對 `chore: update ai-review findings [ai-review-bot][success|failure]` 決定是否直接回報結果。 ```js const gitrepo = require('./src/lib/gitrepo'); const subject = gitrepo.latestCommitSubject(process.cwd()); ``` ### gitrepo.resolveMergeBase 先嘗試 `git fetch origin `(失敗靜默沿用本地資料),再以 `git merge-base origin/ HEAD` 取得共同祖先,作為 diff 比較基準,避免把 base 分支後續演進算進 PR 變更。 ```js const base = gitrepo.resolveMergeBase(cwd, 'master'); // '3f2a…'(40 碼 SHA) ``` ### gitrepo.changedFiles 列出 base 與 HEAD 之間有變更的檔案(repo 相對路徑陣列);結果再經 `.reviewignore` 過濾後逐檔送審。 ```js const files = gitrepo.changedFiles(cwd, base); // ['src/index.js', 'action.yml'] ``` ### gitrepo.fileDiff 取得單一檔案在 base 與 HEAD 之間的 unified diff 原始文字(無變更時為空字串),供組進攻擊方提示。 ```js const diff = gitrepo.fileDiff(cwd, base, 'src/index.js'); ``` ### gitrepo.fileLastUpdatedIso 取得檔案最後一次 commit 的 ISO 8601 時間;查不到(未 commit、git 失敗)回空字串,由呼叫端以「—」佔位。 ```js const iso = gitrepo.fileLastUpdatedIso(cwd, 'src/index.js'); // '2026-07-17T15:00:00+08:00' ``` ### gitrepo.commitAndPushFindings 以 `ai-review-bot` 身分將指定檔案 commit 並 push 回 PR 來源分支;HEAD 停在 merge commit 時先 detach 到 head sha,暫存區無差異時不建空 commit(回傳 `false`),origin push 失敗改用帶 token 的 URL 重試(該 URL 絕不可輸出到日誌)。 ```js const committed = gitrepo.commitAndPushFindings(cwd, { headRef: 'feature/x', headSha: ctx.headSha, message: 'chore: update ai-review findings [ai-review-bot][success]', files: ['.gitea/ai-review/findings/2026-07-17-16:46:13.json'], token: ctx.token, serverUrl: ctx.serverUrl, repository: ctx.repository, }); // true=已推送、false=無變更略過 ``` ### gitea.whoAmI 取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 2 以 `login` 比對留言作者辨識本 action 發過的留言。 ```js const gitea = require('./src/lib/gitea'); const me = await gitea.whoAmI(ctx); // { id, login, ... } ``` ### gitea.createCommentOnIssue 對指定編號的 issue(或 PR,Gitea 兩者共用留言機制)新增一般留言;建問題模式逐條留言問題明細即用本函式。 ```js await gitea.createCommentOnIssue(ctx, issue.number, '🔴 嚴重|...'); ``` ### gitea.createIssueComment 對本次 PR(`ctx.prNumber`)新增一般留言;為 `createCommentOnIssue` 的便捷包裝,主流程各步驟的留言都經由它發出。 ```js const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...'); // created.id 記入本回合留言集合,步驟 2 標註過時時跳過 ``` ### gitea.listLabels 列出存取庫可用標籤(自動分頁);建問題模式先取得標籤,再交給 `review.selectLabels` 由 AI 挑選。 ```js const labels = await gitea.listLabels(ctx); // [{ id, name, color }, ...] ``` ### gitea.createIssue 在存取庫建立 issue;`labels`(標籤 id 陣列)僅在非空時帶入。建問題模式以 PR 標題/描述為內容建立追蹤 issue。 ```js const issue = await gitea.createIssue(ctx, { title: 'PR 標題', body: '…', labels: [3, 7] }); // issue.number 供後續逐條留言 ``` ### gitea.listIssueComments 列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 2 據此找出 bot 舊留言標註〔已過時〕。 ```js const comments = await gitea.listIssueComments(ctx); ``` ### gitea.editIssueComment 以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 2 用來替舊留言加上〔已過時〕前綴。 ```js await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`); ``` ### gitea.createReview 建立 event 為 `COMMENT` 的 code review,並把行內留言逐條掛在檔案行號上;步驟 9 以單一 review 送出全部嚴重問題。若行號不在 PR diff 內會整包失敗,呼叫端(`review.postSevereComments`)會降級為一般留言。 ```js await gitea.createReview(ctx, '## 🔴 嚴重問題(共 2 條)…', [ { path: 'src/a.js', new_position: 42, body: '…' }, ]); ``` ### gitea.listReviews 列出 PR 全部 review(自動分頁);步驟 2 據此逐一取出行內留言嘗試解決。 ```js const reviews = await gitea.listReviews(ctx); ``` ### gitea.listReviewComments 列出指定 review 底下的全部行內留言(單次呼叫、未分頁)。 ```js const comments = await gitea.listReviewComments(ctx, reviews[0].id); ``` ### gitea.tryResolveReviewComment 盡力將行內留言標記為已解決;resolve endpoint 依 Gitea 版本不一定存在(需人工確認),任何失敗一律回 `false` 不丟錯,呼叫端第一次失敗即停止嘗試。 ```js const ok = await gitea.tryResolveReviewComment(ctx, reviewId, commentId); if (!ok) { /* 版本不支援 → 記 WRN 後放棄後續 resolve */ } ``` ### agents.detectTool 依 antigravity → codex → claude 優先序,以 ` --version`(30 秒逾時)偵測可用工具,第一個成功者中選並附版本字串;全部不可用回 `null`(主流程記 ERR 失敗收場)。antigravity 的非互動參數尚未驗證(需人工確認)。 ```js const agents = require('./src/lib/agents'); const tool = agents.detectTool(); // { name: 'codex', version: 'codex-cli 0.144.5', ... } | null ``` ### agents.runAgent 以非互動模式執行一次 sub agent:提示從 stdin 餵入,codex 改讀 `--output-last-message` 暫存檔取最終回覆。永不 reject——逾時、非零退出碼都以 `{ ok: false }` resolve,由呼叫端降級。 ```js const res = await agents.runAgent(tool, { model: '', prompt: '…', cwd, timeoutMs: 600000 }); const data = res.ok ? agents.extractJson(res.output) : null; ``` ### agents.extractJson 從 agent 自由文字回覆萃取 JSON:先剝 code fence、再以「陣列優先」的最大範圍切片嘗試 parse;失敗一律回 `null` 不丟例外。 ```js agents.extractJson('```json\n[{"a":1}]\n```'); // [{ a: 1 }] agents.extractJson('雜訊 {"b":2} 雜訊'); // { b: 2 } agents.extractJson('不是 JSON'); // null ``` ### roles.loadRoles 載入目錄下全部 `*.md` 角色提示檔(依檔名排序),解析開頭 `---` 包夾的輕量 frontmatter(單行「鍵: 值」),回傳 `{ file, meta, body, raw }` 陣列。 ```js const { loadRoles } = require('./src/lib/roles'); const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); // roles[0].meta => { name: 'Assassin', side: 'attack', focus: 'security', ... } ``` ### roles.attackersOf 過濾出 `meta.side === 'attack'` 的攻擊方角色(現況 6 位:Assassin/Bard/Leo/Mage/Maya/Rogue),保留檔名排序、不改原陣列。 ```js const attackers = attackersOf(roles); // 6 位攻擊方 ``` ### roles.defendersOf 過濾出 `meta.side === 'defend'` 的防守方角色(現況 1 位:Paladin,focus: verdict)。 ```js const defenders = defendersOf(roles); // [Paladin] ``` ### templates.toolComment 產生步驟 3 的審查工具留言:工具/版本/模型/審查 commit/Run Job 連結表格+審查管線 mermaid 流程圖;開頭含隱藏標記供步驟 2 辨識。 ```js const body = templates.toolComment({ toolName: 'codex', version: 'codex-cli 0.144.5', model: '', sha: ctx.headSha, runNumber: ctx.runNumber, runLink: `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`, }); ``` ### templates.diffComment 產生步驟 4 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。 ```js const body = templates.diffComment(diffRows, ignoredCount); ``` ### templates.rolesComment 產生步驟 5/7 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。 ```js const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers }); ``` ### templates.severeCommentBody 產生步驟 9 單條嚴重問題的留言內容(程式碼片段/問題/修改建議/建議寫法,結尾提示可回覆);降級為一般留言時以 `withLocation: true` 在內文標明檔案與行號。 ```js const body = templates.severeCommentBody(finding, snippet); const fallback = templates.severeCommentBody(finding, snippet, { withLocation: true }); ``` ### templates.severeReviewBody 產生步驟 9 嚴重問題 review 的總覽 body(標明總數,說明逐條掛行)。 ```js await gitea.createReview(ctx, templates.severeReviewBody(severe.length), comments); ``` ### templates.othersComment 產生步驟 10 的警告+建議彙整表格留言(等級/審查員/檔案名稱/問題起訖行數/問題描述/修改建議),儲存格經防呆逸出。 ```js const body = templates.othersComment(others); // others=非嚴重的保留問題 ``` ### templates.issueBody 產生建問題模式新 issue 的本文:PR 描述為主體(缺省以「(PR 無描述)」佔位),尾端附追溯引言標明來源 PR。 ```js const body = templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }); ``` ### templates.issueFindingComment 產生建問題模式單條問題的 issue 留言(固定模板:嚴重等級/位置起訖行數/問題描述/修改建議/建議寫法);issue 留言無法掛行,位置一律以內文標明。 ```js await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding)); ``` ### templates.nothingToReviewComment 產生「無可審查變更」留言:套用 `.reviewignore` 後送審清單為空時取代變更摘要,宣告本回合視為審查通過。 ```js const body = templates.nothingToReviewComment(ignoredCount); ``` ### review.loadReviewIgnore 讀取 repo 根目錄的 `.reviewignore`(每行一個路徑前綴、`#` 註解、空行略過);檔案不存在回空陣列。 ```js const review = require('./src/lib/review'); const ignores = review.loadReviewIgnore(cwd); // ['.gitea/', 'README.md', ...] ``` ### review.isIgnored 判斷檔案是否忽略不送審:任何深度的 `node_modules/` 一律排除(內建保險),其餘依前綴比對。 ```js const files = allFiles.filter((f) => !review.isIgnored(f, ignores)); ``` ### review.collectDiffRows 為每個送審檔案取得 diff 並計算統計(行數/字元數/最後更新時間),套用單檔 16,000/總量 160,000 字元送審上限(超限記 WRN、不靜默截斷);`purpose` 先以「—」佔位。 ```js const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo }); ``` ### review.fillPurposes 以選定 AI 工具為每個送審檔案產生一行用途描述並就地寫回 `diffRows[].purpose`;失敗只記 WRN 保留「—」,不阻斷流程。 ```js await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); ``` ### review.runAttackers 步驟 6:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。 ```js const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); ``` ### review.runDefenders 步驟 8:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。 ```js const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); ``` ### review.sortFindings 就地排序:嚴重→警告→建議,再依檔案路徑、起始行遞增;供 findings 保存與步驟 9/10 分組留言使用。 ```js review.sortFindings(kept); ``` ### review.appendExclusions 把防守方判定排除(誤判/重複)的問題附加到 `.gitea/ai-review/exclusions.json`(含各防守方理由與來源 PR 編號);既有檔案壞損或非陣列時不動原檔、記 WRN(需人工確認)。回傳是否有寫入,決定收尾是否一併 commit。 ```js const changed = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber }); ``` ### review.sortFindingsForIssue 建問題模式的就地排序:檔案路徑→嚴重等級(嚴重→建議)→起始行,讓 issue 留言同檔集中、便於逐檔處理。 ```js const sorted = [...kept]; review.sortFindingsForIssue(sorted); ``` ### review.selectLabels 以 AI 依 PR 標題/描述與問題列表摘要,從存取庫可用標籤挑選子集合(白名單過濾幻覺名稱後轉標籤 id);無標籤或失敗一律回空陣列不阻斷。 ```js const labelIds = await review.selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }); ``` ### review.createIssueWithFindings 建問題模式主流程:AI 挑標籤 → 建立 issue(標題=PR 標題、本文=PR 描述+追溯)→ 問題依檔案→嚴重度排序逐條留言到 issue;建 issue 失敗記 ERR 回 `null` 不阻斷主流程。 ```js if (ctx.createIssue && kept.length > 0) { await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept }); } ``` ### review.resolveOldComments 步驟 2:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。 ```js await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); ``` ### review.postSevereComments 步驟 9:嚴重問題以單一 code review 逐條掛在對應程式碼行上(含問題區塊程式碼片段,最多 40 行);建立 review 失敗時降級為一般留言逐條發布並在內文標明位置。 ```js if (severe.length > 0) { await review.postSevereComments({ ctx, gitea, severe, cwd }); } ```