From 3fe643c00375fc3be29e19b22c487dca432bca21 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 17 Jul 2026 18:54:00 +0800 Subject: [PATCH 1/8] =?UTF-8?q?docs(doc-funcs):=20=E8=A3=9C=E9=BD=8A=20act?= =?UTF-8?q?ion=20=E8=88=87=20workflow=20=E6=96=87=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yaml | 47 ++++++++++++++++ .gitea/workflows/readme.md | 41 ++++++++++++++ action.yml | 50 ++++++++--------- readme.md | 108 ++++++++++++++++++------------------- src/index.js | 12 ++++- 5 files changed, 178 insertions(+), 80 deletions(-) create mode 100644 .gitea/workflows/readme.md diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index 05a4103..e973722 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -1,45 +1,92 @@ +# ============================================================================ +# 用途:在 pull request 針對 develop 分支開啟或同步時,分別以 Antigravity、Codex、Claude 環境執行本機 AI Code Review action。 +# 更新時間:2026/07/17 18:49:58 +# ============================================================================ +# Workflow 顯示名稱。 name: CI +# Workflow 觸發條件區塊。 on: + # 以 pull request 事件觸發。 pull_request: + # 限定 pull request 目標分支。 branches: + # 僅在目標分支為 develop 時執行。 - develop + # 限定 pull request 開啟與同步更新時執行。 types: [opened, synchronize] +# Job 定義區塊。 jobs: + # Antigravity 工具環境測試 job。 test-antigravity: + # Job 在 workflow UI 顯示的名稱。 name: TEST (Antigravity) + # 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。 runs-on: ubuntu + # Job 執行步驟。 steps: + # 取得存取庫內容供後續 action 使用。 - name: 取得存取庫資訊 + # 使用呼叫端 vars 指定的 checkout action 版本。 uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + # 安裝 Antigravity 工具環境。 - name: 安裝工具 + # 使用 Gitea composite action 安裝 Antigravity,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-antigravity@${{ vars.ACTION_SETUP_ANTIGRAVITY_VERSION }} + # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 + # 以目前存取庫根目錄的 action.yml 作為 action 來源。 uses: ./ + # 傳入 action inputs。 with: + # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # Codex 工具環境測試 job。 test-codex: + # Job 在 workflow UI 顯示的名稱。 name: TEST (Codex) + # 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。 runs-on: ubuntu + # Job 執行步驟。 steps: + # 取得存取庫內容供後續 action 使用。 - name: 取得存取庫資訊 + # 使用呼叫端 vars 指定的 checkout action 版本。 uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + # 安裝 Codex 工具環境。 - name: 安裝工具 + # 使用 Gitea composite action 安裝 Codex,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-codex@${{ vars.ACTION_SETUP_CODEX_VERSION }} + # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 + # 以目前存取庫根目錄的 action.yml 作為 action 來源。 uses: ./ + # 傳入 action inputs。 with: + # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # Claude 工具環境測試 job。 test-claude: + # Job 在 workflow UI 顯示的名稱。 name: TEST (Claude) + # 指定執行 runner 標籤;需人工確認本 Gitea runner 是否使用 ubuntu 標籤。 runs-on: ubuntu + # Job 執行步驟。 steps: + # 取得存取庫內容供後續 action 使用。 - name: 取得存取庫資訊 + # 使用呼叫端 vars 指定的 checkout action 版本。 uses: actions/checkout@${{ vars.ACTION_CHECKOUT_VERSION }} + # 安裝 Claude 工具環境。 - name: 安裝工具 + # 使用 Gitea composite action 安裝 Claude,版本由 vars 指定。 uses: https://gitea.jsc.idv.tw/composite-actions/setup-claude@${{ vars.ACTION_SETUP_CLAUDE_VERSION }} + # 執行目前存取庫的 AI Code Review action。 - name: 程式碼審查 + # 以目前存取庫根目錄的 action.yml 作為 action 來源。 uses: ./ + # 傳入 action inputs。 with: + # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.gitea/workflows/readme.md b/.gitea/workflows/readme.md new file mode 100644 index 0000000..6b2e3c8 --- /dev/null +++ b/.gitea/workflows/readme.md @@ -0,0 +1,41 @@ +# Workflow 文件草稿 + +更新時間:2026/07/17 18:49:58 + +## Workflow 總覽 + +| 名稱 | 檔案位置 | 用途 | 觸發條件 | +| --- | --- | --- | --- | +| CI | `.gitea/workflows/ci.yaml` | 在 PR 事件中分別以 Antigravity、Codex、Claude 工具環境執行本存取庫的 AI Code Review action,驗證 action 可在不同 AI 工具環境下運作。 | `pull_request` 目標分支為 `develop`,事件類型為 `opened` 或 `synchronize`。 | + +## CI + +- Workflow 名稱:`CI` +- 檔案位置:`.gitea/workflows/ci.yaml` +- 用途:針對送往 `develop` 的 pull request 執行三組測試 job,分別安裝 Antigravity、Codex、Claude 環境後呼叫 `uses: ./` 執行目前 action。 +- 觸發條件:`pull_request.branches` 為 `develop`;`pull_request.types` 為 `opened`、`synchronize`。 + +## Job + +| Job ID | 顯示名稱 | Runner | 主要流程 | +| --- | --- | --- | --- | +| `test-antigravity` | `TEST (Antigravity)` | `ubuntu` | checkout 存取庫、安裝 Antigravity、執行本地 action。 | +| `test-codex` | `TEST (Codex)` | `ubuntu` | checkout 存取庫、安裝 Codex、執行本地 action。 | +| `test-claude` | `TEST (Claude)` | `ubuntu` | checkout 存取庫、安裝 Claude、執行本地 action。 | + +## 主要輸入 / 環境參數 + +| 名稱 | 來源 | 使用位置 | 說明 | +| --- | --- | --- | --- | +| `vars.ACTION_CHECKOUT_VERSION` | Gitea / GitHub repository 或 organization vars | `actions/checkout@...` | 指定 checkout action 版本。 | +| `vars.ACTION_SETUP_ANTIGRAVITY_VERSION` | Gitea / GitHub vars | `setup-antigravity@...` | 指定 Antigravity 安裝 action 版本。 | +| `vars.ACTION_SETUP_CODEX_VERSION` | Gitea / GitHub vars | `setup-codex@...` | 指定 Codex 安裝 action 版本。 | +| `vars.ACTION_SETUP_CLAUDE_VERSION` | Gitea / GitHub vars | `setup-claude@...` | 指定 Claude 安裝 action 版本。 | +| `secrets.GITHUB_TOKEN` | Gitea / GitHub secrets | 本地 action input `token` | 提供 action 呼叫 Gitea API 留言與寫回 findings 所需 token。 | + +## 注意事項 + +- `.gitea/workflows/readme.md` 目前不存在;本檔為新增 workflow README 的草稿。 +- `runs-on: ubuntu` 是否符合實際 Gitea runner 標籤需人工確認。 +- `vars.*` 與 `secrets.GITHUB_TOKEN` 必須由呼叫端環境提供,否則 checkout、工具安裝或程式碼審查步驟可能失敗。 +- 三個 job 均以 `uses: ./` 呼叫目前存取庫根目錄的 `action.yml`;因此 `action.yml` 的 `runs.main` 需保持可被 runner 直接執行。 diff --git a/action.yml b/action.yml index c952c50..08eb38f 100644 --- a/action.yml +++ b/action.yml @@ -1,53 +1,53 @@ -# ===================================================== -# 用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings -# 更新時間: 2026/07/17 16:49:21 -# ===================================================== +# ============================================================================ +# 用途:定義 AI Code Review Node action 的名稱、輸入參數與 Node.js 24 進入點,供 Gitea / GitHub workflow 以 uses 引用。 +# 更新時間:2026/07/17 18:49:58 +# ============================================================================ # Gitea / GitHub node action 的 manifest(action.yml): # 定義本 action 的名稱、說明、輸入參數(inputs)與執行方式(runs), # 供呼叫端 workflow 以 `uses:` 引用;runner 讀取此檔後以 node24 執行 src/index.js。 -# action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱 +# action 顯示名稱:呼叫端 workflow log 與 marketplace 列表上看到的名稱。 name: 'AI Code Review' # action 用途說明:多角色 AI code review 流程(攻擊方找問題、防守方裁決誤報), -# 審查結果會留言到 PR 並保存 findings(.gitea/ai-review/findings/) +# 審查結果會留言到 PR 並保存 findings(.gitea/ai-review/findings/)。 description: 'AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings' -# action 作者資訊(僅供辨識,不影響執行) +# action 作者資訊(僅供辨識,不影響執行)。 author: 'Jeffery' # 輸入參數區塊:呼叫端 workflow 以 `with:` 傳入, -# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取 +# runner 會自動注入為 INPUT_* 環境變數(例如 INPUT_TOKEN、INPUT_MODEL、INPUT_CREATE-ISSUE)供主程式讀取。 inputs: - # Gitea API token:用於對 PR 留言審查結果、以及 push 審查結果檔回 repo + # Gitea API token:用於對 PR 留言審查結果、以及 push 審查結果檔回 repo。 token: # 參數用途說明:secrets/vars context 在 action 內不可用, - # 故由呼叫端 workflow 以 secrets.GITHUB_TOKEN 傳入 + # 故由呼叫端 workflow 以 secrets.GITHUB_TOKEN 傳入。 description: 'Gitea API token(PR 留言與 push findings 用;呼叫端以 secrets.GITHUB_TOKEN 傳入)' - # 必填:缺少 token 無法呼叫 Gitea API,action 無法運作 + # 必填:缺少 token 無法呼叫 Gitea API,action 無法運作。 required: true - # 指定 AI 工具使用的模型名稱 + # 指定 AI 工具使用的模型名稱。 model: - # 參數用途說明:留空表示使用各 AI 工具自身的預設模型 + # 參數用途說明:留空表示使用各 AI 工具自身的預設模型。 description: '指定 AI 工具使用的模型(空值=各工具預設)' - # 選填:未指定時採用預設值 + # 選填:未指定時採用預設值。 required: false - # 預設為空字串,代表不覆寫各工具的預設模型 + # 預設為空字串,代表不覆寫各工具的預設模型。 default: '' - # 建問題模式開關:是否把審查保留的問題另建 issue 追蹤 + # 建問題模式開關:是否把審查保留的問題另建 issue 追蹤。 create-issue: - # 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤) + # 參數用途說明:字串 'true' 時建立 issue(標題=PR 標題、描述=PR 描述、AI 挑標籤), # 並逐條留言問題明細,findings 檔不進版控、收尾只 commit exclusions.json; - # 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支) + # 預設 'false' 走原流程(findings 檔與 exclusions.json 一併 commit 回 PR 來源分支)。 description: '是否將問題建到存取庫的問題追蹤(true 時建立 issue 逐條留言問題明細,最後只 commit exclusions.json;預設 false 走原流程)' - # 選填:未指定時採用預設值 + # 選填:未指定時採用預設值。 required: false - # 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用) + # 預設為字串 'false',代表不啟用建問題模式(主程式只認字串 'true' 才啟用)。 default: 'false' -# 執行方式區塊:宣告本 action 為 node action 及其進入點 +# 執行方式區塊:宣告本 action 為 node action 及其進入點。 runs: - # 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite) + # 以 Node.js 24 runtime 直接在 runner 上執行(非 Docker 容器、非 composite)。 using: 'node24' - # 主程式進入點:直接指向 src/index.js(entry point) + # 主程式進入點:直接指向 src/index.js(entry point)。 # 主程式為零外部相依(package.json 無 dependencies,src 僅 require Node 內建模組與本地 lib), # runner 不會自動 npm install,零相依時依 node action 慣例 main 直接指向 src/index.js 即正確; - # 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script) - # 並將 main 改指 dist/index.js、把 dist/ commit 進 repo + # 日後若新增外部相依,需改以 @vercel/ncc 打包(package.json 已備有 build script), + # 並將 main 改指 dist/index.js、把 dist/ commit 進 repo。 main: 'src/index.js' diff --git a/readme.md b/readme.md index 760617f..244c223 100644 --- a/readme.md +++ b/readme.md @@ -1,6 +1,6 @@ # AI Code Review -> 更新時間:2026/07/17 16:49:21 +> 更新時間:2026/07/17 18:49:58 AI 多角色 code review 的 Gitea **node action**(`node24`、零外部相依):以攻擊方六角色(🔮 Mage 邏輯、🗡️ Assassin 安全、⚡ Rogue 效率、🎼 Bard 風格、🧪 Maya 測試、🧰 Leo 可維護性)並行找問題、防守方(🛡️ Paladin)裁決誤報,結果留言到 PR、保存 findings,並以 bot commit 標記審查結果(`[success]`/`[failure]`)供下次觸發快速回報。 @@ -56,19 +56,19 @@ flowchart TD | 專案名稱 | 專案描述 | | --- | --- | -| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/) | 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/) | 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/master/) | 無 | +| [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/master/) | 無 | +| [ai-code-review](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/) | 無 | ## 功能列表 @@ -76,56 +76,56 @@ flowchart TD | 功能名稱 | 功能描述 | | --- | --- | -| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L20) | [取得台北時區 yyyy/MM/dd HH:mm:ss 時間字串](#logtaipeinow) | -| [log.taipeiFileStamp](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L41) | [取得檔名用時間戳 yyyy-MM-dd-HH:mm:ss](#logtaipeifilestamp) | -| [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | -| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | -| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | -| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | -| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | -| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | -| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | -| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | -| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | -| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | -| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | -| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | -| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) | -| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) | -| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) | -| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) | -| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) | -| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) | -| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) | -| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) | -| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) | -| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) | -| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) | -| [roles.loadRoles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L32) | [載入角色提示檔並解析 frontmatter](#rolesloadroles) | -| [roles.attackersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L71) | [過濾出攻擊方角色](#rolesattackersof) | -| [roles.defendersOf](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/roles.js#L90) | [過濾出防守方角色](#rolesdefendersof) | -| [templates.toolComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L88) | [產生步驟 2 審查工具留言](#templatestoolcomment) | -| [templates.diffComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L132) | [產生步驟 3 變更摘要留言](#templatesdiffcomment) | -| [templates.rolesComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L173) | [產生步驟 4/6 角色登場留言](#templatesrolescomment) | -| [templates.severeCommentBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L215) | [產生步驟 9 單條嚴重問題留言](#templatesseverecommentbody) | -| [templates.severeReviewBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L247) | [產生步驟 9 嚴重問題 review 總覽](#templatesseverereviewbody) | -| [templates.othersComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L277) | [產生步驟 10 警告+建議彙整表格留言](#templatesotherscomment) | -| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) | -| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) | -| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) | -| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) | -| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) | -| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) | -| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) | -| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) | -| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) | -| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) | -| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) | -| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) | -| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) | -| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) | -| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) | -| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/master/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) | +| [log.taipeiNow](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L20) | [取得台北時區 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#L41) | [取得檔名用時間戳 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#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | +| [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | +| [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | +| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | +| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | +| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | +| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | +| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | +| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | +| [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | +| [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | +| [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | +| [gitea.listLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L141) | [列出存取庫可用標籤](#gitealistlabels) | +| [gitea.createIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L164) | [在存取庫建立 issue(可掛標籤)](#giteacreateissue) | +| [gitea.listIssueComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L184) | [列出 PR 全部一般留言(自動分頁)](#gitealistissuecomments) | +| [gitea.editIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L202) | [編輯既有一般留言](#giteaeditissuecomment) | +| [gitea.createReview](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L223) | [建立 code review 並掛行內留言](#giteacreatereview) | +| [gitea.listReviews](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L243) | [列出 PR 全部 review(自動分頁)](#gitealistreviews) | +| [gitea.listReviewComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L264) | [列出某 review 的全部行內留言](#gitealistreviewcomments) | +| [gitea.tryResolveReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L287) | [盡力將行內留言標記為已解決](#giteatryresolvereviewcomment) | +| [agents.detectTool](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L53) | [依優先序偵測可用的 AI CLI 工具](#agentsdetecttool) | +| [agents.runAgent](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L98) | [非互動執行一次 sub agent 並取回回覆](#agentsrunagent) | +| [agents.extractJson](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/agents.js#L144) | [從 agent 回覆萃取 JSON(容忍雜訊)](#agentsextractjson) | +| [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.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) | +| [templates.issueBody](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L309) | [產生建問題模式的 issue 本文](#templatesissuebody) | +| [templates.issueFindingComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L341) | [產生建問題模式單條問題的 issue 留言](#templatesissuefindingcomment) | +| [templates.nothingToReviewComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/templates.js#L379) | [產生無可審查變更留言](#templatesnothingtoreviewcomment) | +| [review.loadReviewIgnore](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L29) | [讀取 .reviewignore 忽略前綴清單](#reviewloadreviewignore) | +| [review.isIgnored](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L53) | [判斷檔案是否忽略不送審](#reviewisignored) | +| [review.collectDiffRows](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L77) | [整理送審 diff 資料列(含長度上限)](#reviewcollectdiffrows) | +| [review.fillPurposes](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L127) | [以 AI 補齊每個檔案的一行用途描述](#reviewfillpurposes) | +| [review.runAttackers](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L276) | [攻擊方 sub agent 並行找問題並合併列表](#reviewrunattackers) | +| [review.runDefenders](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L448) | [防守方 sub agent 裁決保留或排除](#reviewrundefenders) | +| [review.sortFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L572) | [依嚴重度→檔案→行號排序 findings](#reviewsortfindings) | +| [review.appendExclusions](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L521) | [誤判問題附加到 exclusions.json](#reviewappendexclusions) | +| [review.sortFindingsForIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L596) | [依檔案→嚴重度→行號排序(建問題模式)](#reviewsortfindingsforissue) | +| [review.selectLabels](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L627) | [以 AI 從可用標籤挑選 issue 標籤](#reviewselectlabels) | +| [review.createIssueWithFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L694) | [建 issue 並逐條留言問題明細](#reviewcreateissuewithfindings) | +| [review.resolveOldComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L778) | [將 PR 舊留言標記為解決/過時](#reviewresolveoldcomments) | +| [review.postSevereComments](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/review.js#L855) | [嚴重問題逐條掛行留言(含降級)](#reviewpostseverecomments) | ## 使用範例 diff --git a/src/index.js b/src/index.js index b8680b9..90ab897 100644 --- a/src/index.js +++ b/src/index.js @@ -4,7 +4,7 @@ console.log('================================================'); console.log('Action : AI Code Review'); console.log('用途 : AI 多角色 code review:攻擊方找問題、防守方裁決誤報,結果留言到 PR 並保存 findings'); -console.log('更新時間: 2026/07/17 16:49:21'); +console.log('更新時間: 2026/07/17 18:49:58'); console.log('================================================'); const fs = require('fs'); @@ -169,6 +169,16 @@ async function main() { // 本回合發出的一般留言 id:步驟 8 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); + /** + * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 + * + * @param {string} body 要發布到 PR 的 Markdown 留言內容。 + * @returns {Promise} Gitea API 建立的留言物件;至少預期包含 `id`。 + * @remarks + * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 + * 警告/建議彙整等一般留言。若 Gitea API 失敗,例外會往上拋出並由 + * 主流程頂層 catch 收斂。 + */ const postComment = async (body) => { const created = await gitea.createIssueComment(ctx, body); currentRunCommentIds.add(created.id); -- 2.53.0 From c4c23d45314bb260d7ec350db4f968021076ee17 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Fri, 17 Jul 2026 19:02:42 +0800 Subject: [PATCH 2/8] =?UTF-8?q?fix(gitrepo):=20=E8=A3=9C=E6=8A=93=E6=B7=BA?= =?UTF-8?q?=E5=B1=A4=20checkout=20=E7=9A=84=20base=20=E6=AD=B7=E5=8F=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- readme.md | 12 ++++++------ src/lib/gitrepo.js | 48 ++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 14 deletions(-) diff --git a/readme.md b/readme.md index 244c223..35df151 100644 --- a/readme.md +++ b/readme.md @@ -81,12 +81,12 @@ flowchart TD | [log.taipeiFromIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L60) | [將 ISO 時間字串轉為台北時區顯示字串](#logtaipeifromiso) | | [log.log](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/log.js#L83) | [以統一格式輸出一行日誌](#loglog) | | [context.loadContext](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/context.js#L64) | [彙整 runner 環境變數與事件 payload 為執行上下文](#contextloadcontext) | -| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L60) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | -| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L81) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | -| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L104) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | -| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L125) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | -| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L143) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | -| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L181) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | +| [gitrepo.latestCommitSubject](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L79) | [取得最新 commit 的訊息標題](#gitrepolatestcommitsubject) | +| [gitrepo.resolveMergeBase](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L100) | [解析 base 分支與 HEAD 的 merge-base](#gitreporesolvemergebase) | +| [gitrepo.changedFiles](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L136) | [列出 base 與 HEAD 之間有變更的檔案](#gitrepochangedfiles) | +| [gitrepo.fileDiff](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L157) | [取得單一檔案的 git diff 內容](#gitrepofilediff) | +| [gitrepo.fileLastUpdatedIso](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L175) | [取得檔案最後一次 commit 的 ISO 時間](#gitrepofilelastupdatediso) | +| [gitrepo.commitAndPushFindings](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitrepo.js#L213) | [以 bot 身分 commit 結果檔並 push 回 PR 來源分支](#gitrepocommitandpushfindings) | | [gitea.whoAmI](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L87) | [取得 token 對應的使用者(bot 身分)](#giteawhoami) | | [gitea.createCommentOnIssue](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L106) | [對指定編號 issue/PR 新增一般留言](#giteacreatecommentonissue) | | [gitea.createIssueComment](https://gitea.jsc.idv.tw/node-actions/ai-code-review/src/branch/develop/src/lib/gitea.js#L124) | [對本次 PR 新增一般留言](#giteacreateissuecomment) | diff --git a/src/lib/gitrepo.js b/src/lib/gitrepo.js index 3ecc5ee..b7c2066 100644 --- a/src/lib/gitrepo.js +++ b/src/lib/gitrepo.js @@ -44,6 +44,25 @@ function gitTrim(cwd, ...args) { return git(cwd, ...args).trim(); } +/** + * 嘗試同步執行 git 指令,失敗時回傳 false,成功時回傳 true。 + * + * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 + * @param {...string} args - 傳給 git 的參數。 + * @returns {boolean} git 指令是否成功結束。 + * @remarks + * 使用情境:修復淺層 checkout 的歷史不足時,部分 fetch 策略可能因 runner + * 或遠端版本不同而失敗;呼叫端可依序嘗試多種策略,不讓第一個失敗中斷流程。 + */ +function tryGit(cwd, ...args) { + try { + git(cwd, ...args); + return true; + } catch { + return false; + } +} + /** * 取得目前 HEAD 最新一筆 commit 的訊息標題(commit message 第一行)。 * @@ -64,14 +83,14 @@ function latestCommitSubject(cwd) { /** * 解析 PR base 分支與目前 HEAD 的 merge-base commit SHA。 * - * 先嘗試 `git fetch origin ` 更新 base 分支資料(失敗時靜默忽略, - * 因 fetch-depth: 0 的 checkout 通常已含 base 分支,可直接沿用本地資料), - * 再以 `git merge-base origin/ HEAD` 取得共同祖先。 + * 先以 refspec 明確更新 `origin/`,再以 `git merge-base origin/ HEAD` + * 取得共同祖先。若 checkout 是淺層歷史而導致 merge-base 失敗,會補抓完整或更深的 + * base/head 歷史後重試,避免 PR workflow 因 checkout 預設深度不足而中斷。 * * @param {string} cwd - git 工作目錄(repo 的 checkout 路徑)。 * @param {string} baseRef - PR 目標(base)分支名稱,例如 'master' 或 'develop';不含 'origin/' 前綴。 * @returns {string} merge-base 的 commit SHA(40 碼十六進位字串)。 - * @throws {Error} 本地不存在 origin/、或兩者無共同祖先時,`git merge-base` 失敗並拋出(fetch 失敗不會拋出)。 + * @throws {Error} 補抓歷史後仍無法取得共同祖先時,丟出含 baseRef 的明確錯誤。 * @remarks * 使用情境:AI code review 以此結果作為 diff 比較基準—— * 先 `resolveMergeBase(cwd, pr.base.ref)` 取得基準 SHA, @@ -79,12 +98,25 @@ function latestCommitSubject(cwd) { * 避免把 base 分支後續演進誤算進 diff。 */ function resolveMergeBase(cwd, baseRef) { + const remoteBase = `origin/${baseRef}`; + tryGit(cwd, 'fetch', '--no-tags', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); try { - git(cwd, 'fetch', 'origin', baseRef); - } catch { - // fetch-depth: 0 的 checkout 通常已含 base 分支,抓不到時直接沿用本地資料。 + return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); + } catch (firstError) { + const isShallow = gitTrim(cwd, 'rev-parse', '--is-shallow-repository') === 'true'; + if (isShallow) { + tryGit(cwd, 'fetch', '--no-tags', '--unshallow', 'origin'); + } + tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', `+refs/heads/${baseRef}:refs/remotes/${remoteBase}`); + tryGit(cwd, 'fetch', '--no-tags', '--deepen=1000', 'origin', 'HEAD'); + try { + return gitTrim(cwd, 'merge-base', remoteBase, 'HEAD'); + } catch { + const error = new Error(`無法解析 origin/${baseRef} 與 HEAD 的 merge-base;請確認 checkout 有足夠歷史,或設定 checkout fetch-depth: 0。`); + error.cause = firstError; + throw error; + } } - return gitTrim(cwd, 'merge-base', `origin/${baseRef}`, 'HEAD'); } /** -- 2.53.0 From ccbe615c2a424e0bcb4ca53a4dadf35058e8d7f6 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Fri, 17 Jul 2026 11:03:14 +0000 Subject: [PATCH 3/8] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/findings/2026-07-17-19:03:13.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-17-19:03:13.json diff --git a/.gitea/ai-review/findings/2026-07-17-19:03:13.json b/.gitea/ai-review/findings/2026-07-17-19:03:13.json new file mode 100644 index 0000000..c70d864 --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-17-19:03:13.json @@ -0,0 +1,12 @@ +{ + "generatedAt": "2026/07/17 19:03:13", + "commitSha": "c4c23d45314bb260d7ec350db4f968021076ee17", + "prNumber": 4, + "tool": { + "name": "codex", + "version": "codex-cli 0.144.5", + "model": "(工具預設)" + }, + "findings": [], + "excluded": [] +} -- 2.53.0 From d751cee69d735efcc7aa96c36a69c3b798398985 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 09:46:12 +0800 Subject: [PATCH 4/8] =?UTF-8?q?refactor(=E5=AF=A9=E6=9F=A5=E6=B5=81?= =?UTF-8?q?=E7=A8=8B):=20=E5=B0=87=E8=88=8A=E7=95=99=E8=A8=80=E6=A8=99?= =?UTF-8?q?=E8=A8=98=E7=A7=BB=E5=88=B0=E5=81=B5=E6=B8=AC=20AI=20=E5=B7=A5?= =?UTF-8?q?=E5=85=B7=E4=B9=8B=E5=89=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 52 +++++++++++++++++++++++++++------------------------- 1 file changed, 27 insertions(+), 25 deletions(-) diff --git a/src/index.js b/src/index.js index 90ab897..b4c86d0 100644 --- a/src/index.js +++ b/src/index.js @@ -42,9 +42,9 @@ const BOT_COMMIT_PREFIX = 'chore: update ai-review findings [ai-review-bot]'; * @param {Array} params.excluded - 被裁決為誤報而排除的問題清單。 * @returns {string} findings JSON 檔相對於 repo 根目錄的路徑(例如 `.gitea/ai-review/findings/xxx.json`)。 * @remarks - * 使用情境:`main()` 步驟 7 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果, + * 使用情境:`main()` 步驟 8 於防守方裁決、`review.sortFindings(kept)` 排序後呼叫本函式保存結果, * 再將回傳的相對路徑交給 `commitFindings` commit 並 push 回 PR 來源分支; - * 另在步驟 3 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。 + * 另在步驟 4 判定無可審查變更時,也會以空清單保存一份空 findings 後以 success 收場。 * 本函式無 try/catch,檔案系統錯誤會往上拋出,由 `main().catch` 以 exit code 1 收場。 */ function saveFindings({ cwd, ctx, tool, kept, excluded }) { @@ -61,7 +61,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { }; fs.writeFileSync(findingsPath, `${JSON.stringify(payload, null, 2)}\n`, 'utf8'); const relativePath = path.relative(cwd, findingsPath); - log('步驟7', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`); + log('步驟8', 'INF', `findings 已保存:${relativePath}(保留 ${kept.length} 條、排除 ${excluded.length} 條)。`); return relativePath; } @@ -88,7 +88,7 @@ function saveFindings({ cwd, ctx, tool, kept, excluded }) { * @remarks * 使用情境:`main()` 於流程尾端依 `severe.length === 0 ? 'success' : 'failure'` 決定 result、 * 依模式組出 filesToCommit(一般模式:findings 檔+有變更時的 exclusions.json; - * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 3 判定無可審查變更且非建問題模式時, + * 建問題模式:只有 exclusions.json)後呼叫本函式;另在步驟 4 判定無可審查變更且非建問題模式時, * 也會以 result: 'success' 提交空 findings。 * 注意 commit 訊息與模組常數 `BOT_COMMIT_PREFIX` 耦合,修改前綴會使步驟 1 的快速回報失效。 */ @@ -119,13 +119,13 @@ function commitFindings({ cwd, ctx, files, result }) { * * 流程概要: * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 偵測 AI 工具(antigravity/codex/claude)並留言; - * 3. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, + * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言); + * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; + * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, * 一般模式 commit success、建問題模式略過 commit,回傳 0); - * 4–5. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; - * 6–7. 防守方登場留言、裁決誤報後排序並保存 findings JSON, + * 5–6. 攻擊方登場留言、每位攻擊方一個 sub agent 並行找問題; + * 7–8. 防守方登場留言、裁決誤報後排序並保存 findings JSON, * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json; - * 8. 將 PR 既有舊留言標記為解決(跳過本回合留言); * 9. 嚴重問題逐條掛在程式碼行上留言; * 10. 警告+建議彙整為單一表格留言; * 建問題模式(input: create-issue):保留問題另建 issue(createIssueWithFindings)逐條留言明細; @@ -167,7 +167,7 @@ async function main() { return 1; } - // 本回合發出的一般留言 id:步驟 8 標註過時時要跳過這些。 + // 本回合發出的一般留言 id:步驟 2 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); /** * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 @@ -185,13 +185,18 @@ async function main() { return created; }; - // ── 步驟 2:偵測 AI agent 工具並留言 ────────────────────────────────── + // ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── + // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時; + // 此時本回合尚未發出任何留言(currentRunCommentIds 為空),之後發出的留言自然不受影響。 + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + + // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); if (!tool) { - log('步驟2', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。'); + log('步驟3', 'ERR', '找不到可用的 AI 工具(antigravity/codex/claude)。'); return 1; } - log('步驟2', 'INF', `選用工具:${tool.name}(${tool.version})。`); + log('步驟3', 'INF', `選用工具:${tool.name}(${tool.version})。`); const runLink = `${ctx.serverUrl}/${ctx.repository}/actions/runs/${ctx.runId}`; await postComment( templates.toolComment({ @@ -204,13 +209,13 @@ async function main() { }), ); - // ── 步驟 3:讀取 .reviewignore、整理 git diff 並留言 ─────────────────── + // ── 步驟 4:讀取 .reviewignore、整理 git diff 並留言 ─────────────────── const ignores = review.loadReviewIgnore(cwd); const base = gitrepo.resolveMergeBase(cwd, ctx.baseRef); const allFiles = gitrepo.changedFiles(cwd, base); const files = allFiles.filter((file) => !review.isIgnored(file, ignores)); const ignoredCount = allFiles.length - files.length; - log('步驟3', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); + log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); if (files.length === 0) { // 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。 @@ -229,20 +234,20 @@ async function main() { await review.fillPurposes({ tool, model: ctx.model, cwd, diffRows }); await postComment(templates.diffComment(diffRows, ignoredCount)); - // ── 步驟 4:攻擊方角色登場留言 ───────────────────────────────────────── + // ── 步驟 5:攻擊方角色登場留言 ───────────────────────────────────────── const roles = loadRoles(path.join(ctx.actionPath, 'src', 'prompts', 'roles')); const attackers = attackersOf(roles); const defenders = defendersOf(roles); - log('步驟4', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); + log('步驟5', 'INF', `攻擊方 ${attackers.length} 位、防守方 ${defenders.length} 位。`); await postComment(templates.rolesComment({ title: '⚔️ 攻擊方登場', roles: attackers })); - // ── 步驟 5:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── + // ── 步驟 6:每個攻擊方一個 sub agent 並行分析,合併問題列表 ──────────── const findings = await review.runAttackers({ tool, model: ctx.model, cwd, attackers, diffRows }); - // ── 步驟 6:防守方角色登場留言 ───────────────────────────────────────── + // ── 步驟 7:防守方角色登場留言 ───────────────────────────────────────── await postComment(templates.rolesComment({ title: '🛡️ 防守方登場', roles: defenders })); - // ── 步驟 7:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── + // ── 步驟 8:防守方裁決 → 排除 → 排序 → 保存 findings ────────────────── const { kept, excluded } = await review.runDefenders({ tool, model: ctx.model, cwd, defenders, findings }); review.sortFindings(kept); const relativePath = saveFindings({ cwd, ctx, tool, kept, excluded }); @@ -250,13 +255,10 @@ async function main() { // 誤判/重複的問題附加到 exclusions.json(之後與審查結果一起 commit)。 const exclusionsChanged = review.appendExclusions({ cwd, excluded, prNumber: ctx.prNumber }); - // ── 步驟 7(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─ + // ── 步驟 8(分組):依嚴重等級分組(嚴重/警告+建議),組內已依檔案與行數排序 ─ const severe = kept.filter((finding) => finding.severity === '嚴重'); const others = kept.filter((finding) => finding.severity !== '嚴重'); - log('步驟7', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); - - // ── 步驟 8:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── - await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); // ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)────────────── if (severe.length > 0) { -- 2.53.0 From 8662e8ca801c3dbf7f74ed7e758a7a55176735b8 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 09:46:12 +0800 Subject: [PATCH 5/8] =?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 排除), * 直接以本留言告知開發者本回合視為審查通過,不再進入 * 攻擊方/防守方審查階段。 -- 2.53.0 From af6b9a197f586bc12b6b18e7966f344c19e37161 Mon Sep 17 00:00:00 2001 From: ai-review-bot Date: Mon, 20 Jul 2026 01:46:49 +0000 Subject: [PATCH 6/8] chore: update ai-review findings [ai-review-bot][success] --- .gitea/ai-review/findings/2026-07-20-09:46:49.json | 12 ++++++++++++ 1 file changed, 12 insertions(+) create mode 100644 .gitea/ai-review/findings/2026-07-20-09:46:49.json diff --git a/.gitea/ai-review/findings/2026-07-20-09:46:49.json b/.gitea/ai-review/findings/2026-07-20-09:46:49.json new file mode 100644 index 0000000..7d9d6ec --- /dev/null +++ b/.gitea/ai-review/findings/2026-07-20-09:46:49.json @@ -0,0 +1,12 @@ +{ + "generatedAt": "2026/07/20 09:46:49", + "commitSha": "8662e8ca801c3dbf7f74ed7e758a7a55176735b8", + "prNumber": 4, + "tool": { + "name": "claude", + "version": "2.1.215 (Claude Code)", + "model": "(工具預設)" + }, + "findings": [], + "excluded": [] +} -- 2.53.0 From 91fff79f22f902f54ba5fe32d91d2a3480ac7208 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 11:24:48 +0800 Subject: [PATCH 7/8] =?UTF-8?q?feat(=E5=BB=BA=E5=95=8F=E9=A1=8C=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F):=20=E5=AF=A9=E6=9F=A5=E7=95=99=E8=A8=80=E6=94=B9?= =?UTF-8?q?=E7=99=BC=E5=88=B0=20issue=E3=80=81=E8=B7=B3=E9=81=8E=E8=88=8A?= =?UTF-8?q?=E7=95=99=E8=A8=80=E8=99=95=E7=90=86=E4=B8=A6=E5=9B=9E=E8=B2=BC?= =?UTF-8?q?=20PR=20=E9=80=A3=E7=B5=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.js | 123 ++++++++++++++++++++++++++++++++++--------- src/lib/gitea.js | 39 ++++++++++---- src/lib/review.js | 100 ++++++++--------------------------- src/lib/templates.js | 52 ++++++++++++++++-- 4 files changed, 199 insertions(+), 115 deletions(-) diff --git a/src/index.js b/src/index.js index b4c86d0..72d6237 100644 --- a/src/index.js +++ b/src/index.js @@ -117,9 +117,9 @@ function commitFindings({ cwd, ctx, files, result }) { /** * AI code review 主流程:依固定 10 步驟執行多角色審查,回傳 process exit code。 * - * 流程概要: + * 流程概要(步驟 2~10 描述一般模式;建問題模式差異見末段): * 1. 快速回報 — 最新 commit 若為 ai-review-bot 的結果 commit([success]/[failure]),直接回報 0/1 不重審; - * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言); + * 2. 將 PR 既有舊留言標記為解決(跳過本回合留言;建問題模式不執行此步); * 3. 偵測 AI 工具(antigravity/codex/claude)並留言; * 4. 讀 .reviewignore、整理 git diff 並留言(無可審查變更時:留言+保存空 findings, * 一般模式 commit success、建問題模式略過 commit,回傳 0); @@ -128,7 +128,10 @@ function commitFindings({ cwd, ctx, files, result }) { * 並以 appendExclusions 把誤判/重複問題回寫 .gitea/ai-review/exclusions.json; * 9. 嚴重問題逐條掛在程式碼行上留言; * 10. 警告+建議彙整為單一表格留言; - * 建問題模式(input: create-issue):保留問題另建 issue(createIssueWithFindings)逐條留言明細; + * 建問題模式(input: create-issue):不執行步驟 2、不觸碰 PR 既有留言;步驟 3~10 的所有留言 + * 改發到追蹤 issue(工具/diff/角色留言先暫存,確定有保留問題後才建立 issue 並一次寫入, + * 嚴重問題與警告+建議亦發到該 issue);無保留問題則不建 issue、僅在 PR 留審查通過提示; + * 收束時依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結形成雙向關聯; * 收尾:組 filesToCommit —— 一般模式 commit findings 檔(+有變更的 exclusions.json)、 * 建問題模式只 commit exclusions.json、無檔案可 commit 時略過; * commit 訊息帶結果標記(success=無嚴重問題、failure=有嚴重問題)。 @@ -167,28 +170,60 @@ async function main() { return 1; } - // 本回合發出的一般留言 id:步驟 2 標註過時時要跳過這些。 + // 本回合(一般模式)發出的 PR 留言 id:resolveOldComments 標註過時時要跳過這些。 const currentRunCommentIds = new Set(); + // 建問題模式:issue 於「確定有保留問題」後才建立;在那之前的情境留言(工具/diff/角色) + // 先暫存於 issueBuffer,建立 issue 後一次寫入。 + const issueBuffer = []; + let issue = null; /** - * 建立本回合 PR 一般留言並記錄留言 id,供後續舊留言處理排除。 + * 發布一則審查留言。依模式決定去向: + * - 一般模式:發到 PR,並記錄留言 id 供 `resolveOldComments` 排除。 + * - 建問題模式:issue 已建立時發到 issue;尚未建立時先暫存到 `issueBuffer`。 * - * @param {string} body 要發布到 PR 的 Markdown 留言內容。 - * @returns {Promise} Gitea API 建立的留言物件;至少預期包含 `id`。 + * @param {string} body 要發布的 Markdown 留言內容。 + * @returns {Promise} 一般模式、或建問題模式且 issue 已建立時回傳 Gitea 留言物件; + * 建問題模式尚未建立 issue 而先暫存時回傳 null。 * @remarks * 使用情境:只在 `main()` 內部使用,處理工具資訊、diff 摘要、角色登場與 - * 警告/建議彙整等一般留言。若 Gitea API 失敗,例外會往上拋出並由 - * 主流程頂層 catch 收斂。 + * 警告/建議彙整等留言。若 Gitea API 失敗,例外會往上拋出並由主流程頂層 catch 收斂。 */ const postComment = async (body) => { + if (ctx.createIssue) { + if (issue) return gitea.createCommentOnIssue(ctx, issue.number, body); + issueBuffer.push(body); + return null; + } const created = await gitea.createIssueComment(ctx, body); currentRunCommentIds.add(created.id); return created; }; + /** + * 建問題模式:建立追蹤 issue(標題=PR 標題、本文=PR 描述+回溯 PR 的引言,先不掛標籤), + * 並把 `issueBuffer` 內暫存的情境留言依序寫入 issue;設定閉包變數 `issue` 供後續留言直接發到 issue。 + * 僅於「確定有保留問題」時呼叫一次。 + * + * @returns {Promise} 無回傳值;結果反映在閉包變數 `issue` 與 issue 留言。 + */ + const ensureIssueCreated = async () => { + issue = await gitea.createIssue(ctx, { + title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, + body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), + labels: [], + }); + log('建問題', 'INF', `已建立追蹤 issue #${issue.number},寫入 ${issueBuffer.length} 則情境留言。`); + for (const body of issueBuffer) { + await gitea.createCommentOnIssue(ctx, issue.number, body); + } + issueBuffer.length = 0; + }; // ── 步驟 2:將 PR 既有留言標記為解決(本回合留言除外)─────────────────── - // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時; - // 此時本回合尚未發出任何留言(currentRunCommentIds 為空),之後發出的留言自然不受影響。 - await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + // 僅一般模式執行;建問題模式不觸碰 PR 既有留言(審查內容改發到 issue)。 + // 早於偵測工具與所有本回合留言:先把上一回合的 bot 留言標為過時(此時尚無本回合留言)。 + if (!ctx.createIssue) { + await review.resolveOldComments({ ctx, gitea, currentRunCommentIds }); + } // ── 步驟 3:偵測 AI agent 工具並留言 ────────────────────────────────── const tool = agents.detectTool(); @@ -218,8 +253,13 @@ async function main() { log('步驟4', 'INF', `變更檔案 ${allFiles.length} 個,套用 .reviewignore 後送審 ${files.length} 個(排除 ${ignoredCount} 個)。`); if (files.length === 0) { - // 沒有可審查的變更:留言說明、保存空 findings、以 success 收場。 - await postComment(templates.nothingToReviewComment(ignoredCount)); + // 沒有可審查的變更:告知、保存空 findings、以 success 收場。 + // 建問題模式無問題可追蹤 → 不建 issue,直接在 PR 告知(暫存的情境留言捨棄)。 + if (ctx.createIssue) { + await gitea.createIssueComment(ctx, templates.nothingToReviewComment(ignoredCount)); + } else { + await postComment(templates.nothingToReviewComment(ignoredCount)); + } const relativePath = saveFindings({ cwd, ctx, tool, kept: [], excluded: [] }); if (ctx.createIssue) { // 建問題模式下 findings 不進版控,且 exclusions.json 無變更 → 沒東西可提交。 @@ -260,24 +300,59 @@ async function main() { const others = kept.filter((finding) => finding.severity !== '嚴重'); log('步驟8', 'INF', `分組結果:嚴重 ${severe.length} 條、警告+建議 ${others.length} 條。`); - // ── 步驟 9:嚴重問題逐條掛在程式碼行上留言(開發者可回覆)────────────── - if (severe.length > 0) { - await review.postSevereComments({ ctx, gitea, severe, cwd }); + // ── 建問題模式:確定有保留問題才建立 issue,並把暫存的情境留言一次寫入; + // 無保留問題則不建 issue,改在 PR 留一則審查通過提示。 ────────────────── + if (ctx.createIssue) { + if (kept.length > 0) { + await ensureIssueCreated(); + } else { + log('建問題', 'INF', '沒有保留的問題,略過建立 issue,於 PR 留審查通過提示。'); + await gitea.createIssueComment(ctx, templates.noFindingsComment()); + } } - // ── 步驟 10:警告+建議彙整為單一表格留言 ────────────────────────────── + // ── 步驟 9:嚴重問題留言(一般模式掛在 PR 程式碼行上;建問題模式發到 issue)─ + if (severe.length > 0) { + if (ctx.createIssue) { + await review.postSevereToIssue({ ctx, gitea, issueNumber: issue.number, severe }); + } else { + await review.postSevereComments({ ctx, gitea, severe, cwd }); + } + } + + // ── 步驟 10:警告+建議彙整為單一表格留言(去向由 postComment 依模式決定)── if (others.length > 0) { await postComment(templates.othersComment(others)); log('步驟10', 'INF', `警告+建議表格留言已發布(${others.length} 條)。`); } - // ── 建問題模式(input: create-issue):另建 issue 逐條留言問題明細 ────── - if (ctx.createIssue) { - if (kept.length > 0) { - await review.createIssueWithFindings({ ctx, gitea, tool, model: ctx.model, cwd, findings: kept }); - } else { - log('建問題', 'INF', '沒有保留的問題,略過建立 issue。'); + // ── 建問題模式收束:依保留問題補掛 issue 標籤,並在 PR 回貼 issue 連結(雙向關聯)─ + if (ctx.createIssue && issue) { + try { + const labels = await gitea.listLabels(ctx); + const labelIds = await review.selectLabels({ + tool, + model: ctx.model, + cwd, + labels, + prTitle: ctx.prTitle, + prBody: ctx.prBody, + findings: kept, + }); + await gitea.addLabelsToIssue(ctx, issue.number, labelIds); + } catch (err) { + log('建問題', 'WRN', `補掛標籤失敗(${err.message}),issue 不掛標籤。`); } + await gitea.createIssueComment( + ctx, + templates.issueLinkComment({ + issueNumber: issue.number, + issueUrl: issue.html_url, + severeCount: severe.length, + otherCount: others.length, + }), + ); + log('建問題', 'INF', `issue #${issue.number} 已寫入審查內容,並在 PR 回貼連結。`); } // ── 收尾:commit 並 push(success=無嚴重問題、failure=有嚴重問題)─────── diff --git a/src/lib/gitea.js b/src/lib/gitea.js index ef71722..91d0c29 100644 --- a/src/lib/gitea.js +++ b/src/lib/gitea.js @@ -99,8 +99,8 @@ function whoAmI(ctx) { * @returns {Promise} 建立成功的留言物件(含 `id`、`body`、`user` 等欄位, * 依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 建立 issue 後,逐條把 finding 明細留言到該 issue; + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 建立 issue 後, + * 把工具/diff/角色情境留言與 `review.postSevereToIssue` 的嚴重問題明細留言到該 issue; * 另外 `createIssueComment` 也委派本函式對 `ctx.prNumber` 留言。 */ function createCommentOnIssue(ctx, issueNumber, body) { @@ -134,9 +134,9 @@ function createIssueComment(ctx, body) { * @returns {Promise} 標籤物件陣列(每筆含 `id`、`name`、`color` 等欄位, * 依 Gitea API 回應而定);存取庫無標籤時為空陣列。 * @throws {Error} 任一頁請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 先以本函式取得可用標籤,再交給 `selectLabels` - * 讓 AI 從中挑選適合掛在新 issue 上的標籤子集合。 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 收束時 + * 先以本函式取得可用標籤,再交給 `review.selectLabels` 讓 AI 挑出適合的標籤子集合, + * 最後以 `addLabelsToIssue` 補掛到追蹤 issue 上。 */ function listLabels(ctx) { return listAll(ctx, `/repos/${ctx.owner}/${ctx.repo}/labels`); @@ -156,10 +156,9 @@ function listLabels(ctx) { * @returns {Promise} 建立成功的 issue 物件(含 `number`、`title`、 * `html_url` 等欄位,依 Gitea API 回應而定)。 * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 - * @remarks 使用情境:建問題模式(input: create-issue)下, - * `createIssueWithFindings` 以 PR 標題/描述為 issue 標題與本文、 - * 配上 `selectLabels` 挑出的標籤 id,呼叫本函式建立追蹤問題的 issue, - * 再逐條把 finding 明細留言到該 issue。 + * @remarks 使用情境:建問題模式(input: create-issue)下,`main()` 的 `ensureIssueCreated` + * 以 PR 標題/描述為 issue 標題與本文(先不掛標籤)呼叫本函式建立追蹤問題的 issue, + * 之後再把審查內容留言到該 issue、並以 `addLabelsToIssue` 補掛標籤。 */ function createIssue(ctx, { title, body, labels }) { return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues`, { @@ -169,6 +168,27 @@ function createIssue(ctx, { title, body, labels }) { }); } +/** + * 為既有 issue 追加標籤(不影響既有標籤)。 + * 對應 endpoint:`POST /repos/{owner}/{repo}/issues/{issueNumber}/labels`。 + * `labels` 為空陣列時不呼叫 API、直接回傳 `null`(省一次無意義請求)。 + * + * @param {object} ctx - 執行環境 context。必要欄位:`apiBase`、`token`、 + * `owner`(repo 擁有者)、`repo`(repo 名稱)。 + * @param {number|string} issueNumber - 目標 issue 編號。 + * @param {number[]} labels - 要追加的標籤 id 陣列。 + * @returns {Promise} 追加後該 issue 的標籤陣列(依 Gitea API 回應而定); + * `labels` 為空時回傳 `null`(未發出請求)。 + * @throws {Error} 請求失敗(非 2xx)由底層 `api` 丟出,錯誤附 `status`、`data`。 + * @remarks 使用情境:建問題模式(input: create-issue)下先以空標籤建立 issue、 + * 待防守方裁決得到保留問題後,再以 `selectLabels` 挑出的標籤 id 呼叫本函式補掛, + * 讓標籤挑選能參考最終的問題清單。 + */ +function addLabelsToIssue(ctx, issueNumber, labels) { + if (!labels || labels.length === 0) return Promise.resolve(null); + return api(ctx, 'POST', `/repos/${ctx.owner}/${ctx.repo}/issues/${issueNumber}/labels`, { labels }); +} + /** * 列出 PR 上的全部一般留言(自動分頁撈取,每頁 50 筆直到取完)。 * 對應 endpoint:`GET /repos/{owner}/{repo}/issues/{prNumber}/comments`。 @@ -303,6 +323,7 @@ module.exports = { createCommentOnIssue, listLabels, createIssue, + addLabelsToIssue, listIssueComments, editIssueComment, createReview, diff --git a/src/lib/review.js b/src/lib/review.js index cf504f8..73302b5 100644 --- a/src/lib/review.js +++ b/src/lib/review.js @@ -578,30 +578,6 @@ function sortFindings(findings) { ); } -/** - * 建問題模式的就地排序:依檔案路徑、再依嚴重等級(嚴重→警告→建議)、再依起始行遞增。 - * - * 與 {@link sortFindings}(嚴重度優先)不同,本排序以檔案路徑為第一鍵, - * 讓 issue 上逐條留言的問題「同檔集中」,便於開發者逐檔處理。 - * 嚴重等級權重取自 `templates.SEVERITY_ORDER`;未知等級排最後。 - * 注意:直接修改傳入陣列(in-place),無回傳值。 - * - * @param {Array<{file: string, severity: string, startLine: number}>} findings - 要排序的 finding 陣列(通常為保留問題 `kept` 的複本)。 - * @returns {void} 無回傳值;排序結果反映在傳入陣列本身。 - * @remarks - * 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings} - * 先以 `[...findings]` 複製保留問題(不動原陣列的嚴重度排序), - * 再對複本呼叫本函式,依「檔案→嚴重度→行號」的順序逐條留言到新 issue。 - */ -function sortFindingsForIssue(findings) { - findings.sort( - (a, b) => - a.file.localeCompare(b.file) || - (templates.SEVERITY_ORDER[a.severity] ?? 9) - (templates.SEVERITY_ORDER[b.severity] ?? 9) || - a.startLine - b.startLine, - ); -} - /** * 建問題模式:以 AI 依 PR 標題/描述與問題列表摘要, * 從存取庫可用標籤中挑選適合掛在追蹤 issue 上的標籤子集合。 @@ -620,9 +596,9 @@ function sortFindingsForIssue(findings) { * @param {Array} params.findings - 保留的問題列表;每條取 severity/focus/file 與截斷 120 字的 problem 作為挑選依據。 * @returns {Promise} 挑中的標籤 id 陣列(可用標籤的子集合);無適合標籤或任何失敗時為空陣列。 * @remarks - * 使用情境:建問題模式(input: create-issue)下,{@link createIssueWithFindings} - * 先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式取得標籤 id 子集合, - * 傳給 `gitea.createIssue` 讓新 issue 自動掛上合適標籤。 + * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js) + * 在建立追蹤 issue 後先呼叫 `gitea.listLabels` 取得可用標籤,再以本函式依保留問題 + * 挑出標籤 id 子集合,交給 `gitea.addLabelsToIssue` 補掛到 issue 上。 */ async function selectLabels({ tool, model, cwd, labels, prTitle, prBody, findings }) { if (labels.length === 0) return []; @@ -670,61 +646,30 @@ ${JSON.stringify(brief)} } /** - * 建問題模式:把保留的審查問題建成存取庫的追蹤 issue 並逐條留言明細。 + * 建問題模式:把嚴重 findings 逐條以一般留言發到追蹤 issue。 * - * 流程:AI 挑標籤(`listLabels` + {@link selectLabels},失敗不掛標籤) - * → 建立 issue(標題=PR 標題、本文=PR 描述加追溯資訊;失敗記 ERR 並回傳 null 不阻斷主流程) - * → 複製 findings 依「檔案路徑→嚴重等級→起始行」排序({@link sortFindingsForIssue}) - * → 逐條以 `templates.issueFindingComment` 留言到 issue。 + * issue 無法把留言掛在程式碼行上(沒有 diff 定位),故改以 + * {@link templates.issueFindingComment} 在內文標明位置逐條發布—— + * 等同一般模式 PR 步驟 9 的嚴重問題,改以 issue 留言呈現。 + * findings 由呼叫端事先以 {@link sortFindings} 排序(嚴重度→檔案→行號),本函式不再排序。 * * @param {Object} params - 解構參數。 - * @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);使用 `prNumber`、`prTitle`、`prBody` 及 Gitea API 認證欄位。 - * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `listLabels`、`createIssue`、`createCommentOnIssue`。 - * @param {Object} params.tool - `detectTool()` 偵測到的 AI CLI 工具描述物件(挑標籤用)。 - * @param {string} params.model - 指定 AI 模型名稱;空字串=工具預設。 - * @param {string} params.cwd - agent 的工作目錄(repo 根目錄)。 - * @param {Array} params.findings - 要寫進 issue 的問題列表(通常為防守方裁決後保留的 `kept`);本函式以複本排序,不改動原陣列順序。 - * @returns {Promise} 建立成功的 Gitea issue 物件(含 `number` 等欄位);建立 issue 失敗時為 null。 - * @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出;列標籤與建 issue 的失敗則已於函式內降級處理。 + * @param {Object} params.ctx - 執行環境 context(`loadContext()` 回傳);供 Gitea API 認證。 + * @param {Object} params.gitea - Gitea API 模組(src/lib/gitea.js);以參數注入便於測試替換,使用 `createCommentOnIssue`。 + * @param {number} params.issueNumber - 目標追蹤 issue 的編號。 + * @param {Array} params.severe - severity 為「嚴重」的 finding 列表(已排序;呼叫端保證非空)。 + * @returns {Promise} 無回傳值;結果反映在 issue 留言與日誌。 + * @throws {Error} 逐條留言(`createCommentOnIssue`)失敗時未攔截、向上拋出,由主流程頂層 catch 收斂。 * @remarks - * 使用情境:`main()`(src/index.js)在步驟 10 之後、收尾之前, - * 於 `ctx.createIssue` 為 true 且 `kept.length > 0` 時呼叫本函式; - * 此模式下問題明細已保存在 issue 留言,收尾只 commit exclusions.json、findings 檔不進版控。 + * 使用情境:建問題模式(input: create-issue)下,`main()`(src/index.js)於防守方裁決後 + * 建立追蹤 issue、寫入情境留言,再以本函式把嚴重問題逐條留言到該 issue; + * 警告+建議則以 {@link templates.othersComment} 彙整成單一表格另發到同一 issue。 */ -async function createIssueWithFindings({ ctx, gitea, tool, model, cwd, findings }) { - let labelIds = []; - try { - const labels = await gitea.listLabels(ctx); - labelIds = await selectLabels({ - tool, - model, - cwd, - labels, - prTitle: ctx.prTitle, - prBody: ctx.prBody, - findings, - }); - } catch (err) { - log('建問題', 'WRN', `取得存取庫標籤失敗(${err.message}),issue 不掛標籤。`); +async function postSevereToIssue({ ctx, gitea, issueNumber, severe }) { + for (const finding of severe) { + await gitea.createCommentOnIssue(ctx, issueNumber, templates.issueFindingComment(finding)); } - let issue; - try { - issue = await gitea.createIssue(ctx, { - title: ctx.prTitle || `AI Code Review:PR #${ctx.prNumber}`, - body: templates.issueBody({ prNumber: ctx.prNumber, prBody: ctx.prBody }), - labels: labelIds, - }); - } catch (err) { - log('建問題', 'ERR', `建立 issue 失敗:${err.message}。`); - return null; - } - const sorted = [...findings]; - sortFindingsForIssue(sorted); - for (const finding of sorted) { - await gitea.createCommentOnIssue(ctx, issue.number, templates.issueFindingComment(finding)); - } - log('建問題', 'INF', `issue #${issue.number} 已建立並逐條留言 ${sorted.length} 條問題。`); - return issue; + log('步驟9', 'INF', `已將 ${severe.length} 條嚴重問題留言到 issue #${issueNumber}。`); } /** @@ -882,9 +827,8 @@ module.exports = { runDefenders, sortFindings, appendExclusions, - sortFindingsForIssue, selectLabels, - createIssueWithFindings, + postSevereToIssue, resolveOldComments, postSevereComments, }; diff --git a/src/lib/templates.js b/src/lib/templates.js index 6219b0f..ef364e7 100644 --- a/src/lib/templates.js +++ b/src/lib/templates.js @@ -302,7 +302,7 @@ function othersComment(findings) { * @param {string} [params.prBody] - PR 描述原文;nullish 或 trim 後為空時輸出佔位文字。 * @returns {string} 完整 issue 本文 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 issue 時, + * 使用情境:建問題模式下 `main()`(src/index.js)的 `ensureIssueCreated` 建立 issue 時, * 以「標題=PR 標題、本文=本函式輸出」呼叫 `gitea.createIssue`, * 讓 issue 讀者能從本文回溯到觸發審查的 PR,再從下方留言逐條查看問題明細。 */ @@ -334,9 +334,9 @@ ${body || '(PR 無描述)'} * @param {string} [finding.suggestedCode] - 建議寫法程式碼;有值才輸出「建議寫法」區塊。 * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 * @remarks - * 使用情境:建問題模式下 `createIssueWithFindings`(src/lib/review.js)建立 issue 後, - * 把保留的 findings 依「檔案路徑→嚴重等級→起始行」排序,逐條以本函式產生留言內容、 - * 經 `gitea.createCommentOnIssue` 發布到新 issue 上,作為問題明細的追蹤紀錄。 + * 使用情境:建問題模式下 `review.postSevereToIssue`(src/lib/review.js)把每條嚴重 finding + * 以本函式產生留言內容、經 `gitea.createCommentOnIssue` 發布到追蹤 issue 上, + * 作為問題明細的追蹤紀錄。 */ function issueFindingComment(finding) { const emoji = SEVERITY_EMOJI[finding.severity] || '🔵'; @@ -383,6 +383,48 @@ function nothingToReviewComment(ignoredCount) { 本次 PR 套用 \`.reviewignore\` 後**沒有可審查的變更**${ignoredCount > 0 ? `(${ignoredCount} 個檔案被排除)` : ''},視為審查通過。`; } +/** + * 產生建問題模式(input: create-issue)下,回貼到「PR」的追蹤問題連結留言。 + * + * 建問題模式把審查內容全部發到 issue、不留在 PR;本留言是 PR 上唯一的一則審查留言, + * 提供 issue 連結與問題數量統計,讓 PR 讀者一眼看到「本次審查結果在哪個 issue」。 + * + * @param {Object} params - 解構參數。 + * @param {number} params.issueNumber - 追蹤問題的 issue 編號;內插為 Markdown 連結文字。 + * @param {string} params.issueUrl - 追蹤問題的 issue 網址;作為 Markdown 連結目標。 + * @param {number} params.severeCount - 嚴重問題條數,顯示在統計。 + * @param {number} params.otherCount - 警告+建議問題條數,顯示在統計。 + * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 + * @remarks + * 使用情境:建問題模式下 `main()`(src/index.js)在 issue 建立並寫入全部審查內容後, + * 以本函式對 PR 留一則連結留言,達成「問題關聯回 PR」;issue 內文另以 + * {@link issueBody} 反向引用 `PR #N`,形成雙向交叉連結。 + */ +function issueLinkComment({ issueNumber, issueUrl, severeCount, otherCount }) { + return `${MARK} +## 🔍 AI Code Review|已建立追蹤問題 + +本次審查結果已彙整到 issue [#${issueNumber}](${issueUrl})(🔴 嚴重 ${severeCount} 條、🟠🔵 警告+建議 ${otherCount} 條),請至該問題追蹤與討論。`; +} + +/** + * 產生「已審查但無需保留問題」時,回貼到「PR」的審查通過留言。 + * + * 建問題模式下,若防守方裁決後沒有任何保留問題(不值得為此開 issue), + * 以本留言在 PR 上告知審查通過,取代原本會發到 issue 的問題內容。 + * + * @returns {string} 完整留言 Markdown 字串(含 MARK 隱藏標記)。 + * @remarks + * 使用情境:建問題模式下 `main()`(src/index.js)於防守方裁決後 + * `kept.length === 0` 時呼叫;此情況不建立 issue,僅在 PR 留下本則通過提示。 + */ +function noFindingsComment() { + return `${MARK} +## ✅ AI Code Review|審查通過 + +本次 AI Code Review 未發現需保留的問題,視為審查通過。`; +} + module.exports = { MARK, OUTDATED_PREFIX, @@ -397,4 +439,6 @@ module.exports = { issueBody, issueFindingComment, nothingToReviewComment, + issueLinkComment, + noFindingsComment, }; -- 2.53.0 From 3631bf359233109159bb5bb5ec7a160da00d2117 Mon Sep 17 00:00:00 2001 From: Jeffery Date: Mon, 20 Jul 2026 11:24:48 +0800 Subject: [PATCH 8/8] =?UTF-8?q?chore(ci):=20=E5=95=9F=E7=94=A8=E5=BB=BA?= =?UTF-8?q?=E5=95=8F=E9=A1=8C=E6=A8=A1=E5=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/ci.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.gitea/workflows/ci.yaml b/.gitea/workflows/ci.yaml index e973722..59705bd 100644 --- a/.gitea/workflows/ci.yaml +++ b/.gitea/workflows/ci.yaml @@ -42,6 +42,8 @@ jobs: with: # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 + create-issue: 'true' # Codex 工具環境測試 job。 test-codex: # Job 在 workflow UI 顯示的名稱。 @@ -66,6 +68,8 @@ jobs: with: # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 + create-issue: 'true' # Claude 工具環境測試 job。 test-claude: # Job 在 workflow UI 顯示的名稱。 @@ -90,3 +94,5 @@ jobs: with: # Gitea / GitHub token,用於 PR 留言與 findings 寫回。 token: ${{ secrets.GITHUB_TOKEN }} + # 啟用建問題模式:審查內容改發到追蹤 issue,並在 PR 回貼 issue 連結。 + create-issue: 'true' -- 2.53.0