# 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 包成新版 wrapper 後寫入 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 檔案,不存在時建立空陣列或 findings wrapper。](#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`)- 審查問題陣列。 - 回傳:`string`(Markdown 表格)。 ```javascript import { formatFindingsStats } from './src/comments.js'; const findings = [ { is_new: true, level: 'critical' }, { is_new: true, level: 'warning' }, { is_new: false, level: 'info' }, ]; console.log(formatFindingsStats(findings)); // | 類型 | 🔴 嚴重 | 🟡 警告 | 🔵 建議 | ⚪ 無法標示 | // | --- | --- | --- | --- | --- | // | 新問題 | 1 筆 | 1 筆 | 0 筆 | 0 筆 | // | 舊問題 | 0 筆 | 0 筆 | 1 筆 | 0 筆 | ``` ### formatFindingsStatsLine 產生與 `formatFindingsStats` 相同統計邏輯(新/舊問題 × 嚴重/警告/建議/無法標示)的單行純文字摘要,供 log 輸出使用,格式如 `新: 嚴重1 / 警告0 / 建議2 / 無法標示0;舊: ...`。 - 參數:`findings`(`Array`)- 審查問題陣列。 - 回傳:`string`(單行摘要)。 ```javascript import { formatFindingsStatsLine } from './src/comments.js'; const line = formatFindingsStatsLine([{ is_new: true, level: 'critical' }]); console.log(line); // => 新: 嚴重1 / 警告0 / 建議0 / 無法標示0;舊: 嚴重0 / 警告0 / 建議0 / 無法標示0 ``` ### postFindingsReview 發布單一 Gitea review:一次性送出「統計摘要 + 逐筆行內 review comment」。降級順序:① 整批 `postReview`(含 comments)失敗 → ② 僅 body 的 `postReview`(comments 為空)失敗 → ③ `postIssue(body)`(此步未包 try/catch,失敗會直接向外拋出);走完任一步不再失敗後,會逐筆嘗試 `postInline` 補發行內 comment,單筆失敗只記錄警告並略過。 - 參數:`findings`(`Array`)- 本次審查的完整 findings;`deps`(`object`,可選)- 可覆寫 `postReview`/`postInline`/`postIssue`/`summaryFindings`/`commentFindings`/`usageSection`,供測試注入。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { postFindingsReview } from './src/comments.js'; const findings = [ { level: 'critical', role: 'Paladin', location: 'src/a.js:10', suggestion: '修正邊界檢查', is_new: true }, ]; await postFindingsReview(findings, { usageSection: '## 使用量\n...' }); // 依序嘗試發布整批 review → summary review → 一般 comment,並逐筆補發行內 comment ``` ### saveFindings 將 findings 包成新版 wrapper 物件後,以 `JSON.stringify(wrapper, null, 2)` 序列化並補結尾換行,寫入 `workspace/.gitea/ai-review/findings.json`;wrapper 內含 `generatedAt`/`commitSha`/`prNumber`/`tool`/`findings`/`excluded`。若提供且不同於 `workspace` 的 `mirrorDir`,會同時寫入該鏡像目錄的相同路徑。寫入前會建立必要的父目錄;本函式為同步阻塞呼叫且未做例外防護,`fs` 錯誤會直接向外拋出。 - 參數:`workspace`(`string`)、`findings`(`Array`)、`mirrorDir`(`?string`,預設 `null`)。 - 回傳:`void`。 ```javascript import { saveFindings } from './src/comments.js'; saveFindings('/workspace', [{ level: 'warning', location: 'a.js:1', suggestion: '...' }], '/workspace/repo'); // 同時寫入 /workspace/.gitea/ai-review/findings.json 與 /workspace/repo/.gitea/ai-review/findings.json ``` ### postOldFindingsComment 以 `!f.is_new`(`is_new` 為 `false`/`undefined`/其他 falsy 值皆視為舊問題)篩選出舊問題,若有則發布一則彙總 comment(表格依 `findings` 原始順序,未依等級排序);若無舊問題則只記錄一行 log 並直接 return,不會呼叫 `postComment`。 - 參數:`findings`(`Array`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { postOldFindingsComment } from './src/comments.js'; await postOldFindingsComment([{ is_new: false, level: 'warning', role: 'Scout', location: 'a.js:5', suggestion: '...' }]); // 發布「## 📋 舊有未解決問題(1 筆)」comment ``` ### postNewNonCriticalComment 以 `f.is_new && f.level !== 'critical'`(`is_new` 須為 truthy 才算新問題)篩選出新的非嚴重問題,若有則發布一則彙總 comment;無則只記錄 log 並直接 return。 - 參數:`findings`(`Array`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { postNewNonCriticalComment } from './src/comments.js'; await postNewNonCriticalComment([{ is_new: true, level: 'warning', role: 'Scout', location: 'a.js:5', suggestion: '...' }]); // 發布「## 🔍 新發現問題(1 筆)」comment ``` ### postNewCriticalComments 以 `f.is_new && f.level === 'critical'` 篩選出新的嚴重問題,逐筆處理:`location` 可解析出行號且 `postInline` 成功時只發行內 comment;否則(無法解析,或 `postInline` 失敗)改用 `postIssue` 發一般 comment(此呼叫未包 try/catch,失敗會中斷迴圈)。 - 參數:`findings`(`Array`)、`deps`(`object`,可選,覆寫 `postInline`/`postIssue`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { postNewCriticalComments } from './src/comments.js'; await postNewCriticalComments([ { is_new: true, level: 'critical', role: 'Paladin', location: 'src/a.js:12', suggestion: '修正 SQL Injection 風險' }, ]); // 對 src/a.js:12 發一則行內 review comment;若無法定位則改發一般 comment ``` ### getInsecureHttpsAgent 取得一個關閉 TLS 憑證驗證(`rejectUnauthorized: false`)的 HTTPS Agent 單例,供連接使用自簽或無效憑證的內部服務(如自架 Gitea、CLIProxyAPI)時使用;首次呼叫建立後於模組層級快取重複使用。僅限受信任的內部環境,需要完整 TLS 安全性時請改用預設 `https.Agent`。 - 參數:無。 - 回傳:`import('https').Agent`。 ```javascript import { getInsecureHttpsAgent } from './src/config.js'; import axios from 'axios'; const httpsAgent = getInsecureHttpsAgent(); await axios.get('https://internal-gitea.example/api/v1/user', { httpsAgent }); ``` ### getLLMConfig 依環境變數解析並回傳 CLIProxyAPI 設定:`INPUT_CLI_PROXY_API`/`CLI_PROXY_API` 作 base URL(trim 並去尾斜線),`INPUT_MODEL`/`CLI_PROXY_API_MODEL`/`MODEL`/`OPENCODE_MODEL` 依序 fallback 作可選模型名稱;若未指定模型,會交由 CLIProxyAPI 自動選擇,`INPUT_CLI_PROXY_API_KEY`/`CLI_PROXY_API_KEY` 作金鑰。base URL 無法解析時 `provider`/`baseURL` 回 `null`、`apiKeys` 回空陣列,但 `model`(若有)仍會回傳。 - 參數:無。 - 回傳:`{ provider, apiKeys, baseURL, model, command }`。 ```javascript import { getLLMConfig } from './src/config.js'; // 環境變數:CLI_PROXY_API=https://proxy.example, CLI_PROXY_API_KEY=sk-xxx const cfg = getLLMConfig(); // => { provider: 'cliproxyapi', apiKeys: ['sk-xxx'], baseURL: 'https://proxy.example', model: null, command: null } ``` ### analyzeWithRole 用單一角色分析 diff:呼叫 `chatJSON` 取得該角色視角下的 code review 問題,過濾出同時具備 `level`/`location`/`suggestion` 的有效 findings,並補上 `role`(一律覆寫為角色定義的 `name`,避免 LLM 自填不一致名稱)與 `is_new: true`。`chatJSON`(LLM)失敗時例外直接向外拋出,不做降級。 - 參數:`role`(`{name: string}`)、`diff`(`string`)。 - 回傳:`Promise>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { analyzeWithRole } from './src/findings.js'; import { loadRole } from './src/roles.js'; const role = loadRole('Scout'); const findings = await analyzeWithRole(role, diffText); // => [{ level: 'warning', role: 'Scout', location: 'a.js:12', problem: '...', suggestion: '...', is_new: true }, ...] ``` ### normalizeText 將任意值正規化為比對用形式:先安全轉字串(非字串轉空字串),再做 NFKC 正規化、轉小寫,把所有標點/符號/空白字元壓縮成單一空白、去頭尾空白。因常對相同字串重複呼叫(findings × exclusions 笛卡爾積比對),以模組層級 `Map` 對字串輸入做快取,程序生命週期內不會清除。 - 參數:`value`(`*`)。 - 回傳:`string`。 ```javascript import { normalizeText } from './src/findings.js'; normalizeText(' 這裡有 SQL Injection!! '); // => '這裡有 sql injection' ``` ### loadOldFindings 讀取來源分支 clone 出的工作目錄下 `FINDINGS_PATH`(`.gitea/ai-review/findings.json`),相容舊版頂層陣列與新版 wrapper 物件;每筆標記 `is_new: false`,並記錄檔案大小/修改時間等診斷日誌。檔案不存在或讀取失敗時視為空陣列,不拋例外。 - 參數:`workspace`(`string`)。 - 回傳:`Array`。 ```javascript import { loadOldFindings } from './src/findings.js'; const old = loadOldFindings('/workspace/repo'); // => [{ level: 'warning', location: 'a.js:5', suggestion: '...', is_new: false }, ...] ``` ### mergeFindings 以 `role + location + suggestion 前 50 字` 組成的字串為 key,將 `newFindings` 中與 `oldFindings`(或先出現的 `newFindings` 自身)key 相同者去除;`oldFindings` 本身不互相去重,回傳 `[...oldFindings, ...去重後的 newFindings]`,不修改傳入的兩個陣列。 - 參數:`oldFindings`(`Array`)、`newFindings`(`Array`)。 - 回傳:`Array`。 ```javascript import { mergeFindings } from './src/findings.js'; const merged = mergeFindings( [{ role: 'Scout', location: 'a.js:5', suggestion: '既有問題' }], [{ role: 'Scout', location: 'a.js:5', suggestion: '既有問題' }, { role: 'Paladin', location: 'b.js:1', suggestion: '新問題' }], ); // => [{...既有問題}, {...新問題}](重複的新問題被濾除) ``` ### sortByLevel 依 `critical > warning > info` 順序排序 findings,回傳新陣列,不修改傳入陣列。等級不在三者之內的項目因 `indexOf` 回傳 `-1`,會被排到 critical 之前(最前面)。 - 參數:`findings`(`Array`)。 - 回傳:`Array`。 ```javascript import { sortByLevel } from './src/findings.js'; sortByLevel([{ level: 'info' }, { level: 'critical' }, { level: 'warning' }]); // => [{ level: 'critical' }, { level: 'warning' }, { level: 'info' }] ``` ### resolveMissingLineNumbers 對「只有檔名、缺行號」的 findings,反問原角色依該檔 diff 找出行號(每條最多 `maxAttempts` 次,預設 3 次),成功則就地修改(mutate)該 finding 的 `location` 為 `檔案:行號`,否則保留原檔名。各條 finding 以獨立 LLM 呼叫並行定位,併發上限見 `concurrency`(預設 `LLM_CONCURRENCY`)。 - 參數:`findings`(`Array`)、`diff`(`string`)、`deps`(`object`,可選:`chatFn`/`getRole`/`maxAttempts`/`concurrency`)。 - 回傳:`Promise>`(與輸入相同參照)。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { resolveMissingLineNumbers } from './src/findings.js'; const findings = [{ role: 'Scout', location: 'src/a.js', problem: '...', suggestion: '...' }]; await resolveMissingLineNumbers(findings, diffText); // findings[0].location 可能被就地改為 'src/a.js:42' ``` ### deduplicateWithAI 呼叫 LLM(Paladin 角色)進行語意去重:合併「同位置+同問題本質」的重複 findings,重複者保留等級較高者。為避免幻覺,結果逐筆以 `(location + suggestion 前 50 字)` 對應回原始 findings;對應不到、空結果、非陣列或數量超過輸入者,皆整批降級為保留所有原始 findings。 - 參數:`findings`(`Array`)。 - 回傳:`Promise>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { deduplicateWithAI } from './src/findings.js'; const deduped = await deduplicateWithAI(mergedFindings); // => 去重後的原始 finding 物件陣列;AI 失敗時原樣回傳 mergedFindings ``` ### loadExclusions 讀取來源分支工作目錄下 `EXCLUSIONS_PATH`(`.gitea/ai-review/exclusions.json`),正規化並去重後回傳。若偵測到舊格式(`{ exclusions: [] }` 或 `{ excluded_findings: [] }`),會就地覆寫為標準頂層陣列(並同步寫入 `mirrorWorkspace`,若提供且路徑不同)。檔案不存在或讀取失敗時皆視為空陣列,不拋例外。 - 參數:`workspace`(`string`)、`repoState`(`?object`,僅供診斷日誌)、`mirrorWorkspace`(`?string`)。 - 回傳:`Array`。 ```javascript import { loadExclusions } from './src/findings.js'; const exclusions = loadExclusions('/workspace/repo', { branch: 'feature/x', shortSha: 'abc1234' }, '/workspace'); // => [{ location: 'a.js', role: 'Scout', text: '...', textKey: '...', fingerprint: '...' }, ...] ``` ### appendExclusions 把新的排除條目(raw 形式)append 到 `exclusions.json`,以「檔案路徑(location 冒號前段)+ `normalizeText` 後的原文」為簽名去重後,以頂層陣列格式寫回 `workspace`(及提供且路徑不同的 `mirrorWorkspace`)。`newEntries` 為空時直接回傳 `null`;若全部重複則回傳既有陣列且不寫檔。 - 參數:`workspace`(`string`)、`newEntries`(`Array`)、`mirrorWorkspace`(`?string`)。 - 回傳:`Array | null`。 ```javascript import { appendExclusions } from './src/findings.js'; appendExclusions('/workspace/repo', [ { location: 'a.js:10', role: 'Scout', original_finding: '此處誤報', reason: 'AI 對話收斂判定為誤報' }, ], '/workspace'); // => 合併後的完整排除條目陣列,並寫入 /workspace/repo 與 /workspace 的 exclusions.json ``` ### applyExclusions 套用排除規則過濾 findings:對每個 exclusion,`locationMatches`(只比對檔案路徑,忽略行號)且 `roleMatches`(未指定則萬用)且(`exclusion` 同時未指定 `filePath` 與 `role` 時才比對正規化後文字是否互相包含,否則直接視為符合)即視為命中並剔除。`exclusions` 為空時原樣回傳新陣列(不修改原輸入)。 - 參數:`findings`(`Array`)、`exclusions`(`Array`)。 - 回傳:`Array`。 ```javascript import { applyExclusions } from './src/findings.js'; const filtered = applyExclusions( [{ location: 'a.js:5', role: 'Scout', suggestion: '此處為既知誤報' }], [{ filePath: 'a.js', role: 'Scout' }], ); // => [](同檔案同角色即視為命中,文字比對被略過) ``` ### filterFalsePositivesWithAI 由「防守方」角色(固定為 Paladin)逐條裁決 findings 是否為誤報,剔除誤報、保留成立者;多筆時各派一個裁決任務平行處理(併發上限 `LLM_CONCURRENCY`)。任一筆裁決失敗時保守保留該問題,不中斷整體流程。 - 參數:`findings`(`Array`)、`exclusions`(`Array`,預設 `[]`,用於引導相似誤報更寬鬆判定)、`chatFn`(`Function`,預設 `chatJSON`)。 - 回傳:`Promise>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { filterFalsePositivesWithAI } from './src/findings.js'; const kept = await filterFalsePositivesWithAI(ruleFiltered, exclusions); // => 裁決為「非誤報」而保留下來的 finding 陣列 ``` ### getBotReviewOutcome 解析文字中的 `[ai-review-bot][success|failure]` 標記,判斷上一次自動審查結果;用於 commit 訊息或留言內容,無標記或無後綴時視為未知。 - 參數:`message`(`string`)。 - 回傳:`'success' | 'failure' | 'unknown'`。 ```javascript import { getBotReviewOutcome } from './src/gitea.js'; getBotReviewOutcome('chore: update ai-review findings [ai-review-bot][failure]'); // => 'failure' getBotReviewOutcome('一般 commit 訊息'); // => 'unknown' ``` ### parseReviewIgnore 解析 `.reviewignore` 文字為排除前綴陣列(gitignore 風格):每行一個路徑前綴,trim 後略過空行與 `#` 開頭的註解行。 - 參數:`text`(`string`)。 - 回傳:`string[]`。 ```javascript import { parseReviewIgnore } from './src/gitea.js'; parseReviewIgnore('# 註解\n.gitea/\n\nREADME.md\n'); // => ['.gitea/', 'README.md'] ``` ### getReviewIgnore 從被審 PR 的 head ref 取得 `.reviewignore` 並解析為排除清單;檔案不存在或為空時退回 `DEFAULT_REVIEW_IGNORE`。成功套用自訂規則時會輸出一行 log。 - 參數:無。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { getReviewIgnore } from './src/gitea.js'; const patterns = await getReviewIgnore(); // => ['.gitea/', '.github/', 'README.md', ...](無自訂 .reviewignore 時為預設清單) ``` ### getPRDiff 取得目前 PR 的完整 unified diff(`GET /repos/{repo}/pulls/{index}.diff`),並依 `.reviewignore`(讀不到時用內建預設)排除不需審查的路徑。 - 參數:無。 - 回傳:`Promise`。 - 例外:Gitea API 請求失敗時拋出。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { getPRDiff } from './src/gitea.js'; const diff = await getPRDiff(); // => 過濾後的 unified diff 文字,供各角色分析使用 ``` ### getCommitMessageBySha 依 commit SHA 向 Gitea 查詢該 commit 的訊息(`GET /repos/{repo}/git/commits/{sha}`);失敗或 `sha` 為空時不拋例外,記錄警告並回傳空字串。 - 參數:`sha`(`string`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 環境變數。 import { getCommitMessageBySha } from './src/gitea.js'; const message = await getCommitMessageBySha('abcdef1234567890'); // => commit 訊息文字,查無或失敗時為 '' ``` ### getBranchHeadCommitMessage 取得指定分支 head commit 的訊息:先查 `GET /repos/{repo}/branches/{branch}` 取 SHA,再查該 commit;失敗或 `branch` 為空時不拋例外,回傳空字串。 - 參數:`branch`(`string`,預設 `PR_HEAD_BRANCH`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 環境變數。 import { getBranchHeadCommitMessage } from './src/gitea.js'; const message = await getBranchHeadCommitMessage('feature/x'); ``` ### shouldSkipBotCommit 判斷目前 PR head(commit 或分支 head)的訊息是否帶 `[ai-review-bot]` 標記;若是,代表本次變更為 bot 自動提交,呼叫端應跳過審查以避免自我審查迴圈。內部查詢失敗會降級為空字串(視為未命中),正常情況下不會拋例外。 - 參數:`options.sha`(`string`,預設 `PR_HEAD_SHA`)、`options.branch`(`string`,預設 `PR_HEAD_BRANCH`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { shouldSkipBotCommit } from './src/gitea.js'; if (await shouldSkipBotCommit()) { // 本次為 bot 自動提交,跳過本輪審查 } ``` ### filterDiff 過濾 unified diff,移除檔案路徑前綴命中 `excludePrefixes` 的區塊;以每個 `diff --git ` 行為界切割,並一律強制排除任何深度的 `node_modules/`。`diff` 必須為字串,否則拋出 TypeError。 - 參數:`diff`(`string`)、`excludePrefixes`(`string[]`,預設 `[]`)。 - 回傳:`string`。 ```javascript import { filterDiff } from './src/gitea.js'; const filtered = filterDiff(rawDiff, ['.gitea/', 'README.md']); // => 移除 .gitea/ 與 README.md 相關區塊、且一律排除 node_modules/ 後的 diff 文字 ``` ### postComment 在目前 PR 下發布一則一般留言(Gitea 以 issue comment 形式處理 PR 留言),透過 `POST /repos/{repo}/issues/{index}/comments`,優先使用 `GITEA_COMMENT_TOKEN` 授權。 - 參數:`body`(`string`)。 - 回傳:`Promise`。 - 例外:請求失敗時拋出。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { postComment } from './src/gitea.js'; await postComment('## 🔍 新發現問題(1 筆)\n\n...'); ``` ### postPullReviewComment 在 PR 指定檔案的指定新版行號發布一筆行內 review comment(建立只含單一 comment 的 `COMMENT` review);該行不在 diff 範圍時 Gitea 會回錯誤而拋例外,呼叫端可降級為一般留言。 - 參數:`{ path, line, body }`。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { postPullReviewComment } from './src/gitea.js'; await postPullReviewComment({ path: 'src/a.js', line: 42, body: '**建議**:補上邊界檢查' }); ``` ### postPullReview 建立一個 PR review:本文放摘要,`comments` 批次放多筆行內 review comments;透過 `POST /repos/{repo}/pulls/{index}/reviews`(`event=COMMENT`),優先使用 `GITEA_COMMENT_TOKEN`。 - 參數:`{ body, comments }`(`comments` 預設 `[]`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { postPullReview } from './src/gitea.js'; await postPullReview({ body: '## AI Code Review 統計\n...', comments: [{ path: 'src/a.js', body: '**建議**:...', new_position: 10 }], }); ``` ### listPullReviews 取得目前 PR 上所有 review(`GET /repos/{repo}/pulls/{index}/reviews`);回應非陣列時回傳空陣列以保證型別一致。 - 參數:無。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { listPullReviews } from './src/gitea.js'; const reviews = await listPullReviews(); ``` ### getPullReviewComments 取得指定 review 底下的所有行內 comment(`GET /repos/{repo}/pulls/{index}/reviews/{id}/comments`)。 - 參數:`reviewId`(`number | string`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { getPullReviewComments } from './src/gitea.js'; const comments = await getPullReviewComments(123); ``` ### listAllReviewComments 取得目前 PR 上所有 review 的行內 comment 並展平為單一陣列;單一 review 取 comment 失敗時記錄警告並略過,不中斷整體流程,最後輸出統計日誌。 - 參數:無。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { listAllReviewComments } from './src/gitea.js'; const allComments = await listAllReviewComments(); ``` ### resolvePullReviewComment 解決(resolve)指定 review comment 所屬的對話,對應 Gitea API `POST /repos/{repo}/pulls/comments/{id}/resolve`,使用 `GITEA_COMMENT_TOKEN` 授權。 - 參數:`commentId`(`number | string`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 環境變數。 import { resolvePullReviewComment } from './src/gitea.js'; await resolvePullReviewComment(456); ``` ### getFileContentAtRef 取得指定 ref(預設 PR head)下某檔案的文字內容,透過 Gitea contents API,base64 內容會自動解碼為 UTF-8 字串;檔案不存在、非文字或請求失敗時不拋例外,記錄警告並回傳空字串。 - 參數:`filePath`(`string`)、`ref`(`string`,預設 `PR_HEAD_SHA||PR_HEAD_BRANCH`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { getFileContentAtRef } from './src/gitea.js'; const content = await getFileContentAtRef('.reviewignore'); ``` ### getRepoState 讀取指定 repo 目錄的目前狀態(HEAD SHA、短 SHA、目前分支、commit 時間 `%cI`);所有查詢皆採容錯讀取,任一失敗對應欄位即為空字串,不會丟出例外。 - 參數:`repoDir`(`string`)、`_spawnSync`(測試用依賴注入,預設 `spawnSync`)。 - 回傳:`{ repoDir, branch, headSha, shortSha, commitTime }`。 ```javascript import { getRepoState } from './src/git.js'; const state = getRepoState('/workspace/repo'); // => { repoDir: '/workspace/repo', branch: 'feature/x', headSha: 'abc123...', shortSha: 'abc123', commitTime: '2026-08-07T13:41:28+08:00' } ``` ### getHeadCommitMessage 取得 HEAD commit 的完整 commit message(`%B`,含 subject 與 body),容錯讀取,失敗時回傳空字串。 - 參數:`repoDir`(`string`)、`_spawnSync`(測試用依賴注入)。 - 回傳:`string`。 ```javascript import { getHeadCommitMessage } from './src/git.js'; const message = getHeadCommitMessage('/workspace/repo'); ``` ### isBotAutoCommit 判斷 HEAD commit 是否為 AI Review 機器人自己產生的自動 commit(commit message 含 `BOT_COMMIT_MARKER`:`[ai-review-bot]`),常用於避免機器人 commit 反覆觸發新一輪審查。 - 參數:`repoDir`(`string`)、`_spawnSync`(測試用依賴注入)。 - 回傳:`boolean`。 ```javascript import { isBotAutoCommit } from './src/git.js'; if (isBotAutoCommit('/workspace/repo')) { // 跳過本輪審查 } ``` ### verifyRemoteAccess 用與 push 相同的 askpass + remote URL 機制跑一次唯讀的 `git ls-remote`,驗證 git 對 remote 的認證與連線是否可用(不寫入任何東西);查詢分支為 `PR_HEAD_BRANCH || 'HEAD'`。所有例外皆在內部捕捉,不會向外拋出。 - 參數:`workspace`(`string`)、`_spawnSync`(測試用依賴注入)。 - 回傳:`{ ok: boolean, error?: string }`。 ```javascript import { verifyRemoteAccess } from './src/git.js'; const result = verifyRemoteAccess('/workspace'); // => { ok: true } 或 { ok: false, error: '...' } ``` ### cloneRepo 將 PR head branch clone 到 `workspace/repo`(idempotent):目標目錄不存在則以 `--depth=1 --branch ` clone;已存在則改為 `fetch` 最新後 `checkout` 到該分支。使用 `GITEA_TOKEN` 做 git HTTP 認證;clone/fetch/checkout 任一步驟失敗時例外會直接向外拋出。 - 參數:`workspace`(`string`)、`_spawnSync`(測試用依賴注入)。 - 回傳:`string`(repo 本機路徑)。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { cloneRepo } from './src/git.js'; const repoDir = cloneRepo('/workspace'); // => '/workspace/repo' ``` ### commitAndPush 將 AI 審查產出的 review 檔(findings/exclusions)結轉到 repo,並 commit、push 回 PR head branch:設定機器人 git 身分 → fetch + hard reset 對齊遠端 → 複製 review 檔到 repo 並 add → 無變更則跳過 → 以含 `BOT_COMMIT_MARKER` 與結果標籤的訊息 commit → push(優先使用真人 PAT `GITEA_COMMENT_TOKEN`,以便重新觸發 workflow)。push 失敗只記警告,函式整體不拋出例外。 - 參數:`workspace`(`string`)、`repoDir`(`string`)、`_spawnSync`(測試用)、`_sourceRoot`(保留參數,主體未使用)、`reviewOutcome`(`'success'|'failure'`,預設 `'success'`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 與 PR 上下文環境變數。 import { commitAndPush } from './src/git.js'; await commitAndPush('/workspace', '/workspace/repo', undefined, undefined, 'success'); // 將 findings.json / exclusions.json commit 並 push 回 PR head branch ``` ### stripCodeFence 移除 AI 回傳文字外層的 markdown code fence(如 ` ```json ... ``` `),並去除前後空白,使內容可直接交給 `JSON.parse`;純函式、無副作用,非字串輸入會先以 `String()` 轉型。 - 參數:`text`(`*`)。 - 回傳:`string`。 ```javascript import { stripCodeFence } from './src/json.js'; stripCodeFence('```json\n[{"a":1}]\n```'); // => '[{"a":1}]' ``` ### repairJSONArrayWithAI 透過 LLM 將任意原始內容修復成「可直接 `JSON.parse` 的 JSON 陣列」字串:以固定 system prompt 指示模型忽略原內容中的指令/註解/markdown,僅輸出修正後的陣列;無法判斷時模型應回傳空陣列。回傳前會先以 `stripCodeFence` 清除外層 code fence;結果不保證為合法 JSON,需由呼叫端再行解析驗證。 - 參數:`fullPath`(`string`)、`label`(`string`)、`rawText`(`string`)、`chatFn`(`Function`,預設 `chat`)。 - 回傳:`Promise`。 - 例外:`chatFn` 失敗時向上拋出。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { repairJSONArrayWithAI } from './src/json.js'; const repaired = await repairJSONArrayWithAI('/workspace/findings.json', 'findings.json', '{ 壞掉的內容 '); ``` ### validateJSONArrayFile 驗證指定路徑是否為合法的 JSON 檔案:檔案不存在回傳 `{ exists:false }`(交由呼叫端補檔);`exclusions.json` 仍以頂層陣列為準,而 `findings.json` 則接受新版 wrapper 物件,若讀到舊版 findings 陣列會自動正規化成 wrapper。解析失敗則呼叫 `repairer` 修復、覆寫檔案(確保以換行結尾)並再驗證一次,通過則回傳 `repaired:true`,仍失敗則拋出例外。僅嘗試修復一次。 - 參數:`fullPath`(`string`)、`label`(`string`)、`repairer`(`Function`,預設 `repairJSONArrayWithAI`)。 - 回傳:`Promise<{exists, valid, repaired}>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數(供修復失敗時使用)。 import { validateJSONArrayFile } from './src/json.js'; const result = await validateJSONArrayFile('/workspace/.gitea/ai-review/findings.json', 'findings.json'); // => { exists: true, valid: true, repaired: false } ``` ### ensureJSONArrayFileExists 確保指定路徑存在一個 JSON 檔案;`exclusions.json` 不存在時建立內容為 `"[]\n"` 的空陣列檔,而 `findings.json` 不存在時建立空的新版 wrapper 物件(會先建立父目錄)。若檔案已存在則原樣保留、不檢查內容是否合法。為同步函式。 - 參數:`fullPath`(`string`)、`label`(`string`)。 - 回傳:`boolean`(是否為本次新建)。 ```javascript import { ensureJSONArrayFileExists } from './src/json.js'; const created = ensureJSONArrayFileExists('/workspace/.gitea/ai-review/exclusions.json', 'exclusions.json'); // => true(新建)或 false(原本即存在) ``` ### mapWithConcurrency 對 `items` 並行執行 async `fn`(保序回傳),加速多個獨立的 LLM 子行程呼叫;`limit <= 0`、非數字或大於項目數時「不限制」(全部並行)。`fn` 需自行處理例外,否則 reject 會使本函式立即向外拋出(其他已啟動的併發工作不會被取消,只是結果被捨棄)。 - 參數:`items`(`T[]`)、`limit`(`number`)、`fn`(`(item, index) => Promise`)。 - 回傳:`Promise`。 ```javascript import { mapWithConcurrency } from './src/llm.js'; const results = await mapWithConcurrency([1, 2, 3], 2, async (n) => n * 2); // => [2, 4, 6](同時最多 2 個併發) ``` ### extractMeaningfulError 從 proxy API 錯誤輸出中抽出「真正有意義的錯誤」:先抽出看起來像錯誤的行(含 ERROR/unauthorized/rate limit 等關鍵字),抽不到再退取尾段;回傳長度受 `limit` 限制。 - 參數:`raw`(`string`)、`limit`(`number`,預設 1000)。 - 回傳:`string`。 ```javascript import { extractMeaningfulError } from './src/llm.js'; extractMeaningfulError('some noise\nERROR: rate limit exceeded\nmore noise'); // => 'ERROR: rate limit exceeded' ``` ### chat 對目前環境可用的 CLIProxyAPI 送出一次對話請求並回傳純文字回應:未偵測到 proxy 設定時拋錯;不含任何重試邏輯,失敗一次即向外拋出(重新包裝為精簡訊息的 `Error`)。成功時記錄一次 usage 呼叫。 - 參數:`systemPrompt`(`string`)、`userContent`(`string`)。 - 回傳:`Promise`。 - 例外:設定缺失、API 呼叫失敗或回應無文字內容時拋出。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數(CLI_PROXY_API;MODEL 可省略)。 import { chat } from './src/llm.js'; const reply = await chat('你是程式碼審查員', '請審查以下 diff:...'); ``` ### chatJSON 對 CLIProxyAPI 送出對話並將回應解析為 JSON:先呼叫 `chat` 取得文字回應,再經 `extractJSONText` 抽出 JSON 片段後 `JSON.parse`。僅 JSON 解析失敗時容錯(回傳空陣列 `[]`);若 `chat()` 本身失敗,例外會直接向外傳播。 - 參數:`systemPrompt`(`string`)、`userContent`(`string`)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { chatJSON } from './src/llm.js'; const findings = await chatJSON('請以 JSON 陣列回覆審查結果', diffText); // => [] 或解析後的 JSON 值 ``` ### extractBalancedJSON 從指定索引起,以括號平衡方式擷取一段完整配對的 JSON 子字串;依起始字元判定為物件或陣列,逐字元計數巢狀深度並正確略過字串字面值與跳脫字元,深度歸零時回傳完整片段,找不到配對時回傳 `null`。 - 參數:`text`(`*`)、`startIndex`(`number`,應指向 `{` 或 `[`)。 - 回傳:`string | null`。 ```javascript import { extractBalancedJSON } from './src/llm.js'; extractBalancedJSON('前言 [1, 2, {"a": "]"}] 後言', 3); // => '[1, 2, {"a": "]"}]' ``` ### extractJSONText 從可能夾雜雜訊或被 code fence 包裹的文字中,盡力抽出可被 `JSON.parse` 解析的片段:先去除外層 fence;若整段即為合法 JSON 直接回傳;否則由左至右尋找每個 `{`/`[` 起點,以括號平衡擷取候選片段並試解析,回傳第一個成功者;全數失敗則回傳去 fence 後的原文。 - 參數:`text`(`*`)。 - 回傳:`string`。 ```javascript import { extractJSONText } from './src/llm.js'; extractJSONText('這是回應:```json\n[{"level":"info"}]\n```'); // => '[{"level":"info"}]' ``` ### section 輸出最上層的「區塊/章節」分隔標題(前綴空行 + `[時間][INF]: === 標題 ===`),用於切分整個執行流程中彼此獨立的大段落,讓 CI log 在視覺上分群。 - 參數:`title`(`string`)。 - 回傳:`void`。 ```javascript import { section } from './src/log.js'; section('AI Code Review Pipeline'); // => 印出:\n[2026/08/07 13:51:53][INF]: === AI Code Review Pipeline === ``` ### step 輸出某個「步驟」的標題(前綴空行 + `[步驟代號] 標題`),適合在一個 section 之下標示流程中的各個有序步驟。 - 參數:`stepName`(`string`)、`title`(`string`)。 - 回傳:`void`。 ```javascript import { step } from './src/log.js'; step('Step2', '前置驗證(驗證相關設定)'); ``` ### line 輸出一筆縮排的一般明細列(` - 訊息`),用於列出不帶語意成敗的中性資訊。 - 參數:`message`(`string`)。 - 回傳:`void`。 ```javascript import { line } from './src/log.js'; line('已套用 .reviewignore:3 條排除規則'); ``` ### input 輸出「階段輸入」描述(` ← 輸入:訊息`),標示目前步驟吃進了什麼資料。 - 參數:`message`(`string`)。 - 回傳:`void`。 ```javascript import { input } from './src/log.js'; input('repo=owner/name PR=#12 feature/x → develop'); ``` ### output 輸出「階段輸出」描述(` → 輸出:訊息`),標示目前步驟產出了什麼結果。 - 參數:`message`(`string`)。 - 回傳:`void`。 ```javascript import { output } from './src/log.js'; output('新 findings 3 筆(嚴重1 / 警告1 / 建議1 / 無法標示0)'); ``` ### result 輸出一筆檢查/把關結果列,依 `passed` 以 `✅ 成功` 或 `❌ 失敗` 為前綴;成功寫 `INF` 等級、失敗寫 `ERR` 等級,但一律輸出至 stdout(不因失敗改寫 stderr)。 - 參數:`passed`(`boolean`)、`message`(`string`)。 - 回傳:`void`。 ```javascript import { result } from './src/log.js'; result(true, '前置驗證通過'); result(false, '發現 1 個嚴重問題,workflow 失敗(exit 1)'); ``` ### ok 輸出一筆成功/完成訊息(` ✓ 訊息`),用於確認某項動作已順利完成的正向回饋。 - 參數:`message`(`string`)。 - 回傳:`void`。 ```javascript import { ok } from './src/log.js'; ok('GITEA_TOKEN 可讀取 repo owner/name'); ``` ### warn 輸出一筆警告訊息(` ! 訊息`),透過 `console.warn` 寫入 stderr;用於流程仍可繼續、但需要提醒注意的非致命狀況。 - 參數:`message`(`string`)。 - 回傳:`void`。 ```javascript import { warn } from './src/log.js'; warn('對話收斂失敗(繼續執行): timeout'); ``` ### error 輸出一筆錯誤訊息(` x 訊息`),透過 `console.error` 寫入 stderr;用於明確的失敗或例外狀況,是日誌中最高的嚴重層級。 - 參數:`message`(`string`)。 - 回傳:`void`。 ```javascript import { error } from './src/log.js'; error('缺少必要環境變數: GITEA_TOKEN, PR_NUMBER'); ``` ### main AI Code Review Pipeline 的總指揮:依序串接 Step1~Step11(啟動、前置驗證、自動提交檢查、PR 對話收斂、角色平行分析、findings 合併去重、排除規則與誤報過濾、寫入 findings 並發布 Review、JSON 格式驗證、commit/push、嚴重問題把關)。結果主要透過 `process.exit()` 決定 workflow 成敗,而非以回傳值傳遞;未被攔截的未預期例外會由頂層 `main().catch(...)` 接住並以 `process.exit(1)` 結束。 - 參數:無。 - 回傳:`Promise`(正常走完且無嚴重問題時 resolve;多數結束路徑直接 `process.exit()`)。 ```javascript // 範例為示意,實際執行需搭配完整的 Gitea / CLIProxyAPI 環境變數, // 通常由 entrypoint.sh 以 `node src/main.js` 直接啟動整個 pipeline,不建議手動 import 呼叫。 import { main } from './src/main.js'; await main(); ``` ### checkRequiredEnv 檢查 code review 所需的必要環境變數是否齊全(`GITEA_TOKEN`、`GITEA_REPOSITORY`、`PR_NUMBER`、`CLI_PROXY_API`),缺任何一項即列出缺少項目。`CLI_PROXY_API` 一律直接讀環境變數,不受 `opts` 覆寫。 - 參數:`opts.token`/`opts.repo`/`opts.pr`(可選,供測試注入)。 - 回傳:`{ ok: boolean, missing: string[] }`。 ```javascript import { checkRequiredEnv } from './src/preflight.js'; checkRequiredEnv({ token: '', repo: 'owner/name', pr: '12' }); // => { ok: false, missing: ['GITEA_TOKEN', ...(若 CLI_PROXY_API 環境變數也未設)] } ``` ### verifyGiteaToken 驗證 Gitea token 有效且對指定 repo 有讀取權限:透過唯讀的 `GET /repos/{repo}` 探測;任何錯誤都被攔截並轉為回傳值,不會 throw。 - 參數:`token`(`string`,預設 `GITEA_TOKEN`)、`repo`(`string`,預設 `GITEA_REPOSITORY`)。 - 回傳:`Promise<{ok:true} | {ok:false, error:string}>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 環境變數。 import { verifyGiteaToken } from './src/preflight.js'; const result = await verifyGiteaToken(); // => { ok: true } 或 { ok: false, error: 'HTTP 401 ...' } ``` ### verifyCommentToken 驗證選用的 comment token(`GITEA_COMMENT_TOKEN`)是否可用:未提供時直接視為通過並標記 `skipped:true`(之後 comment 會沿用主 token);有提供則以 `GET /user` 探測。 - 參數:`token`(`string`,預設 `GITEA_COMMENT_TOKEN`)。 - 回傳:`Promise<{ok:true, skipped?:true} | {ok:false, error:string}>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 Gitea Token 環境變數。 import { verifyCommentToken } from './src/preflight.js'; const result = await verifyCommentToken(); ``` ### fetchLLMModels 呼叫 CLIProxyAPI 的 `/v1/models`,確認 proxy 可用與模型清單可讀;`baseURL` 未設定、連線錯誤、401 認證失效或非 2xx 回應都會被轉為結構化的失敗結果。 - 參數:`deps.fetchImpl`(預設 `fetch`)、`deps.baseURL`(預設 `CLI_PROXY_API`)、`deps.apiKey`(預設 `CLI_PROXY_API_KEY`)。 - 回傳:`Promise<{ok:true, slugs:string[]} | {ok:false, error:string}>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { fetchLLMModels } from './src/preflight.js'; const result = await fetchLLMModels(); // => { ok: true, slugs: ['gpt-4o', 'claude-sonnet-5', ...] } ``` ### verifyLLM 驗證 LLM proxy 設定可用:確認目前環境可偵測到 CLIProxyAPI;若有指定 model,額外向模型清單端點確認該 model 在可用清單內(不送 prompt)。未指定 model 時,只要求 proxy 與模型清單端點可連線。 - 參數:`deps.fetchLLMModelsFn`(預設 `fetchLLMModels`)。 - 回傳:`Promise<{ok:true, provider, command, model, models?} | {ok:false, provider?, command?, model?, error}>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { verifyLLM } from './src/preflight.js'; const result = await verifyLLM(); ``` ### runPreflight 執行所有前置驗證(Step2):環境變數、Gitea token、comment token、git 遠端(`ls-remote`)、LLM proxy;全程唯讀,不發布任何 comment,任一檢查失敗即記錄錯誤並回傳 `false`。各檢查可經 `deps` 注入覆寫,方便單元測試。 - 參數:`workspace`(`string`,預設 `GITHUB_WORKSPACE||'/workspace'`)、`deps`(可覆寫各檢查函式)。 - 回傳:`Promise`。 ```javascript // 範例為示意,實際呼叫需搭配完整的 Gitea / CLIProxyAPI 環境變數。 import { runPreflight } from './src/preflight.js'; const passed = await runPreflight('/workspace'); if (!passed) process.exit(1); ``` ### parseBotReviewComment 嘗試把一則 review comment 內文解析回 bot 產生的 finding 欄位:同時支援 review comment(嚴重等級/審查員/問題/建議)與行內 critical comment(等級/審查員/建議)兩種格式。不符合格式(如人工自由留言)時回傳 `null`。 - 參數:`body`(`string`)。 - 回傳:`{level, role, problem, suggestion} | null`。 ```javascript import { parseBotReviewComment } from './src/resolve.js'; parseBotReviewComment('**嚴重等級**:🔴 嚴重\n**審查員**:Paladin\n**問題**:缺少輸入驗證\n**建議**:補上檢查'); // => { level: 'critical', role: 'Paladin', problem: '缺少輸入驗證', suggestion: '補上檢查' } ``` ### groupConversations 把 PR 上的行內 review comment 依「檔案路徑 + 行號」收斂成對話(同一處的留言與回覆視為一段對話);對話只要任一則 comment 帶有 `resolver` 即視為已解決,同時嘗試解析出對應的每一則 bot finding。缺少 `path` 的留言會整筆跳過。 - 參數:`comments`(Gitea PR review comments 原始陣列)。 - 回傳:`Array<{key, path, line, commentIds, bodies, resolved, botFinding, botFindings, thread}>`。 ```javascript import { groupConversations } from './src/resolve.js'; const conversations = groupConversations(rawComments); // => [{ key: 'a.js|10', path: 'a.js', line: 10, resolved: false, botFindings: [...], thread: '...' }, ...] ``` ### codeWindow 取目標行附近的程式碼片段(含 1-based 行號前綴),讓 AI 對照判斷問題是否已解決;`content` 為空時回傳空字串,`lineNum` 非正數或非有限數時退回以檔案第一行為中心。 - 參數:`content`(`string`)、`lineNum`(`number`)、`radius`(`number`,預設 `CODE_WINDOW_RADIUS`=20)。 - 回傳:`string`。 ```javascript import { codeWindow } from './src/resolve.js'; const snippet = codeWindow(fileContent, 42, 5); // => '38: ...\n39: ...\n...\n47: ...'(第 42 行上下各 5 行) ``` ### judgeConversations 批次請 AI 將每個對話判為 `resolved` / `false_positive` / `open`;AI 回傳非陣列、缺漏或不合法的 `idx`,對應結果一律降級為 `open`(寧可保留)。 - 參數:`items`(`Array<{idx, path, line, thread, code}>`)、`chatFn`(預設 `chatJSON`)。 - 回傳:`Promise>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 CLIProxyAPI 環境變數。 import { judgeConversations } from './src/resolve.js'; const verdicts = await judgeConversations([{ idx: 0, path: 'a.js', line: 10, thread: '...', code: '...' }]); // => [{ idx: 0, verdict: 'resolved' }] ``` ### isSafeRepoPath 安全守衛:判定路徑是否為 repo 內的相對路徑(拒絕絕對路徑、Windows 磁碟機前綴與含 `..` 的路徑穿越),用於防止以外部 PR 檔名讀取 repo 外的檔案。 - 參數:`p`(`string`)。 - 回傳:`boolean`。 ```javascript import { isSafeRepoPath } from './src/resolve.js'; isSafeRepoPath('src/a.js'); // => true isSafeRepoPath('../../etc/passwd'); // => false isSafeRepoPath('/etc/passwd'); // => false ``` ### reconcileConversations 對話收斂主流程:取得 PR 所有行內 review comment,把每個未解決 comment 一律呼叫 Gitea resolve API 關閉;再以「檔案路徑+行號」收斂成對話、取最新程式碼交 AI 判斷,依判斷結果把對應 findings 分流為 `resolvedFindings`(已修復)/`excludedFindings`(誤報,寫入 exclusions)/`carriedFindings`(仍成立)。任一外部呼叫失敗都降級保守處理(視為 open),不中斷整體 pipeline。 - 參數:`deps`(可覆寫 `listComments`/`resolveComment`/`getFileContent`/`judge`,供測試注入)。 - 回傳:`Promise<{resolvedFindings, excludedFindings, carriedFindings, resolvedCount, falsePositiveCount, openCount, closedCount, unresolvedCount}>`。 ```javascript // 範例為示意,實際呼叫需搭配完整的 Gitea / CLIProxyAPI 環境變數。 import { reconcileConversations } from './src/resolve.js'; const reconcile = await reconcileConversations(); // => { resolvedFindings: [...], excludedFindings: [...], carriedFindings: [...], closedCount: 3, ... } ``` ### dropResolvedFindings 從 `findings` 中移除「已解決對話」對應的問題,以檔案路徑+建議內容比對(對行號漂移不敏感);`resolvedFindings` 為空時回傳 `findings` 原引用(未複製)。 - 參數:`findings`(`Array`)、`resolvedFindings`(`Array`,預設 `[]`)。 - 回傳:`Array`。 ```javascript import { dropResolvedFindings } from './src/resolve.js'; const remaining = dropResolvedFindings(oldFindings, reconcile.resolvedFindings); ``` ### addCarriedFindings 把「未解決對話」對應、但目前 `findings` 清單中已遺漏的問題加回(去重以檔案路徑+建議內容為準);有實際新增項目時輸出一行提示 log。 - 參數:`findings`(`Array`)、`carriedFindings`(`Array`,預設 `[]`)。 - 回傳:`Array`。 ```javascript import { addCarriedFindings } from './src/resolve.js'; const updated = addCarriedFindings(oldFindings, reconcile.carriedFindings); ``` ### parseRoleFile 解析單一角色 Markdown 檔內容,拆出前置 YAML frontmatter(徽章、代表色、面向、個性等欄位,會攤平到回傳物件)與本文(`body`,已去除頭尾空白);缺少合法 `---` frontmatter 區塊時拋出例外。 - 參數:`content`(`string`)。 - 回傳:`{name?, side?, focus?, badge?, color?, personality?, body, ...}`。 - 例外:缺 frontmatter 或 YAML 格式錯誤時拋出。 ```javascript import { parseRoleFile } from './src/roles.js'; const role = parseRoleFile('---\nname: Scout\nside: attack\nfocus: 安全性\n---\n審查重點...'); // => { name: 'Scout', side: 'attack', focus: '安全性', body: '審查重點...' } ``` ### loadRoles 載入所有「攻擊方」角色(frontmatter `side === 'attack'`),依檔名排序;供 Step5(角色分析產生 findings)階段使用。防守方角色(如 Paladin)不在回傳之列。 - 參數:無。 - 回傳:`Array`。 ```javascript import { loadRoles } from './src/roles.js'; const roles = loadRoles(); // => [{ name: 'Scout', side: 'attack', ... }, { name: 'Sentinel', side: 'attack', ... }, ...] ``` ### loadRole 依 frontmatter `name` 取得單一角色(比對不分大小寫),不分攻擊方/防守方,找不到回傳 `null`。 - 參數:`name`(`string`)。 - 回傳:`object | null`。 ```javascript import { loadRole } from './src/roles.js'; const paladin = loadRole('paladin'); // => { name: 'Paladin', side: 'defense', ... } ``` ### buildAnalysisPrompt 由攻擊方角色定義組出其分析用 system prompt:套用角色的徽章、名稱、面向(缺省「綜合」)、個性與審查重點本文,並附上固定指示——分析 diff 僅針對新增/修改處找問題,以固定 JSON 陣列格式(`level`/`role`/`location`/`problem`/`suggestion`)回傳,強制每條問題帶 `檔案路徑:行號`。 - 參數:`role`(`object`,需含 `name`/`body`)。 - 回傳:`string`。 - 例外:`role` 為 `null`/`undefined` 時拋出 TypeError。 ```javascript import { buildAnalysisPrompt } from './src/roles.js'; import { loadRole } from './src/roles.js'; const prompt = buildAnalysisPrompt(loadRole('Scout')); ``` ### buildLocateLinePrompt 組出「補行號」用的 system prompt:用於某角色先前提出的 finding 其 `location` 只有檔名、缺行號時,請 LLM 對照該檔 diff 找出實際行號,只回 `{"line": 數字}`(找不到回 `{"line": 0}`)。`role` 可省略或為 `null`,此時名稱退回 `'AI Review'`。 - 參數:`role`(`object | null | undefined`,可選)。 - 回傳:`string`。 ```javascript import { buildLocateLinePrompt } from './src/roles.js'; const prompt = buildLocateLinePrompt({ name: 'Scout', focus: '安全性' }); ``` ### buildVerdictPrompt 由防守方角色定義組出「單條 finding 誤報裁決」用的 system prompt:要求對一條攻擊方 finding 判定「成立 / 誤報」,只回 `{"verdict", "reason"}`,無法確定時一律回 `"confirmed"`(寧可保留)。`role` 為空值時退回固定的通用裁判 persona(🛡️ Paladin)。 - 參數:`role`(`object | null | undefined`)、`exclusionHint`(`string`,預設 `''`)。 - 回傳:`string`。 ```javascript import { buildVerdictPrompt } from './src/roles.js'; import { loadRole } from './src/roles.js'; const prompt = buildVerdictPrompt(loadRole('Paladin'), '已知誤報清單:...'); ``` ### getRoleIntro 由角色陣列產生「AI Code Review 團隊」介紹用的 Markdown 表格(角色/面向/個性三欄),通常用於 PR 留言/審查報告開頭呈現參與審查的角色陣容。 - 參數:`roles`(`Array`)。 - 回傳:`string`(Markdown 表格)。 - 例外:`roles` 非可迭代值時拋出 TypeError。 ```javascript import { getRoleIntro } from './src/roles.js'; import { loadRoles } from './src/roles.js'; console.log(getRoleIntro(loadRoles())); // ## 🤖 AI Code Review 團隊 // | 👤 角色 | 🎯 面向 | 🧠 個性 | // |--------|--------|--------| // | **🔍 Scout** | 安全性 | ... | ``` ### extractUsage 把各平台回應中的 token usage 正規化成 `{ promptTokens, completionTokens, totalTokens }`:支援 OpenAI 相容 usage、OpenAI Responses、Gemini `usageMetadata`、Ollama 原生欄位、OpenCode tokens。回應中沒有任何可辨識的 usage 時回傳 `null`。 - 參數:`data`(`*`)。 - 回傳:`{promptTokens, completionTokens, totalTokens} | null`。 ```javascript import { extractUsage } from './src/usage.js'; extractUsage({ usage: { prompt_tokens: 100, completion_tokens: 50, total_tokens: 150 } }); // => { promptTokens: 100, completionTokens: 50, totalTokens: 150 } ``` ### recordUsage 記錄一次 LLM 呼叫的 usage(無法解析時仍計一次呼叫,但 token 計 0),並累加進模組級 `runUsage`。 - 參數:`data`(`*`)。 - 回傳:`{promptTokens, completionTokens, totalTokens} | null`。 ```javascript import { recordUsage } from './src/usage.js'; recordUsage({ usage: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 } }); ``` ### getRunUsage 取得本次執行至今的 token 累計(複本),修改回傳值不影響內部狀態。 - 參數:無。 - 回傳:`{calls, promptTokens, completionTokens, totalTokens}`。 ```javascript import { getRunUsage } from './src/usage.js'; const usage = getRunUsage(); // => { calls: 5, promptTokens: 1200, completionTokens: 600, totalTokens: 1800 } ``` ### resetRunUsage 重置模組級 token 累計(測試用),將 `calls`/`promptTokens`/`completionTokens`/`totalTokens` 全部歸零。 - 參數:無。 - 回傳:`void`。 ```javascript import { resetRunUsage } from './src/usage.js'; resetRunUsage(); ``` ### recordRateLimit 從回應 header 擷取速率配額剩餘量/上限:支援 OpenAI 相容(`x-ratelimit-*-tokens`)與 Anthropic(`anthropic-ratelimit-tokens-*`),兩者皆缺時退而採用 requests 維度;記錄「最近一次」的數值。 - 參數:`headers`(`object | null | undefined`)。 - 回傳:`void`。 ```javascript import { recordRateLimit } from './src/usage.js'; recordRateLimit({ 'x-ratelimit-remaining-tokens': '9000', 'x-ratelimit-limit-tokens': '10000' }); ``` ### getRateLimit 取得最近一次的速率配額快照(複本)。 - 參數:無。 - 回傳:`{hasData, remaining, limit, kind}`。 ```javascript import { getRateLimit } from './src/usage.js'; const rate = getRateLimit(); // => { hasData: true, remaining: 9000, limit: 10000, kind: 'tokens' } ``` ### resetRateLimit 重置速率配額快照(測試用),將 `hasData`/`remaining`/`limit`/`kind` 回復為初始狀態。 - 參數:無。 - 回傳:`void`。 ```javascript import { resetRateLimit } from './src/usage.js'; resetRateLimit(); ``` ### fetchAccountQuota 取得指定平台的帳號額度;任何失敗都降級為 `{ available: false, reason }`,不丟例外。多數平台(如 CLIProxyAPI)因權限限制誠實回報「無法取得」,僅 OpenRouter 這類平台可實際查詢。 - 參數:`provider`(`string`)、`config`(`{apiKey?, apiKeys?, baseURL?}`,預設 `{}`)、`deps.get`(可注入 HTTP GET,預設 `axios.get`)。 - 回傳:`Promise<{available, reason?, used?, limit?, remaining?, currency?, source?}>`。 ```javascript // 範例為示意,實際呼叫需搭配有效的 API Key 環境變數。 import { fetchAccountQuota } from './src/usage.js'; const quota = await fetchAccountQuota('cliproxyapi', { apiKeys: [], baseURL: 'https://proxy.example' }); // => { available: false, reason: 'CLIProxyAPI 不提供帳號額度資訊' } ``` ### resolveRemainingPercent 計算「剩餘可用百分比」,依優先序擇一:① 帳號額度(有有效上限)→ 剩餘 credits / 上限;② 速率配額(有有效上限)→ 當前視窗剩餘 / 上限。上限或剩餘為無效值時跳過計算,落到 `{ percent: null, reason }`。 - 參數:`quota`(`fetchAccountQuota` 的回傳結果)、`rate`(`getRateLimit` 的回傳結果)。 - 回傳:`{percent, basis, remaining, limit, unit} | {percent:null, reason}`。 ```javascript import { resolveRemainingPercent } from './src/usage.js'; resolveRemainingPercent( { available: false, reason: '不提供額度' }, { hasData: true, remaining: 9000, limit: 10000, kind: 'tokens' }, ); // => { percent: 90, basis: '速率配額(當前視窗,token)', remaining: 9000, limit: 10000, unit: '' } ``` ### formatUsageStats 產生 PR Review 本文用的「AI 助理使用量」Markdown 區塊,內含本次呼叫次數、token 表格與剩餘可用百分比說明。 - 參數:`provider`(`string`)、`model`(`string`)、`usage`(`getRunUsage` 結果)、`quota`(`fetchAccountQuota` 結果)、`rate`(`getRateLimit` 結果)。 - 回傳:`string`(多行 Markdown)。 ```javascript import { formatUsageStats } from './src/usage.js'; const section = formatUsageStats('cliproxyapi', 'gpt-4o', { calls: 5, promptTokens: 1200, completionTokens: 600, totalTokens: 1800 }, quota, rate); ``` ### formatUsageStatsLine 產生單行 log 用的使用量摘要文字,內容與 `formatUsageStats` 一致但為單行純文字,token 數字未套用千分位格式化。 - 參數:`provider`(`string`)、`model`(`string`)、`usage`(`getRunUsage` 結果)、`quota`(`fetchAccountQuota` 結果)、`rate`(`getRateLimit` 結果)。 - 回傳:`string`。 ```javascript import { formatUsageStatsLine } from './src/usage.js'; const summary = formatUsageStatsLine('cliproxyapi', 'gpt-4o', { calls: 5, promptTokens: 1200, completionTokens: 600, totalTokens: 1800 }, quota, rate); // => '本次 cliproxyapi/gpt-4o: 提示1200 + 回應600 = 1800 token(5 次呼叫);剩餘可用: 90%(...)' ```