# AI Code Review
更新時間:2026/08/07 13:51:53
## 專案列表
| 專案名稱 | 專案描述 |
| --- | --- |
| [AI Code Review](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src) | Gitea Docker 容器 action:對 PR 的 diff 派多個角色進行 AI 程式碼審查,產生 findings 並依對話收斂、排除規則與 AI 誤報裁決收斂結果;負責 Gitea PR API(diff/comment/review/resolve)串接、CLIProxyAPI 對話與 usage/額度統計、git clone/commit/push 持久化 findings,以及執行前的 token/LLM/git 遠端前置驗證。 |
| 專案名稱 | 參考專案列表 |
| --- | --- |
| [AI Code Review](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src) | 無 |
| 專案名稱 | npm 套件列表 |
| --- | --- |
| [AI Code Review](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src) | axios ^1.6.7
js-yaml ^4.1.0 |
## 功能列表
### AI Code Review
| 功能名稱 | 功能描述 |
| --- | --- |
| [parseLocation](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L77) | [解析 finding 的 location 字串,取出檔案路徑與起始行號,供行內 comment 定位使用。](#parselocation) |
| [formatFindingsStats](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L173) | [產生新舊問題依嚴重等級(嚴重/警告/建議/無法標示)分類統計的 Markdown 表格。](#formatfindingsstats) |
| [formatFindingsStatsLine](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L194) | [產生與統計表相同內容的單行文字摘要,供 log 輸出使用。](#formatfindingsstatsline) |
| [postFindingsReview](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L261) | [發布整批 findings 的 Gitea review(摘要+行內 comment),並提供多層降級機制。](#postfindingsreview) |
| [saveFindings](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L311) | [將 findings 陣列以 JSON 格式寫入 workspace(及可選的鏡像目錄)。](#savefindings) |
| [postOldFindingsComment](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L335) | [發布所有舊有未解決問題的彙總 comment。](#postoldfindingscomment) |
| [postNewNonCriticalComment](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L359) | [發布新問題中非 critical 等級者的彙總 comment。](#postnewnoncriticalcomment) |
| [postNewCriticalComments](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/comments.js#L388) | [針對每個新的 critical 問題逐筆發布行內 comment,無法定位或失敗時降級為一般 comment。](#postnewcriticalcomments) |
| [getInsecureHttpsAgent](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/config.js#L57) | [取得關閉 TLS 憑證驗證的 HTTPS Agent 單例,供連接自簽憑證的內部服務使用。](#getinsecurehttpsagent) |
| [getLLMConfig](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/config.js#L77) | [依環境變數解析目前可用的 CLIProxyAPI 設定(base URL、model、API key)。](#getllmconfig) |
| [analyzeWithRole](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L20) | [用指定角色分析 diff,呼叫 LLM 產生該角色視角下的 findings 陣列。](#analyzewithrole) |
| [normalizeText](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L127) | [將文字正規化(NFKC、轉小寫、壓縮空白)為比對用形式,並以快取加速重複呼叫。](#normalizetext) |
| [loadOldFindings](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L314) | [讀取來源分支的舊 findings 檔案,標記為非新問題並記錄診斷日誌。](#loadoldfindings) |
| [mergeFindings](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L337) | [依 role/location/suggestion 組成的 key 合併新舊 findings 並去重。](#mergefindings) |
| [sortByLevel](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L358) | [依 critical/warning/info 順序排序 findings。](#sortbylevel) |
| [resolveMissingLineNumbers](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L425) | [對只有檔名缺行號的 findings,反問原角色依 diff 補上行號。](#resolvemissinglinenumbers) |
| [deduplicateWithAI](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L478) | [呼叫 LLM 對 findings 做語意去重,合併同位置同問題本質的重複項。](#deduplicatewithai) |
| [loadExclusions](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L513) | [讀取並正規化 exclusions 檔案,相容多種舊格式並就地修正為標準陣列。](#loadexclusions) |
| [appendExclusions](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L570) | [將新的排除條目去重後追加寫入 exclusions.json。](#appendexclusions) |
| [applyExclusions](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L625) | [依 exclusions 規則過濾 findings,移除符合排除條件的問題。](#applyexclusions) |
| [filterFalsePositivesWithAI](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/findings.js#L671) | [由防守方角色逐條裁決 findings 是否為誤報並剔除。](#filterfalsepositiveswithai) |
| [getBotReviewOutcome](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L39) | [解析文字中的 `[ai-review-bot]` 標記,回傳 success/failure/unknown。](#getbotreviewoutcome) |
| [parseReviewIgnore](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L62) | [把 `.reviewignore` 文字解析成排除前綴陣列。](#parsereviewignore) |
| [getReviewIgnore](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L75) | [讀取並解析 PR 的 `.reviewignore`,沒有規則時退回內建預設清單。](#getreviewignore) |
| [getPRDiff](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L90) | [取得目前 PR 的 diff,並套用 `.reviewignore` 與內建過濾規則。](#getprdiff) |
| [getCommitMessageBySha](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L102) | [依 commit SHA 向 Gitea 查詢該 commit 的訊息。](#getcommitmessagebysha) |
| [getBranchHeadCommitMessage](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L123) | [讀取指定分支 head commit 的訊息。](#getbranchheadcommitmessage) |
| [shouldSkipBotCommit](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L148) | [判斷目前 PR head 是否為 bot 自動提交,決定是否跳過審查。](#shouldskipbotcommit) |
| [filterDiff](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L166) | [過濾 unified diff 中不需要審查的路徑區塊。](#filterdiff) |
| [postComment](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L186) | [在 PR 下發布一則一般 Markdown 留言。](#postcomment) |
| [postPullReviewComment](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L205) | [對 PR 指定檔案行號發送單筆行內 review comment。](#postpullreviewcomment) |
| [postPullReview](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L228) | [建立包含摘要與多筆行內 comment 的 PR review。](#postpullreview) |
| [listPullReviews](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L248) | [列出目前 PR 的所有 review。](#listpullreviews) |
| [getPullReviewComments](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L262) | [依 review ID 取得該 review 底下的所有行內 comment。](#getpullreviewcomments) |
| [listAllReviewComments](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L276) | [彙整目前 PR 所有 review 的行內 comments 成單一陣列。](#listallreviewcomments) |
| [resolvePullReviewComment](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L298) | [解決指定 review comment 所屬的對話。](#resolvepullreviewcomment) |
| [getFileContentAtRef](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/gitea.js#L315) | [讀取指定 ref 下檔案的文字內容(自動 base64 解碼)。](#getfilecontentatref) |
| [getRepoState](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/git.js#L120) | [讀取指定 git repo 目錄的 HEAD SHA、分支與 commit 時間等狀態快照。](#getrepostate) |
| [getHeadCommitMessage](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/git.js#L138) | [讀取 HEAD commit 的完整 commit message。](#getheadcommitmessage) |
| [isBotAutoCommit](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/git.js#L153) | [判斷 HEAD commit 是否為 AI Review bot 自動產生的 commit。](#isbotautocommit) |
| [verifyRemoteAccess](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/git.js#L171) | [用 `git ls-remote` 驗證 remote 認證與連線是否可用。](#verifyremoteaccess) |
| [cloneRepo](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/git.js#L197) | [以可重入方式將 PR head branch clone/fetch 到工作目錄。](#clonerepo) |
| [commitAndPush](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/git.js#L241) | [將 findings/exclusions 結轉到 repo 並 commit、push 回 PR head branch。](#commitandpush) |
| [stripCodeFence](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/json.js#L17) | [移除文字外層的 markdown code fence 並清理前後空白。](#stripcodefence) |
| [repairJSONArrayWithAI](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/json.js#L43) | [透過 LLM 將原始內容修復成可直接 JSON.parse 的 JSON 陣列字串。](#repairjsonarraywithai) |
| [validateJSONArrayFile](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/json.js#L93) | [驗證 JSON 檔案是否合法,格式錯誤時嘗試以 AI 修復一次。](#validatejsonarrayfile) |
| [ensureJSONArrayFileExists](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/json.js#L137) | [確保指定路徑存在 JSON 檔案,不存在時建立空陣列檔。](#ensurejsonarrayfileexists) |
| [mapWithConcurrency](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/llm.js#L26) | [以可控併發數並行處理陣列項目並保序回傳結果。](#mapwithconcurrency) |
| [extractMeaningfulError](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/llm.js#L102) | [從 CLI/HTTP 原始輸出中擷取最有用的錯誤訊息片段。](#extractmeaningfulerror) |
| [chat](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/llm.js#L209) | [呼叫 CLIProxyAPI 送出對話請求並回傳純文字回應。](#chat) |
| [chatJSON](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/llm.js#L248) | [呼叫 chat 取得回應後,將文字解析為 JSON。](#chatjson) |
| [extractBalancedJSON](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/llm.js#L285) | [從指定索引以括號平衡方式擷取完整的 JSON 子字串。](#extractbalancedjson) |
| [extractJSONText](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/llm.js#L328) | [從雜訊文字中盡力抽出可被 JSON.parse 解析的片段。](#extractjsontext) |
| [section](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L32) | [輸出最上層的區塊分隔標題,切分整體執行流程。](#section) |
| [step](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L46) | [輸出流程中某個步驟的標題。](#step) |
| [line](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L59) | [輸出一行縮排的中性明細資訊。](#line) |
| [input](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L71) | [輸出「階段輸入」描述,標示目前步驟吃進了什麼資料。](#input) |
| [output](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L83) | [輸出「階段輸出」描述,標示目前步驟產出了什麼結果。](#output) |
| [result](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L97) | [依布林結果輸出成功或失敗的檢查/把關結果列。](#result) |
| [ok](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L110) | [輸出一筆成功/完成訊息。](#ok) |
| [warn](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L123) | [輸出一筆警告訊息(寫入 stderr)。](#warn) |
| [error](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/log.js#L136) | [輸出一筆錯誤訊息(寫入 stderr)。](#error) |
| [main](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/main.js#L60) | [AI Code Review Pipeline 的總指揮,依序執行 Step1~Step11 並依結果決定 exit code。](#main) |
| [checkRequiredEnv](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/preflight.js#L57) | [檢查 code review 所需的必要環境變數是否齊全。](#checkrequiredenv) |
| [verifyGiteaToken](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/preflight.js#L75) | [驗證 Gitea token 有效且對指定 repo 有讀取權限。](#verifygiteatoken) |
| [verifyCommentToken](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/preflight.js#L92) | [驗證選用的 comment token(GITEA_COMMENT_TOKEN)是否可用。](#verifycommenttoken) |
| [fetchLLMModels](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/preflight.js#L133) | [呼叫 CLIProxyAPI 的 `/v1/models`,確認 proxy 可用與模型清單可讀。](#fetchllmmodels) |
| [verifyLLM](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/preflight.js#L182) | [驗證 LLM proxy 設定可用,且設定的模型在可用清單內。](#verifyllm) |
| [runPreflight](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/preflight.js#L214) | [執行所有前置驗證(環境變數、Gitea token、comment token、git 遠端、LLM proxy)。](#runpreflight) |
| [parseBotReviewComment](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L55) | [嘗試把一則 review comment 內文解析回 bot 產生的 finding 欄位。](#parsebotreviewcomment) |
| [groupConversations](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L84) | [把 PR 上的行內 review comment 依檔案路徑+行號收斂成對話。](#groupconversations) |
| [codeWindow](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L119) | [取目標行附近的程式碼片段,供 AI 對照判斷問題是否已解決。](#codewindow) |
| [judgeConversations](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L149) | [批次請 AI 將每個對話判為 resolved / false_positive / open。](#judgeconversations) |
| [isSafeRepoPath](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L202) | [安全守衛:判定路徑是否為 repo 內的相對路徑,拒絕路徑穿越。](#issaferepopath) |
| [reconcileConversations](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L223) | [對話收斂主流程:關閉未解決 comment,並依 AI 判斷把 findings 分流為已修復/誤報/仍成立。](#reconcileconversations) |
| [dropResolvedFindings](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L371) | [從 findings 中移除已判定為「已解決對話」對應的問題。](#dropresolvedfindings) |
| [addCarriedFindings](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/resolve.js#L384) | [把仍成立但目前 findings 清單中遺漏的問題加回。](#addcarriedfindings) |
| [parseRoleFile](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/roles.js#L25) | [解析角色 Markdown 檔內容,拆出 frontmatter 與本文。](#parserolefile) |
| [loadRoles](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/roles.js#L71) | [載入所有「攻擊方」角色定義(`side === 'attack'`)。](#loadroles) |
| [loadRole](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/roles.js#L85) | [依名稱(不分大小寫)取得單一角色定義。](#loadrole) |
| [buildAnalysisPrompt](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/roles.js#L104) | [由攻擊方角色定義組出分析 diff 用的 system prompt。](#buildanalysisprompt) |
| [buildLocateLinePrompt](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/roles.js#L149) | [組出「補行號」用的 system prompt。](#buildlocatelineprompt) |
| [buildVerdictPrompt](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/roles.js#L173) | [由防守方角色定義組出單條 finding 誤報裁決用的 system prompt。](#buildverdictprompt) |
| [getRoleIntro](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/roles.js#L206) | [由角色陣列產生「AI Code Review 團隊」介紹用的 Markdown 表格。](#getroleintro) |
| [extractUsage](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L29) | [把各平台回應中的 token usage 正規化成統一格式。](#extractusage) |
| [recordUsage](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L75) | [記錄一次 LLM 呼叫的 usage 並累加進模組層級統計。](#recordusage) |
| [getRunUsage](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L91) | [取得本次執行至今的 token 累計(複本)。](#getrunusage) |
| [resetRunUsage](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L99) | [重置本次執行的 token 累計(測試用)。](#resetrunusage) |
| [recordRateLimit](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L128) | [從回應 header 擷取速率配額剩餘量/上限並記錄。](#recordratelimit) |
| [getRateLimit](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L153) | [取得最近一次的速率配額快照(複本)。](#getratelimit) |
| [resetRateLimit](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L161) | [重置速率配額快照(測試用)。](#resetratelimit) |
| [fetchAccountQuota](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L245) | [取得指定平台的帳號額度資訊,任何失敗都降級回報無法取得。](#fetchaccountquota) |
| [resolveRemainingPercent](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L320) | [依優先序(帳號額度→速率配額)計算「剩餘可用百分比」。](#resolveremainingpercent) |
| [formatUsageStats](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L366) | [產生 PR Review 本文用的「AI 助理使用量」Markdown 區塊。](#formatusagestats) |
| [formatUsageStatsLine](https://gitea.jsc.idv.tw/actions/ai-code-review/src/branch/develop/src/usage.js#L396) | [產生單行 log 用的使用量摘要文字。](#formatusagestatsline) |
## 使用範例
### parseLocation
解析 finding 的 `location` 欄位,取出檔案路徑與(起始)行號,供行內 comment 標註使用。支援 `"file:19"`(單行)與 `"file:70-82"`(範圍,僅取起始行);若 `location` 非字串、包含逗號(代表對應多個檔案),或無法比對出行號格式,一律回傳 `null`,呼叫端應據此降級為一般(非行內)comment。
- 參數:`location`(`string`)- finding 的位置字串。
- 回傳:`{ file: string, line: number } | null`。
```javascript
import { parseLocation } from './src/comments.js';
parseLocation('src/config.js:57');
// => { file: 'src/config.js', line: 57 }
parseLocation('src/config.js:70-82');
// => { file: 'src/config.js', line: 70 }(範圍格式僅取起始行)
parseLocation('a.js:1,b.js:2');
// => null(多檔案不支援)
```
### formatFindingsStats
產生 findings 統計的 Markdown 表格:以 `is_new === false` 判定為舊問題、其餘為新問題,分別統計嚴重(critical)/警告(warning)/建議(info)/無法標示(level 不在三者之內)四欄的筆數,輸出含表頭、分隔列與兩筆資料列的表格字串。空陣列時仍會輸出表格(各欄為 0 筆)。
- 參數:`findings`(`Array