Files
ai-code-review/readme.md
T

619 lines
30 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 tokenPR 留言與 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 diffcommit 操作、Gitea REST API 客戶端(留言/reviewissue/標籤)、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) | [產生步驟 57 角色登場留言](#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) |
## 使用範例
<a id="logtaipeinow"></a>
### 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'
```
<a id="logtaipeifilestamp"></a>
### 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
```
<a id="logtaipeifromiso"></a>
### log.taipeiFromIso
將 ISO 8601 時間字串轉為台北時區顯示字串;輸入為空或無法解析時回傳佔位符「—」不丟例外,適合直接嵌進留言表格。
```js
const { taipeiFromIso } = require('./src/lib/log');
taipeiFromIso('2026-07-17T06:30:05Z'); // '2026/07/17 14:30:05'
taipeiFromIso(''); // '—'
```
<a id="loglog"></a>
### log.log
以統一格式 `[yyyy/MM/dd HH:mm:ss][階段][等級]: 訊息` 輸出一行日誌到 stdout;stage 為空時省略階段區塊,等級約定限 INF/WRN/ERRTRCDBG。
```js
const { log } = require('./src/lib/log');
log('步驟4', 'INF', '變更檔案 5 個,送審 3 個。');
// [2026/07/17 16:46:13][步驟4][INF]: 變更檔案 5 個,送審 3 個。
```
<a id="contextloadcontext"></a>
### 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
```
<a id="gitrepolatestcommitsubject"></a>
### 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());
```
<a id="gitreporesolvemergebase"></a>
### gitrepo.resolveMergeBase
先嘗試 `git fetch origin <baseRef>`(失敗靜默沿用本地資料),再以 `git merge-base origin/<baseRef> HEAD` 取得共同祖先,作為 diff 比較基準,避免把 base 分支後續演進算進 PR 變更。
```js
const base = gitrepo.resolveMergeBase(cwd, 'master'); // '3f2a…'40 碼 SHA
```
<a id="gitrepochangedfiles"></a>
### gitrepo.changedFiles
列出 base 與 HEAD 之間有變更的檔案(repo 相對路徑陣列);結果再經 `.reviewignore` 過濾後逐檔送審。
```js
const files = gitrepo.changedFiles(cwd, base); // ['src/index.js', 'action.yml']
```
<a id="gitrepofilediff"></a>
### gitrepo.fileDiff
取得單一檔案在 base 與 HEAD 之間的 unified diff 原始文字(無變更時為空字串),供組進攻擊方提示。
```js
const diff = gitrepo.fileDiff(cwd, base, 'src/index.js');
```
<a id="gitrepofilelastupdatediso"></a>
### gitrepo.fileLastUpdatedIso
取得檔案最後一次 commit 的 ISO 8601 時間;查不到(未 commit、git 失敗)回空字串,由呼叫端以「—」佔位。
```js
const iso = gitrepo.fileLastUpdatedIso(cwd, 'src/index.js'); // '2026-07-17T15:00:00+08:00'
```
<a id="gitrepocommitandpushfindings"></a>
### 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=無變更略過
```
<a id="giteawhoami"></a>
### gitea.whoAmI
取得 token 對應的使用者(`GET /user`),即 bot 身分;步驟 2 以 `login` 比對留言作者辨識本 action 發過的留言。
```js
const gitea = require('./src/lib/gitea');
const me = await gitea.whoAmI(ctx); // { id, login, ... }
```
<a id="giteacreatecommentonissue"></a>
### gitea.createCommentOnIssue
對指定編號的 issue(或 PR,Gitea 兩者共用留言機制)新增一般留言;建問題模式逐條留言問題明細即用本函式。
```js
await gitea.createCommentOnIssue(ctx, issue.number, '🔴 嚴重|...');
```
<a id="giteacreateissuecomment"></a>
### gitea.createIssueComment
對本次 PR`ctx.prNumber`)新增一般留言;為 `createCommentOnIssue` 的便捷包裝,主流程各步驟的留言都經由它發出。
```js
const created = await gitea.createIssueComment(ctx, '## 📋 變更摘要 ...');
// created.id 記入本回合留言集合,步驟 2 標註過時時跳過
```
<a id="gitealistlabels"></a>
### gitea.listLabels
列出存取庫可用標籤(自動分頁);建問題模式先取得標籤,再交給 `review.selectLabels` 由 AI 挑選。
```js
const labels = await gitea.listLabels(ctx); // [{ id, name, color }, ...]
```
<a id="giteacreateissue"></a>
### gitea.createIssue
在存取庫建立 issue`labels`(標籤 id 陣列)僅在非空時帶入。建問題模式以 PR 標題/描述為內容建立追蹤 issue。
```js
const issue = await gitea.createIssue(ctx, { title: 'PR 標題', body: '…', labels: [3, 7] });
// issue.number 供後續逐條留言
```
<a id="gitealistissuecomments"></a>
### gitea.listIssueComments
列出 PR 全部一般留言(自動分頁,每頁 50 筆);步驟 2 據此找出 bot 舊留言標註〔已過時〕。
```js
const comments = await gitea.listIssueComments(ctx);
```
<a id="giteaeditissuecomment"></a>
### gitea.editIssueComment
以新內容整段覆寫既有一般留言(留言 id 於 repo 層級定位);步驟 2 用來替舊留言加上〔已過時〕前綴。
```js
await gitea.editIssueComment(ctx, comment.id, `> 〔已過時〕…\n\n${comment.body}`);
```
<a id="giteacreatereview"></a>
### 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: '…' },
]);
```
<a id="gitealistreviews"></a>
### gitea.listReviews
列出 PR 全部 review(自動分頁);步驟 2 據此逐一取出行內留言嘗試解決。
```js
const reviews = await gitea.listReviews(ctx);
```
<a id="gitealistreviewcomments"></a>
### gitea.listReviewComments
列出指定 review 底下的全部行內留言(單次呼叫、未分頁)。
```js
const comments = await gitea.listReviewComments(ctx, reviews[0].id);
```
<a id="giteatryresolvereviewcomment"></a>
### gitea.tryResolveReviewComment
盡力將行內留言標記為已解決;resolve endpoint 依 Gitea 版本不一定存在(需人工確認),任何失敗一律回 `false` 不丟錯,呼叫端第一次失敗即停止嘗試。
```js
const ok = await gitea.tryResolveReviewComment(ctx, reviewId, commentId);
if (!ok) { /* 版本不支援 → 記 WRN 後放棄後續 resolve */ }
```
<a id="agentsdetecttool"></a>
### agents.detectTool
依 antigravity → codex → claude 優先序,以 `<tool> --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
```
<a id="agentsrunagent"></a>
### 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;
```
<a id="agentsextractjson"></a>
### 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
```
<a id="rolesloadroles"></a>
### 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', ... }
```
<a id="rolesattackersof"></a>
### roles.attackersOf
過濾出 `meta.side === 'attack'` 的攻擊方角色(現況 6 位:AssassinBardLeoMageMayaRogue),保留檔名排序、不改原陣列。
```js
const attackers = attackersOf(roles); // 6 位攻擊方
```
<a id="rolesdefendersof"></a>
### roles.defendersOf
過濾出 `meta.side === 'defend'` 的防守方角色(現況 1 位:Paladinfocus: verdict)。
```js
const defenders = defendersOf(roles); // [Paladin]
```
<a id="templatestoolcomment"></a>
### 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}`,
});
```
<a id="templatesdiffcomment"></a>
### templates.diffComment
產生步驟 4 的變更摘要留言:四欄表格(檔案/用途/git diff 長度/最後更新時間),截斷送審的檔案加註,結尾統計送審與排除數。
```js
const body = templates.diffComment(diffRows, ignoredCount);
```
<a id="templatesrolescomment"></a>
### templates.rolesComment
產生步驟 5/7 共用的角色登場留言:三欄表格(角色/面向/個性),面向以「中文(原文)」並列。
```js
const body = templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers });
```
<a id="templatesseverecommentbody"></a>
### templates.severeCommentBody
產生步驟 9 單條嚴重問題的留言內容(程式碼片段/問題/修改建議/建議寫法,結尾提示可回覆);降級為一般留言時以 `withLocation: true` 在內文標明檔案與行號。
```js
const body = templates.severeCommentBody(finding, snippet);
const fallback = templates.severeCommentBody(finding, snippet, { withLocation: true });
```
<a id="templatesseverereviewbody"></a>
### templates.severeReviewBody
產生步驟 9 嚴重問題 review 的總覽 body(標明總數,說明逐條掛行)。
```js
await gitea.createReview(ctx, templates.severeReviewBody(severe.length), comments);
```
<a id="templatesotherscomment"></a>
### templates.othersComment
產生步驟 10 的警告+建議彙整表格留言(等級/審查員/檔案名稱/問題起訖行數/問題描述/修改建議),儲存格經防呆逸出。
```js
const body = templates.othersComment(others); // others=非嚴重的保留問題
```
<a id="templatesissuebody"></a>
### templates.issueBody
產生建問題模式新 issue 的本文:PR 描述為主體(缺省以「(PR 無描述)」佔位),尾端附追溯引言標明來源 PR。
```js
const body = templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody });
```
<a id="templatesissuefindingcomment"></a>
### templates.issueFindingComment
產生建問題模式單條問題的 issue 留言(固定模板:嚴重等級/位置起訖行數/問題描述/修改建議/建議寫法);issue 留言無法掛行,位置一律以內文標明。
```js
await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding));
```
<a id="templatesnothingtoreviewcomment"></a>
### templates.nothingToReviewComment
產生「無可審查變更」留言:套用 `.reviewignore` 後送審清單為空時取代變更摘要,宣告本回合視為審查通過。
```js
const body = templates.nothingToReviewComment(ignoredCount);
```
<a id="reviewloadreviewignore"></a>
### review.loadReviewIgnore
讀取 repo 根目錄的 `.reviewignore`(每行一個路徑前綴、`#` 註解、空行略過);檔案不存在回空陣列。
```js
const review = require('./src/lib/review');
const ignores = review.loadReviewIgnore(cwd); // ['.gitea/', 'README.md', ...]
```
<a id="reviewisignored"></a>
### review.isIgnored
判斷檔案是否忽略不送審:任何深度的 `node_modules/` 一律排除(內建保險),其餘依前綴比對。
```js
const files = allFiles.filter((f) => !review.isIgnored(f, ignores));
```
<a id="reviewcollectdiffrows"></a>
### review.collectDiffRows
為每個送審檔案取得 diff 並計算統計(行數/字元數/最後更新時間),套用單檔 16,000/總量 160,000 字元送審上限(超限記 WRN、不靜默截斷);`purpose` 先以「—」佔位。
```js
const diffRows = review.collectDiffRows({ cwd, files, base, gitrepo });
```
<a id="reviewfillpurposes"></a>
### review.fillPurposes
以選定 AI 工具為每個送審檔案產生一行用途描述並就地寫回 `diffRows[].purpose`;失敗只記 WRN 保留「—」,不阻斷流程。
```js
await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows });
```
<a id="reviewrunattackers"></a>
### review.runAttackers
步驟 6:每位攻擊方角色一個 sub agent 並行分析 diff,回覆經檢核標準化後合併為單一問題列表並編派 `F001…` 流水號;單一角色失敗只記 WRN 以空結果代替。
```js
const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows });
```
<a id="reviewrundefenders"></a>
### review.runDefenders
步驟 8:每位防守方角色一個 sub agent 配合 `exclusions.json` 與歷史 findings 裁決;「全部防守方都判可排除」才移除,拿不準一律保留,每條附 `verdicts` 供追溯。
```js
const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings });
```
<a id="reviewsortfindings"></a>
### review.sortFindings
就地排序:嚴重→警告→建議,再依檔案路徑、起始行遞增;供 findings 保存與步驟 9/10 分組留言使用。
```js
review.sortFindings(kept);
```
<a id="reviewappendexclusions"></a>
### review.appendExclusions
把防守方判定排除(誤判/重複)的問題附加到 `.gitea/ai-review/exclusions.json`(含各防守方理由與來源 PR 編號);既有檔案壞損或非陣列時不動原檔、記 WRN(需人工確認)。回傳是否有寫入,決定收尾是否一併 commit。
```js
const changed = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber });
```
<a id="reviewsortfindingsforissue"></a>
### review.sortFindingsForIssue
建問題模式的就地排序:檔案路徑→嚴重等級(嚴重→建議)→起始行,讓 issue 留言同檔集中、便於逐檔處理。
```js
const sorted = [...kept];
review.sortFindingsForIssue(sorted);
```
<a id="reviewselectlabels"></a>
### review.selectLabels
以 AI 依 PR 標題/描述與問題列表摘要,從存取庫可用標籤挑選子集合(白名單過濾幻覺名稱後轉標籤 id);無標籤或失敗一律回空陣列不阻斷。
```js
const labelIds = await review.selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings });
```
<a id="reviewcreateissuewithfindings"></a>
### 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 });
}
```
<a id="reviewresolveoldcomments"></a>
### review.resolveOldComments
步驟 2:bot 舊一般留言(非本回合)編輯加〔已過時〕前綴;review 行內留言盡力呼叫 resolve API,第一次失敗即判定版本不支援並停止。任何失敗只記 WRN 不阻斷。
```js
await review.resolveOldComments({ ctx, gitea, currentRunCommentIds });
```
<a id="reviewpostseverecomments"></a>
### review.postSevereComments
步驟 9:嚴重問題以單一 code review 逐條掛在對應程式碼行上(含問題區塊程式碼片段,最多 40 行);建立 review 失敗時降級為一般留言逐條發布並在內文標明位置。
```js
if (severe.length > 0) {
await review.postSevereComments({ ctx, gitea, severe, cwd });
}
```