ai-review-resolve/20260623-110950
develop
新增「階段十三:PR 對話收斂」功能。前置驗證通過、且非 AI 助理自動提交後(Step2),讀取 PR 上所有行內 review comment,依「檔案路徑+行號」收斂成對話,對照 PR head 最新程式碼由 AI 判斷各對話指出的問題是否已解決:
Step2
POST /repos/{repo}/pulls/comments/{id}/resolve
Step4
findings.json
app/resolve.js
parseBotReviewComment
groupConversations
codeWindow
judgeConversationsResolved
reconcileConversations
dropResolvedFindings
addCarriedFindings
app/gitea.js
listPullReviews
getPullReviewComments
listAllReviewComments
resolvePullReviewComment
getFileContentAtRef
app/main.js
app/resolve.test.js
app/gitea.test.js
README.md
TODO.md
node --test *.test.js
🔍 服務:gemini 模型:gemini-2.5-flash
@@ -0,0 +37,4 @@
level: level || 'warning',
role: role || 'AI Review',
problem: problem || '',
suggestion: suggestion || problem || '',
嚴重等級:🟡 警告 審查員:Maya 問題:函式 parseBotReviewComment 缺少對 levelRaw 為空但其他欄位存在時的測試案例。程式碼有 level: level || 'warning' 處理,但此行為應被明確驗證。 建議:請新增測試案例,模擬評論內文缺少 嚴重等級 或 等級 欄位,但有 審查員 和 問題/建議 欄位時,確認 level 會正確地預設為 warning。
levelRaw
level: level || 'warning'
嚴重等級
等級
審查員
問題
建議
level
warning
@@ -0,0 +38,4 @@
};
嚴重等級:🟡 警告 審查員:Maya 問題:函式 parseBotReviewComment 缺少對 problem 存在但 suggestion 為空字串的測試案例。程式碼有 suggestion: suggestion || problem || '' 處理,但此行為應被明確驗證。 建議:請新增測試案例,模擬評論內文只包含 問題 欄位而無 建議 欄位時,確認 suggestion 會正確地使用 problem 的內容。
problem
suggestion
suggestion: suggestion || problem || ''
@@ -0,0 +67,4 @@
return [...groups.values()].map(g => ({ ...g, thread: g.bodies.join('\n---\n') }));
}
/** 取目標行附近的程式碼片段(含行號),讓 AI 對照判斷問題是否已解決。 */
嚴重等級:🔵 建議 審查員:Mage 問題:在 groupConversations 函式中,若行內 review comment 缺乏 path 或 position/original_position 資訊,它們將會被歸類到一個共同的 key (例如 |0)。這可能導致多個實際上不相關的、缺乏位置資訊的留言被錯誤地歸類為同一個對話群組。雖然這類留言通常不屬於「行內」評論,且 parseBotReviewComment 可能會將其視為非 bot 留言,但這種歸類方式可能與預期不符。 建議:考慮是否應明確地過濾掉缺乏 path 或有效 position 的留言,或為這些留言提供一個更具區分性的預設 key,以避免不相關的留言被意外地歸併。例如,可以在迴圈開始時增加判斷:if (!c?.path || (!c?.position && !c?.original_position)) continue;。
path
position
original_position
key
|0
if (!c?.path || (!c?.position && !c?.original_position)) continue;
@@ -0,0 +87,4 @@
const payload = items.map(it => ({ idx: it.idx, path: it.path, line: it.line, thread: it.thread, code: it.code }));
const result = await chatFn(systemPrompt, JSON.stringify(payload));
const byIdx = new Map(
(Array.isArray(result) ? result : [])
嚴重等級:🟡 警告 審查員:Maya 問題:函式 judgeConversationsResolved 缺少對 chatFn 拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。 建議:請新增測試案例,模擬 chatFn 拋出錯誤時,確認 judgeConversationsResolved 會正確地將錯誤向上拋出,以便呼叫者處理。
chatFn
@@ -0,0 +88,4 @@
.filter(r => Number.isInteger(r?.idx))
嚴重等級:🟡 警告 審查員:Maya 問題:函式 judgeConversationsResolved 缺少對 AI 回傳結果中元素缺少 idx 或 resolved 欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。 建議:請新增測試案例,模擬 chatFn 回傳的陣列中,有些物件缺少 idx 或 resolved 屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。
idx
resolved
@@ -0,0 +129,4 @@
line(`對話收斂: 對話總數=${conversations.length} 已解決/不可處理=${alreadyResolved} 待判斷=${open.length}`);
if (open.length === 0) return { ...EMPTY };
const fileCache = new Map();
嚴重等級:🔴 嚴重 審查員:Maya 問題:函式 reconcileConversations 在取得單一檔案內容 (getFileContent) 失敗時,會中斷整個對話收斂流程。這會導致即使只有一個檔案出錯,整個 PR 的收斂都無法完成。 建議:請修改 reconcileConversations,在 fileCache.set(filePath, await getFileContent(filePath)) 的迴圈中,為 getFileContent 加上 try-catch 區塊。當單一檔案取得失敗時,應記錄警告並將該檔案的內容視為空字串,而不是中斷整個流程,以確保其他檔案的處理不受影響。
getFileContent
fileCache.set(filePath, await getFileContent(filePath))
try-catch
@@ -0,0 +141,4 @@
thread: c.thread,
code: codeWindow(fileCache.get(c.path) || '', c.line),
}));
嚴重等級:🟡 警告 審查員:Maya 問題:函式 reconcileConversations 缺少對 judge 拋出錯誤情境的明確測試。雖然程式碼有 try-catch 處理,但應有專門的測試案例來驗證此失敗路徑的行為。 建議:請新增測試案例,模擬 judge 函式拋出錯誤時,確認 reconcileConversations 能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即 verdicts 應全部為 resolved: false)。
judge
verdicts
resolved: false
@@ -0,0 +151,4 @@
const resolvedSet = new Set(verdicts.filter(v => v.resolved).map(v => v.idx));
const resolvedFindings = [];
嚴重等級:🔴 嚴重 審查員:Rogue 問題:這裡又在浪費時間!reconcileConversations 函式在取得所有獨特的檔案路徑後,又在迴圈裡對每個檔案路徑依序呼叫 getFileContent。如果有很多檔案需要檢查,這會導致 F 次遠端 API 呼叫依序執行,嚴重拖慢整體流程。 建議:改用 Promise.all 或 Promise.allSettled 來並行發送所有 getFileContent 的請求。這樣可以大幅減少等待時間,讓檔案內容的取得幾乎同時完成。
F
Promise.all
Promise.allSettled
@@ -0,0 +170,4 @@
pushCarried(carriedFindings, c);
const unresolvedCount = open.length - resolvedCount;
嚴重等級:🔴 嚴重 審查員:Rogue 問題:又來了!reconcileConversations 函式在迴圈裡對每個需要解決的對話依序呼叫 resolveComment。這又是一個 N+1 查詢問題,如果有很多對話需要解決,會導致 N_open 次遠端 API 呼叫依序執行,效率極差。 建議:改用 Promise.allSettled 來並行發送所有 resolveComment 的請求。這樣可以大幅減少等待時間,讓對話的解決幾乎同時完成,即使部分失敗也不會中斷其他請求。
resolveComment
N_open
@@ -0,0 +49,4 @@
const groups = new Map();
for (const c of comments || []) {
const filePath = typeof c?.path === 'string' ? c.path : '';
if (!filePath) continue; // 無檔案路徑的留言無法定位,跳過以免併入共用群組
嚴重等級:🟡 警告 審查員:Assassin 問題:在 parseBotReviewComment 函式中,從 Gitea comment 內文解析出的 problem 和 suggestion 欄位,若包含惡意 HTML 或 JavaScript 程式碼,且這些內容在後續的處理或顯示中未經適當的輸出編碼,可能導致跨網站指令碼(XSS)攻擊。 建議:確保所有從外部來源解析出的字串(特別是 problem 和 suggestion)在任何將其渲染到網頁或其他使用者介面的地方,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼、JavaScript 字串編碼等),以防止 XSS 攻擊。
@@ -0,0 +142,4 @@
const items = open.map((c, idx) => ({
嚴重等級:🔴 嚴重 審查員:Assassin 問題:在 judgeConversationsResolved 函式中,thread(來自 Gitea comment 內容)和 code(來自 PR 檔案內容)被直接拼接進傳給 LLM 的 payload 中。如果攻擊者能夠控制這些內容,他們可以透過注入惡意指令來劫持 LLM 的行為,例如使其始終將特定問題判斷為已解決,或嘗試從 LLM 獲取敏感資訊(提示詞注入)。 建議:對所有傳遞給 LLM 的外部輸入(如 thread 和 code)進行嚴格的淨化和隔離。考慮使用結構化輸入而非直接拼接字串,並在 LLM 提示詞中明確指示其忽略任何試圖改變其行為的指令。對於敏感操作,應建立多層驗證機制,不單純依賴 LLM 的判斷。
thread
code
payload
@@ -0,0 +177,4 @@
const c = open[i];
const outcome = resolveOutcome.get(i);
if (outcome?.status === 'fulfilled') {
resolvedCount += 1;
嚴重等級:🔴 嚴重 審查員:Assassin 問題:在 reconcileConversations 函式中,從外部 Gitea comment 取得的 c.path(檔案路徑)未經額外驗證或淨化,直接傳遞給了 getFileContent(即 getFileContentAtRef)。由於 getFileContentAtRef 存在路徑穿越漏洞,攻擊者可以透過在 PR 中建立惡意檔案名稱,並在該檔案上留言,來觸發路徑穿越,讀取伺服器上的任意檔案。 建議:在將 c.path 傳遞給 getFileContent 之前,必須對其進行嚴格的白名單驗證,確保它只包含預期的檔案名稱字元,且不包含任何路徑穿越序列(例如 .. 或 /)。或者,確保 getFileContentAtRef 的路徑處理是絕對安全的,不允許任何形式的路徑穿越。
c.path
..
/
@@ -0,0 +204,4 @@
.trim()
.toLowerCase();
嚴重等級:🟡 警告 審查員:Leo 問題:函式 normalizeKey 對建議內容進行了非常積極的正規化,移除了所有標點符號、符號和空白字元。雖然這有助於避免行號漂移和微小措辭差異造成的重複判斷,但過度正規化可能會導致不同但語意相近的建議被視為相同,進而影響問題追蹤的精確性。 建議:請評估這種積極正規化是否會導致誤判。如果發現有不同建議被錯誤合併的情況,可以考慮放寬正規化規則,例如只移除空白字元和部分標點符號,或加入其他判斷維度(如關鍵字比對)來提高精確度。
normalizeKey
🔍 服務:opencode 模型:gemini-2.5-flash
@@ -0,0 +5,4 @@
const EMPTY = { resolvedFindings: [], carriedFindings: [], resolvedCount: 0, unresolvedCount: 0 };
/** 取出 "**label**:value" 這一行的 value(單行)。 */
function fieldValue(body, label) {
嚴重等級:🔵 建議 審查員:Bard 問題:RegExp 在函式內部重複建立,造成不必要的效能損耗。 建議:將正則表達式移至函式外部宣告為常數。
@@ -0,0 +15,4 @@
if (raw.includes('嚴重')) return 'critical';
if (raw.includes('警告')) return 'warning';
if (raw.includes('建議')) return 'info';
return null;
嚴重等級:🟡 警告 審查員:Assassin 問題:函式 parseBotReviewComment 動態產生正規表達式,且輸入來源 body 為外部輸入,存在 Regex Injection 風險。 建議:將正規表達式改為靜態定義,並透過 String.raw 或更安全的字串處理方式來匹配標籤,確保輸入不包含特殊 regex 字元。
body
String.raw
嚴重等級:🟡 警告 審查員:Assassin 問題:在 parseBotReviewComment 函式中,從 Gitea comment 內文解析出的 problem 和 suggestion 欄位若包含惡意內容且未經適當輸出編碼,可能導致 XSS 攻擊。 建議:確保所有從外部來源解析出的字串在渲染到任何介面時,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼),以防止 XSS 攻擊。
嚴重等級:🔵 建議 審查員:Mage 問題:缺乏位置資訊的留言會被歸類到同一個預設 key,可能導致不相關留言被錯誤歸併。 建議:明確過濾缺乏 path 或 position 的留言,或提供更具區分性的預設 key。
@@ -0,0 +74,4 @@
const lines = content.split('\n');
const center = Number.isFinite(lineNum) && lineNum > 0 ? lineNum - 1 : 0;
const start = Math.max(0, center - radius);
const end = Math.min(lines.length, center + radius + 1);
嚴重等級:🔴 嚴重 審查員:Mage 問題:在 judgeConversationsResolved 函式中,對 chatFn 的結果結構缺乏足夠的嚴格檢查。若回傳結構不符合預期,可能導致所有對話被錯誤判定為「未解決」。 建議:增加對 result 結構的嚴格檢查。如果 result 不是預期的陣列結構,應拋出例外或進行更謹慎的錯誤處理,而不是默默地將所有對話視為未解決。
result
嚴重等級:🟡 警告 審查員:Leo 問題:程式碼片段定位邏輯(如字串拼接行號)與上下文擷取策略(如 radius)寫死在函式內,擴展性與維護性不足。 建議:建立明確的 Location 物件封裝定位資訊,並將 radius 或擷取策略抽離為配置參數或常數。
Location
radius
嚴重等級:🟡 警告 審查員:Rogue 問題:大量使用字串拼接產生暫存物件,以及並行請求未限制數量,在高負載下可能導致 GC 壓力或觸發 API 限流。 建議:對於大量 comments,考慮使用複合物件或分層 Map 結構。引入請求並行限制(如 p-limit)來確保系統穩定性。
p-limit
fileCache.set(filePath, '');
嚴重等級:🟡 警告 審查員:Maya 問題:缺少關鍵邊界條件與異常路徑的測試案例。包含 judge 拋出錯誤、chatFn 解析異常、levelRaw 或 suggestion 空值、getFileContent 失敗以及混合正確/錯誤的判斷數據等場景。 建議:請在 app/resolve.test.js 中新增這些邊界條件的測試案例,確保系統在面對 AI 異常輸出、API 失敗、或輸入欄位缺失時,仍能穩健處理並符合預期行為。
嚴重等級:🔴 嚴重 審查員:Assassin 問題:LLM 提示詞注入風險:在 judgeConversationsResolved 函式中,外部來源的 thread 和 code 被直接拼接進傳給 LLM 的 payload 中,攻擊者可能注入惡意指令來劫持 LLM 行為。 建議:對所有傳遞給 LLM 的外部輸入進行嚴格的淨化和隔離。使用結構化輸入而非直接拼接字串,並在提示詞中明確指示 AI 忽略任何試圖下達指令的內容,僅對邏輯進行判斷。對於敏感操作,應建立多層驗證機制。
resolveTargets.map(({ c }) => resolveComment(c.commentIds[0])),
);
const resolveOutcome = new Map();
resolveTargets.forEach(({ i }, j) => resolveOutcome.set(i, settled[j]));
嚴重等級:🔵 建議 審查員:Rogue 問題:Promise.allSettled 的結果處理邏輯過於冗長,產生不必要的中間變數。 建議:優化處理邏輯,直接在迴圈內處理或使用更緊湊的寫法。
@@ -0,0 +184,4 @@
if (outcome?.status === 'rejected') {
warn(`resolve 對話失敗(保留為未解決): ${c.path}:${c.line} error=${outcome.reason?.message}`);
嚴重等級:🟡 警告 審查員:Mage 問題:在 reconcileConversations 函式中,並行(Promise.all)呼叫 resolveComment,即使個別呼叫失敗,也僅在 settled 中記錄為 rejected 並印出 warn。然而,若 resolveComment 失敗是因為 Authorization token 過期或權限不足,後續所有的 resolve 呼叫都會失敗,此時程式碼沒有對這些特定的錯誤進行分類處理。 建議:應判斷 outcome.reason 的錯誤類型。若是連線/權限相關的嚴重錯誤,應立即停止後續的 resolve 嘗試,避免在已知無法成功的情況下發出無效請求。
settled
rejected
warn
Authorization
resolve
outcome.reason
@@ -0,0 +192,4 @@
ok(`對話收斂完成: resolved=${resolvedCount} unresolved=${unresolvedCount} 加回 findings=${carriedFindings.length}`);
return { resolvedFindings, carriedFindings, resolvedCount, unresolvedCount };
嚴重等級:🔵 建議 審查員:Mage 問題:對 botFinding 的存取缺乏防禦性檢查。 建議:在 push 之前增加防禦性檢查,確保物件完整性。
botFinding
push
嚴重等級:🟡 警告 審查員:Leo 問題:正規化邏輯(normalizeKey 等)過於激進且未快取,既可能導致語意相近建議被誤判為相同,也在頻繁比較時造成效能浪費。 建議:請評估目前的正規化規則,若發現誤判,放寬規則或加入關鍵字比對。將簽章產生邏輯抽離為獨立 Helper 函式,並在產生時進行快取(Memoize)以提升效能。
本次審查(opencode / gemini-2.5-flash,共 9 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
export function parseBotReviewComment(body) {
if (typeof body !== 'string' || !body.includes('**')) return null;
const normalized = body.replace(/\r\n/g, '\n');
const levelRaw = fieldValue(normalized, '嚴重等級') || fieldValue(normalized, '等級');
const role = fieldValue(normalized, '審查員');
if (!content) return '';
* 4. 未解決且可解析為 bot finding 者,收集為「加回問題列表」清單。
* 任一外部呼叫失敗都降級處理(保守視為未解決),不中斷整體 pipeline。
*/
export async function reconcileConversations(deps = {}) {
@@ -0,0 +12,4 @@
/**
* 把各平台回應中的 token usage 正規化成 { promptTokens, completionTokens, totalTokens }。
* 支援:OpenAI 相容 usage、OpenAI Responses(input/output_tokens)、
* Gemini usageMetadata、Ollama 原生 eval_count、OpenCode tokens。
嚴重等級:🟡 警告 審查員:Mage 問題:extractUsage 中對於 OpenAI 相容格式的處理:const total = num(u.total_tokens) || prompt + completion;。如果 API 回傳了 total_tokens: 0(雖然極少見但非零可能),這裡的邏輯會觸發 prompt + completion 的計算,導致數值不準確。 建議:應明確判斷 u.total_tokens != null 而非僅檢查其 truthiness,以確保在 API 明確回傳 0 時能正確讀取。
const total = num(u.total_tokens) || prompt + completion;
total_tokens: 0
prompt + completion
u.total_tokens != null
@@ -0,0 +163,4 @@
export async function fetchAccountQuota(provider, config = {}, deps = {}) {
const get = deps.get || axios.get;
const strategy = QUOTA_STRATEGIES[provider];
嚴重等級:🟡 警告 審查員:Assassin 問題:在 fetchAccountQuota 中,使用 axios.get 直接請求傳入的 baseURL。如果 baseURL 是由設定檔動態讀取,攻擊者可能會透過修改設定檔將其導向惡意伺服器(SSRF),進而竊取 API Key 或發送偽造請求。 建議:應對 baseURL 進行嚴格的白名單校驗,確保其僅能連線至合法的 API 提供商域名。不要信任外部設定檔中的 URL。
fetchAccountQuota
axios.get
baseURL
if (!g.botFinding) {
const finding = parseBotReviewComment(body);
if (finding) g.botFinding = { ...finding, location: lineNum ? `${filePath}:${lineNum}` : filePath };
嚴重等級:🟡 警告 審查員:Maya 問題:在 codeWindow 函數中,缺乏對輸入的邊界檢查,特別是當 lineNum 為 0 或負數,或是大於總行數時,可能導致行為不預期或 slice 產生錯誤。 建議:建議在計算 start 和 end 時,增加明確的邊界檢核與處理,確保即使 lineNum 異常時也能安全返回或處理。
lineNum
start
end
@@ -0,0 +14,4 @@
* 回應中沒有任何可辨識的 usage 時回傳 null。
嚴重等級:🔴 嚴重 審查員:Mage 問題:在 extractUsage 中,對於 data.usage 的屬性存取直接使用 num(...),這在 data.usage 如果是 null 或其他 falsy 值但被 typeof 判斷通過時(JS 的 typeof null === 'object'),會導致錯誤。 建議:應明確檢查 u 是否為嚴格的 object 且非 null,例如 if (u && typeof u === 'object' && !Array.isArray(u))。
extractUsage
data.usage
num(...)
null
typeof
typeof null === 'object'
u
object
if (u && typeof u === 'object' && !Array.isArray(u))
@@ -0,0 +103,4 @@
if (remaining == null || limit == null) return;
rateLimit.hasData = true;
嚴重等級:🔴 嚴重 審查員:Assassin 問題:函數 isOpenRouterBaseURL 僅使用 new URL(baseURL).hostname.endsWith('.openrouter.ai') 來判斷,這極易受到偽造域名攻擊(如 openrouter.ai.malicious.com),導致惡意主機被信任為 OpenRouter,進而洩漏 API Key。 建議:應修改為嚴格比對,例如 hostname === 'openrouter.ai',且必須包含 protocol 檢查(如 https),並建議採用白名單機制而非簡單的 endsWith。
isOpenRouterBaseURL
new URL(baseURL).hostname.endsWith('.openrouter.ai')
openrouter.ai.malicious.com
hostname === 'openrouter.ai'
https
endsWith
@@ -0,0 +112,4 @@
/** 取得最近一次的速率配額快照(複本)。 */
export function getRateLimit() {
return { ...rateLimit };
嚴重等級:🟡 警告 審查員:Mage 問題:在 recordRateLimit 中,處理 Header 時將所有 Key 轉為小寫並存入物件 h,如果原始 Header 中存在多個相同名稱但不同大小寫的 Header(雖然 HTTP 標準規定 Key 不區分大小寫,但某些實作可能會有不一致),可能會造成覆蓋。 建議:雖然 HTTP 規範不區分,但為了安全起見,應先確認環境使用的 axios 版本對 Header 的處理方式,或確保在轉換前沒有遺漏必要資訊。
recordRateLimit
h
* (如 `openrouter.ai.evil.com` 或 `evil.com/openrouter.ai`)矇騙而把 API key 送往惡意主機。
function isOpenRouterBaseURL(baseURL) {
try {
嚴重等級:🔴 嚴重 審查員:Assassin 問題:在 QUOTA_STRATEGIES 中,如果 config.apiKeys 是一個陣列,代碼只取 [0] 作為 API Key,但如果這個 key 是洩漏的或是環境配置錯誤,可能會導致敏感資訊在未經嚴格驗證的情況下被發送到 baseURL 指定的端點。 建議:請務必確保所有的 API 請求都經過完整的信任邊界審核,不要僅憑環境變數就自動信任該 Key 具備查詢帳號額度的權限,並在傳輸前對 baseURL 進行嚴格的白名單檢查。
QUOTA_STRATEGIES
config.apiKeys
[0]
if (isOpenRouterBaseURL(cfg.baseURL)) return fetchOpenRouterQuota(cfg, get);
return { available: false, reason: 'OpenAI 帳號額度需 dashboard session 權限,API key 無法取得' };
},
claude: async () => ({ available: false, reason: 'Anthropic 額度需 Admin API 權限,一般 API key 無法取得' }),
@@ -0,0 +173,4 @@
* 取得指定平台的帳號額度。任何失敗都降級為 { available: false, reason },不丟例外。
* deps.get 可注入以利測試(預設 axios.get)。
嚴重等級:🔴 嚴重 審查員:Mage 問題:在 fetchAccountQuota 中,呼叫 strategy 時傳入的 config 物件,如果在特定 strategy 中被意外修改,會影響到全域的 config 狀態,且傳入的 get 函數來源若未被嚴格隔離,可能存在潛在的請求偽造風險。 建議:傳入 strategy 的 config 應進行淺拷貝(shallow copy),確保不可變性。
strategy
config
get
@@ -0,0 +86,4 @@
export function codeWindow(content, lineNum, radius = CODE_WINDOW_RADIUS) {
嚴重等級:🟡 警告 審查員:Maya 問題:在 judgeConversationsResolved 函數中,AI 判斷回傳結構如果不符合預期(非陣列),雖有降級處理,但未驗證當 AI 回傳包含無效 idx 或缺少 resolved 欄位的物件時,對應邏輯是否正確過濾。 建議:補測試案例,模擬 AI 回傳包含無效結構(如 idx 為字串、缺少 resolved)的 JSON,確保系統能正確忽略無效項並將其視為未解決。
@@ -0,0 +139,4 @@
* 2. 取每個對話所在檔案的最新內容,請 AI 判斷問題是否已解決;
* 3. 已解決者呼叫 Gitea resolve API 解決對話,並記錄其 finding(供移除舊問題);
嚴重等級:🔴 嚴重 審查員:Maya 問題:reconcileConversations 核心流程中,對於 getFileContent 失敗或內容為空的處理邏輯,直接降級為空字串並視為未解決,但若檔案內容實際上非空且未解決,這可能導致判斷偏差。 建議:補測試案例,模擬 getFileContent 拋出錯誤時,reconcileConversations 是否正確地將對話保留為未解決,且後續統計數字(carriedFindings)是否正確。
carriedFindings
opencode: async () => ({ available: false, reason: '自架服務,無帳號額度概念' }),
嚴重等級:🟡 警告 審查員:Maya 問題:fetchAccountQuota 策略在處理 API key 時,假設 apiKeys 陣列存在並取第一個,若傳入的 config.apiKeys 為空陣列或 undefined,缺乏明確的防禦與測試。 建議:補測試案例,模擬 config.apiKeys 為空或無效的情境,確認系統降級行為是否符合預期。
apiKeys
@@ -0,0 +208,4 @@
* 計算「剩餘可用百分比」,依優先序擇一:
* 1. 帳號額度(quota 有上限)→ 剩餘 credits / 上限;
* 2. 速率配額(rate limit header)→ 當前視窗剩餘 / 上限;
嚴重等級:🟡 警告 審查員:Maya 問題:resolveRemainingPercent 函數負責處理額度計算,但針對 quota.limit 為 0 的情況缺乏顯式處理,可能會導致除以零或錯誤的百分比計算結果。 建議:補測試案例,模擬 quota.limit 為 0 的情境,確認系統是否正確處理或返回錯誤訊息,避免計算偏差。
resolveRemainingPercent
quota.limit
@@ -0,0 +4,4 @@
// 預先編譯各欄位標籤的擷取正則(靜態定義:避免每次呼叫重建,也排除以外部輸入動態組 regex 的風險)
嚴重等級:🔵 建議 審查員:Bard 問題:EMPTY 常數命名過於通用,容易與其他模組中的同名變數衝突,且定義在模組頂層略顯突兀。 建議:建議加上命名空間前綴,例如 RECONCILE_DEFAULT_STATE,以增加語義清晰度。
EMPTY
RECONCILE_DEFAULT_STATE
@@ -0,0 +7,4 @@
const FIELD_PATTERNS = {
嚴重等級: /\*\*嚴重等級\*\*[::]\s*(.+)/,
等級: /\*\*等級\*\*[::]\s*(.+)/,
嚴重等級:🔵 建議 審查員:Bard 問題:FIELD_PATTERNS 的正則表達式對於冒號的定義同時包含了全形與半形,雖然容錯性高,但建議統一規範以維持風格一致性。 建議:建議統一使用半形冒號,並在解析前進行正規化處理,而非在正則中處理所有可能性。
FIELD_PATTERNS
@@ -0,0 +120,4 @@
rateLimit.remaining = null;
rateLimit.limit = null;
rateLimit.kind = null;
嚴重等級:🟡 警告 審查員:Bard 問題:recordRateLimit 函式中對於 headers 的處理,將所有 key 轉換為小寫後檢查,這雖然兼容了多種平台規範,但處理邏輯稍顯冗長,降低了程式碼的流暢度。 建議:建議提取一個專門處理 header 正規化的工具函式,使主邏輯更簡潔。
@@ -0,0 +164,4 @@
gemini: async () => ({ available: false, reason: 'Gemini 額度由 Google Cloud quota 管理,API key 無法直接查詢' }),
嚴重等級:🔵 建議 審查員:Bard 問題:fetchAccountQuota 中的 QUOTA_STRATEGIES 物件定義龐大,將所有平台的策略硬編碼在此處,未來若新增更多 LLM 供應商,此處將變得難以維護。 建議:建議將各供應商的額度查詢策略抽離至獨立的檔案或策略模式處理,以保持 usage.js 的整潔。
usage.js
本次審查(opencode / gemini-2.5-flash,共 19 次呼叫)
@@ -271,3 +273,3 @@
REVIEW_SEVERITY_LABELS,
['🟡 警告', '🔵 建議'],
assert.deepEqual(
嚴重等級:🟡 警告 審查員:Maya 問題:新增了 usageSection 功能,但測試案例中沒有驗證當 usageSection 為空字串或未傳入時,輸出的 body 是否正確排版(例如不會多出不必要的換行符號)。 建議:補充測試案例,驗證當 usageSection 為空時,輸出的 Markdown 結構是否如預期(沒有多餘的 `
usageSection
` 結尾)。
@@ -0,0 +243,4 @@
falsePositiveCount += 1;
if (c.botFinding) excludedFindings.push(toExclusion(c.botFinding));
} else {
openCount += 1;
嚴重等級:🟡 警告 審查員:Maya 問題:reconcileConversations 中的 reconcile 流程包含多個步驟(取得 comments、group、判斷、resolve),一旦中間有外部呼叫失敗就降級。目前的測試案例主要覆蓋了「全部成功」或「特定某個失敗」,但缺乏對「部分 resolve 成功,部分 resolve 失敗」這種狀態的驗證。 建議:補充測試案例,模擬部分 resolveComment 成功、部分失敗的情境,驗證最終回傳的 closedCount 與 resolvedFindings 等統計數據是否正確計算。
reconcile
closedCount
resolvedFindings
本次審查(opencode / gemini-2.5-flash,共 13 次呼叫)
Reviewed-on: actions/code-review#43
Reviewed-on: actions/code-review#44
本次審查(opencode / gemini-2.5-flash,共 12 次呼叫)
嚴重等級:🔴 嚴重 審查員:Maya 問題:新增了 usageSection 功能,但測試案例中未針對該區段若包含惡意程式碼(例如注入 ## 🤖 AI 助理使用量)進行安全測試,若 usageSection 來源不可控,可能導致統計版面被偽造訊息覆蓋。 建議:補充一個測試案例,傳入帶有惡意 Markdown 格式或假統計資料的 usageSection,確認最終產出的 body 結構是否如預期被正確組裝,而非被惡意內容竄改結構。
## 🤖 AI 助理使用量
嚴重等級:🟡 警告 審查員:Maya 問題:在 filterFalsePositivesWithAI 的測試中,雖然模擬了平行處理,但並未測試當多個並行裁決(Promise.all)中,部分成功、部分失敗時的結果一致性(即確保失敗者保守保留)。 建議:增加測試案例:模擬其中一個 sub-agent 拋出錯誤、另一個判為誤報、第三個判為成立,驗證最終結果是否正確地保留了「失敗者」與「成立者」,且只剔除「確認誤報者」。
filterFalsePositivesWithAI
本次審查(opencode / gemini-2.5-flash,共 14 次呼叫)
@@ -248,3 +247,4 @@
it('posts inline comments only for new findings, not old ones', async () => {
const reviewCalls = [];
const findings = [
{ level: 'info', role: 'Maya', location: 'app/c.js:30', suggestion: 'I', is_new: true },
嚴重等級:🔴 嚴重 審查員:Maya 問題:新增的 postFindingsReview 使用統計功能,但在測試中完全未驗證輸出內容。 建議:應斷言 reviewCalls[0].body 確實包含了預期的 usageSection 資訊與統計數據。
postFindingsReview
reviewCalls[0].body
嚴重等級:🔴 嚴重 審查員:Maya 問題:filterFalsePositivesWithAI 測試不足,缺乏對 judgeFindingIsFalsePositive 內部的獨立單元測試。 建議:為內部函數 judgeFindingIsFalsePositive 補寫測試,單獨驗證其對不同 Verdict 值(false_positive, confirmed, 異常值)的處理邏輯。
judgeFindingIsFalsePositive
@@ -142,6 +189,126 @@ describe('findings exclusions', () => {
assert.ok(capturedUserContent.includes('"suggestion":"update tests"'));
嚴重等級:🟡 警告 審查員:Maya 問題:未測試 resolveMissingLineNumbers 當 chatFn 回傳無效行號時的處理。 建議:補上測試案例:模擬 chatFn 回傳無效行號,確保其進入 fallback 邏輯。
resolveMissingLineNumbers
@@ -0,0 +18,4 @@
const f = parseBotReviewComment(reviewBody('🔴 嚴重', 'Assassin', '可能空指標', '加上 null 檢查'));
assert.deepEqual(f, { level: 'critical', role: 'Assassin', problem: '可能空指標', suggestion: '加上 null 檢查' });
});
嚴重等級:🟡 警告 審查員:Maya 問題:在 parseBotReviewComment 的測試中,沒有驗證解析失敗時的行為。 建議:補上邊界測試:輸入不完整的內容,驗證函數是否正確回傳 null。
@@ -0,0 +130,4 @@
const verdicts = await judgeConversations(items, chatFn);
assert.deepEqual(verdicts, [
{ idx: 0, verdict: 'open' },
{ idx: 1, verdict: 'resolved' },
嚴重等級:🔵 建議 審查員:Maya 問題:judgeConversations 的測試中,未對「AI 回傳空陣列」或「所有 Verdict 皆為空」的情境進行邊界驗證。 建議:補上邊界測試,驗證該情境下是否將所有對話歸類為 open(保守保留)。
judgeConversations
open
This reverts commit b0b8560090.
b0b8560090
本次審查(opencode / gemini-2.5-flash,共 10 次呼叫)
@@ -0,0 +143,4 @@
嚴重等級:🟡 警告 審查員:Rogue 問題:在 recordRateLimit 中頻繁呼叫 lowerCaseKeys,這會對每個請求的 headers 進行複製與轉換,增加記憶體分配開銷。 建議:建議直接存取 headers 時改用不區分大小寫的存取函式,避免複製整個物件。
lowerCaseKeys
@@ -0,0 +237,4 @@
it('summarises tokens and remaining percent on one line', () => {
const line = formatUsageStatsLine('openai', 'gpt-4o-mini', usage, { available: true, used: 1, limit: 10, remaining: 9, currency: 'USD' }, null);
assert.equal(line, '本次 openai/gpt-4o-mini: 提示100 + 回應20 = 120 token(3 次呼叫);剩餘可用: 90%(帳號額度 USD 9/USD 10)');
嚴重等級:🟡 警告 審查員:Maya 問題:formatUsageStatsLine 測試案例中,僅驗證了單一平台的格式,缺失了當 quota 或 rate 資料缺失或包含無效數字(如 NaN)時的處理測試。 建議:補充針對 quota 或 rate 傳入異常資料(如 limit: NaN)的測試,驗證 formatUsageStatsLine 是否能產生安全的預設文字,而非輸出 NaN 或破壞版面。
formatUsageStatsLine
quota
rate
NaN
limit: NaN
No dependencies set.
The note is not visible to the blocked user.
變更摘要
新增「階段十三:PR 對話收斂」功能。前置驗證通過、且非 AI 助理自動提交後(
Step2),讀取 PR 上所有行內 review comment,依「檔案路徑+行號」收斂成對話,對照 PR head 最新程式碼由 AI 判斷各對話指出的問題是否已解決:POST /repos/{repo}/pulls/comments/{id}/resolve)resolve,並於Step4從findings.json問題清單移除。Step4加回問題清單(涵蓋「問題仍在但findings.json已遺漏」的情況)。影響範圍與重點檔案
app/resolve.js(新增):對話收斂核心。提供parseBotReviewComment、groupConversations、codeWindow、judgeConversationsResolved、reconcileConversations、dropResolvedFindings、addCarriedFindings。移除/加回皆以「檔案路徑+正規化建議內容」為簽章比對,對行號漂移與標點差異穩定,避免重複。app/gitea.js:新增listPullReviews、getPullReviewComments、listAllReviewComments、resolvePullReviewComment、getFileContentAtRef(contents API base64 解碼)。app/main.js:以Step2呼叫reconcileConversations,於Step4套用dropResolvedFindings/addCarriedFindings。app/resolve.test.js(新增)、app/gitea.test.js(擴充):覆蓋解析、收斂、AI 判斷對齊、resolve/降級、移除/加回去重等情境。README.md、TODO.md:補上流程第 2.5 點與階段十三的說明與驗收紀錄。風險與注意事項
node --test *.test.js全數通過。🤖 AI Code Review 團隊
AI Code Review 統計
@@ -0,0 +37,4 @@level: level || 'warning',role: role || 'AI Review',problem: problem || '',suggestion: suggestion || problem || '',嚴重等級:🟡 警告
審查員:Maya
問題:函式
parseBotReviewComment缺少對levelRaw為空但其他欄位存在時的測試案例。程式碼有level: level || 'warning'處理,但此行為應被明確驗證。建議:請新增測試案例,模擬評論內文缺少
嚴重等級或等級欄位,但有審查員和問題/建議欄位時,確認level會正確地預設為warning。@@ -0,0 +38,4 @@role: role || 'AI Review',problem: problem || '',suggestion: suggestion || problem || '',};嚴重等級:🟡 警告
審查員:Maya
問題:函式
parseBotReviewComment缺少對problem存在但suggestion為空字串的測試案例。程式碼有suggestion: suggestion || problem || ''處理,但此行為應被明確驗證。建議:請新增測試案例,模擬評論內文只包含
問題欄位而無建議欄位時,確認suggestion會正確地使用problem的內容。@@ -0,0 +67,4 @@return [...groups.values()].map(g => ({ ...g, thread: g.bodies.join('\n---\n') }));}/** 取目標行附近的程式碼片段(含行號),讓 AI 對照判斷問題是否已解決。 */嚴重等級:🔵 建議
審查員:Mage
問題:在
groupConversations函式中,若行內 review comment 缺乏path或position/original_position資訊,它們將會被歸類到一個共同的key(例如|0)。這可能導致多個實際上不相關的、缺乏位置資訊的留言被錯誤地歸類為同一個對話群組。雖然這類留言通常不屬於「行內」評論,且parseBotReviewComment可能會將其視為非 bot 留言,但這種歸類方式可能與預期不符。建議:考慮是否應明確地過濾掉缺乏
path或有效position的留言,或為這些留言提供一個更具區分性的預設key,以避免不相關的留言被意外地歸併。例如,可以在迴圈開始時增加判斷:if (!c?.path || (!c?.position && !c?.original_position)) continue;。@@ -0,0 +87,4 @@const payload = items.map(it => ({ idx: it.idx, path: it.path, line: it.line, thread: it.thread, code: it.code }));const result = await chatFn(systemPrompt, JSON.stringify(payload));const byIdx = new Map((Array.isArray(result) ? result : [])嚴重等級:🟡 警告
審查員:Maya
問題:函式
judgeConversationsResolved缺少對chatFn拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。建議:請新增測試案例,模擬
chatFn拋出錯誤時,確認judgeConversationsResolved會正確地將錯誤向上拋出,以便呼叫者處理。@@ -0,0 +88,4 @@const result = await chatFn(systemPrompt, JSON.stringify(payload));const byIdx = new Map((Array.isArray(result) ? result : []).filter(r => Number.isInteger(r?.idx))嚴重等級:🟡 警告
審查員:Maya
問題:函式
judgeConversationsResolved缺少對 AI 回傳結果中元素缺少idx或resolved欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。建議:請新增測試案例,模擬
chatFn回傳的陣列中,有些物件缺少idx或resolved屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。@@ -0,0 +129,4 @@line(`對話收斂: 對話總數=${conversations.length} 已解決/不可處理=${alreadyResolved} 待判斷=${open.length}`);if (open.length === 0) return { ...EMPTY };const fileCache = new Map();嚴重等級:🔴 嚴重
審查員:Maya
問題:函式
reconcileConversations在取得單一檔案內容 (getFileContent) 失敗時,會中斷整個對話收斂流程。這會導致即使只有一個檔案出錯,整個 PR 的收斂都無法完成。建議:請修改
reconcileConversations,在fileCache.set(filePath, await getFileContent(filePath))的迴圈中,為getFileContent加上try-catch區塊。當單一檔案取得失敗時,應記錄警告並將該檔案的內容視為空字串,而不是中斷整個流程,以確保其他檔案的處理不受影響。@@ -0,0 +141,4 @@thread: c.thread,code: codeWindow(fileCache.get(c.path) || '', c.line),}));嚴重等級:🟡 警告
審查員:Maya
問題:函式
reconcileConversations缺少對judge拋出錯誤情境的明確測試。雖然程式碼有try-catch處理,但應有專門的測試案例來驗證此失敗路徑的行為。建議:請新增測試案例,模擬
judge函式拋出錯誤時,確認reconcileConversations能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即verdicts應全部為resolved: false)。@@ -0,0 +151,4 @@}const resolvedSet = new Set(verdicts.filter(v => v.resolved).map(v => v.idx));const resolvedFindings = [];嚴重等級:🔴 嚴重
審查員:Rogue
問題:這裡又在浪費時間!
reconcileConversations函式在取得所有獨特的檔案路徑後,又在迴圈裡對每個檔案路徑依序呼叫getFileContent。如果有很多檔案需要檢查,這會導致F次遠端 API 呼叫依序執行,嚴重拖慢整體流程。建議:改用
Promise.all或Promise.allSettled來並行發送所有getFileContent的請求。這樣可以大幅減少等待時間,讓檔案內容的取得幾乎同時完成。@@ -0,0 +170,4 @@pushCarried(carriedFindings, c);}const unresolvedCount = open.length - resolvedCount;嚴重等級:🔴 嚴重
審查員:Rogue
問題:又來了!
reconcileConversations函式在迴圈裡對每個需要解決的對話依序呼叫resolveComment。這又是一個 N+1 查詢問題,如果有很多對話需要解決,會導致N_open次遠端 API 呼叫依序執行,效率極差。建議:改用
Promise.allSettled來並行發送所有resolveComment的請求。這樣可以大幅減少等待時間,讓對話的解決幾乎同時完成,即使部分失敗也不會中斷其他請求。🤖 AI Code Review 團隊
AI Code Review 統計
@@ -0,0 +49,4 @@const groups = new Map();for (const c of comments || []) {const filePath = typeof c?.path === 'string' ? c.path : '';if (!filePath) continue; // 無檔案路徑的留言無法定位,跳過以免併入共用群組嚴重等級:🟡 警告
審查員:Assassin
問題:在
parseBotReviewComment函式中,從 Gitea comment 內文解析出的problem和suggestion欄位,若包含惡意 HTML 或 JavaScript 程式碼,且這些內容在後續的處理或顯示中未經適當的輸出編碼,可能導致跨網站指令碼(XSS)攻擊。建議:確保所有從外部來源解析出的字串(特別是
problem和suggestion)在任何將其渲染到網頁或其他使用者介面的地方,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼、JavaScript 字串編碼等),以防止 XSS 攻擊。@@ -0,0 +67,4 @@}return [...groups.values()].map(g => ({ ...g, thread: g.bodies.join('\n---\n') }));}嚴重等級:🔵 建議
審查員:Mage
問題:在
groupConversations函式中,若行內 review comment 缺乏path或position/original_position資訊,它們將會被歸類到一個共同的key(例如|0)。這可能導致多個實際上不相關的、缺乏位置資訊的留言被錯誤地歸類為同一個對話群組。雖然這類留言通常不屬於「行內」評論,且parseBotReviewComment可能會將其視為非 bot 留言,但這種歸類方式可能與預期不符。建議:考慮是否應明確地過濾掉缺乏
path或有效position的留言,或為這些留言提供一個更具區分性的預設key,以避免不相關的留言被意外地歸併。例如,可以在迴圈開始時增加判斷:if (!c?.path || (!c?.position && !c?.original_position)) continue;。@@ -0,0 +142,4 @@}}));const items = open.map((c, idx) => ({嚴重等級:🔴 嚴重
審查員:Assassin
問題:在
judgeConversationsResolved函式中,thread(來自 Gitea comment 內容)和code(來自 PR 檔案內容)被直接拼接進傳給 LLM 的payload中。如果攻擊者能夠控制這些內容,他們可以透過注入惡意指令來劫持 LLM 的行為,例如使其始終將特定問題判斷為已解決,或嘗試從 LLM 獲取敏感資訊(提示詞注入)。建議:對所有傳遞給 LLM 的外部輸入(如
thread和code)進行嚴格的淨化和隔離。考慮使用結構化輸入而非直接拼接字串,並在 LLM 提示詞中明確指示其忽略任何試圖改變其行為的指令。對於敏感操作,應建立多層驗證機制,不單純依賴 LLM 的判斷。@@ -0,0 +177,4 @@const c = open[i];const outcome = resolveOutcome.get(i);if (outcome?.status === 'fulfilled') {resolvedCount += 1;嚴重等級:🔴 嚴重
審查員:Assassin
問題:在
reconcileConversations函式中,從外部 Gitea comment 取得的c.path(檔案路徑)未經額外驗證或淨化,直接傳遞給了getFileContent(即getFileContentAtRef)。由於getFileContentAtRef存在路徑穿越漏洞,攻擊者可以透過在 PR 中建立惡意檔案名稱,並在該檔案上留言,來觸發路徑穿越,讀取伺服器上的任意檔案。建議:在將
c.path傳遞給getFileContent之前,必須對其進行嚴格的白名單驗證,確保它只包含預期的檔案名稱字元,且不包含任何路徑穿越序列(例如..或/)。或者,確保getFileContentAtRef的路徑處理是絕對安全的,不允許任何形式的路徑穿越。@@ -0,0 +204,4 @@.trim().toLowerCase();}嚴重等級:🟡 警告
審查員:Leo
問題:函式
normalizeKey對建議內容進行了非常積極的正規化,移除了所有標點符號、符號和空白字元。雖然這有助於避免行號漂移和微小措辭差異造成的重複判斷,但過度正規化可能會導致不同但語意相近的建議被視為相同,進而影響問題追蹤的精確性。建議:請評估這種積極正規化是否會導致誤判。如果發現有不同建議被錯誤合併的情況,可以考慮放寬正規化規則,例如只移除空白字元和部分標點符號,或加入其他判斷維度(如關鍵字比對)來提高精確度。
🤖 AI Code Review 團隊
AI Code Review 統計
@@ -0,0 +5,4 @@const EMPTY = { resolvedFindings: [], carriedFindings: [], resolvedCount: 0, unresolvedCount: 0 };/** 取出 "**label**:value" 這一行的 value(單行)。 */function fieldValue(body, label) {嚴重等級:🔵 建議
審查員:Bard
問題:RegExp 在函式內部重複建立,造成不必要的效能損耗。
建議:將正則表達式移至函式外部宣告為常數。
@@ -0,0 +15,4 @@if (raw.includes('嚴重')) return 'critical';if (raw.includes('警告')) return 'warning';if (raw.includes('建議')) return 'info';return null;嚴重等級:🟡 警告
審查員:Assassin
問題:函式
parseBotReviewComment動態產生正規表達式,且輸入來源body為外部輸入,存在 Regex Injection 風險。建議:將正規表達式改為靜態定義,並透過
String.raw或更安全的字串處理方式來匹配標籤,確保輸入不包含特殊 regex 字元。@@ -0,0 +49,4 @@const groups = new Map();for (const c of comments || []) {const filePath = typeof c?.path === 'string' ? c.path : '';if (!filePath) continue; // 無檔案路徑的留言無法定位,跳過以免併入共用群組嚴重等級:🟡 警告
審查員:Assassin
問題:在
parseBotReviewComment函式中,從 Gitea comment 內文解析出的problem和suggestion欄位若包含惡意內容且未經適當輸出編碼,可能導致 XSS 攻擊。建議:確保所有從外部來源解析出的字串在渲染到任何介面時,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼),以防止 XSS 攻擊。
@@ -0,0 +67,4 @@}return [...groups.values()].map(g => ({ ...g, thread: g.bodies.join('\n---\n') }));}嚴重等級:🔵 建議
審查員:Mage
問題:缺乏位置資訊的留言會被歸類到同一個預設 key,可能導致不相關留言被錯誤歸併。
建議:明確過濾缺乏
path或position的留言,或提供更具區分性的預設 key。@@ -0,0 +74,4 @@const lines = content.split('\n');const center = Number.isFinite(lineNum) && lineNum > 0 ? lineNum - 1 : 0;const start = Math.max(0, center - radius);const end = Math.min(lines.length, center + radius + 1);嚴重等級:🔴 嚴重
審查員:Mage
問題:在
judgeConversationsResolved函式中,對chatFn的結果結構缺乏足夠的嚴格檢查。若回傳結構不符合預期,可能導致所有對話被錯誤判定為「未解決」。建議:增加對
result結構的嚴格檢查。如果result不是預期的陣列結構,應拋出例外或進行更謹慎的錯誤處理,而不是默默地將所有對話視為未解決。嚴重等級:🟡 警告
審查員:Leo
問題:程式碼片段定位邏輯(如字串拼接行號)與上下文擷取策略(如 radius)寫死在函式內,擴展性與維護性不足。
建議:建立明確的
Location物件封裝定位資訊,並將radius或擷取策略抽離為配置參數或常數。嚴重等級:🟡 警告
審查員:Rogue
問題:大量使用字串拼接產生暫存物件,以及並行請求未限制數量,在高負載下可能導致 GC 壓力或觸發 API 限流。
建議:對於大量 comments,考慮使用複合物件或分層 Map 結構。引入請求並行限制(如
p-limit)來確保系統穩定性。@@ -0,0 +141,4 @@fileCache.set(filePath, '');}}));嚴重等級:🟡 警告
審查員:Maya
問題:缺少關鍵邊界條件與異常路徑的測試案例。包含
judge拋出錯誤、chatFn解析異常、levelRaw或suggestion空值、getFileContent失敗以及混合正確/錯誤的判斷數據等場景。建議:請在
app/resolve.test.js中新增這些邊界條件的測試案例,確保系統在面對 AI 異常輸出、API 失敗、或輸入欄位缺失時,仍能穩健處理並符合預期行為。@@ -0,0 +142,4 @@}}));const items = open.map((c, idx) => ({嚴重等級:🔴 嚴重
審查員:Assassin
問題:LLM 提示詞注入風險:在
judgeConversationsResolved函式中,外部來源的thread和code被直接拼接進傳給 LLM 的payload中,攻擊者可能注入惡意指令來劫持 LLM 行為。建議:對所有傳遞給 LLM 的外部輸入進行嚴格的淨化和隔離。使用結構化輸入而非直接拼接字串,並在提示詞中明確指示 AI 忽略任何試圖下達指令的內容,僅對邏輯進行判斷。對於敏感操作,應建立多層驗證機制。
@@ -0,0 +170,4 @@resolveTargets.map(({ c }) => resolveComment(c.commentIds[0])),);const resolveOutcome = new Map();resolveTargets.forEach(({ i }, j) => resolveOutcome.set(i, settled[j]));嚴重等級:🔵 建議
審查員:Rogue
問題:
Promise.allSettled的結果處理邏輯過於冗長,產生不必要的中間變數。建議:優化處理邏輯,直接在迴圈內處理或使用更緊湊的寫法。
@@ -0,0 +177,4 @@const c = open[i];const outcome = resolveOutcome.get(i);if (outcome?.status === 'fulfilled') {resolvedCount += 1;嚴重等級:🔴 嚴重
審查員:Assassin
問題:在
reconcileConversations函式中,從外部 Gitea comment 取得的c.path(檔案路徑)未經額外驗證或淨化,直接傳遞給了getFileContent(即getFileContentAtRef)。由於getFileContentAtRef存在路徑穿越漏洞,攻擊者可以透過在 PR 中建立惡意檔案名稱,並在該檔案上留言,來觸發路徑穿越,讀取伺服器上的任意檔案。建議:在將
c.path傳遞給getFileContent之前,必須對其進行嚴格的白名單驗證,確保它只包含預期的檔案名稱字元,且不包含任何路徑穿越序列(例如..或/)。或者,確保getFileContentAtRef的路徑處理是絕對安全的,不允許任何形式的路徑穿越。@@ -0,0 +184,4 @@}if (outcome?.status === 'rejected') {warn(`resolve 對話失敗(保留為未解決): ${c.path}:${c.line} error=${outcome.reason?.message}`);}嚴重等級:🟡 警告
審查員:Mage
問題:在
reconcileConversations函式中,並行(Promise.all)呼叫resolveComment,即使個別呼叫失敗,也僅在settled中記錄為rejected並印出warn。然而,若resolveComment失敗是因為Authorizationtoken 過期或權限不足,後續所有的resolve呼叫都會失敗,此時程式碼沒有對這些特定的錯誤進行分類處理。建議:應判斷
outcome.reason的錯誤類型。若是連線/權限相關的嚴重錯誤,應立即停止後續的resolve嘗試,避免在已知無法成功的情況下發出無效請求。@@ -0,0 +192,4 @@ok(`對話收斂完成: resolved=${resolvedCount} unresolved=${unresolvedCount} 加回 findings=${carriedFindings.length}`);return { resolvedFindings, carriedFindings, resolvedCount, unresolvedCount };}嚴重等級:🔵 建議
審查員:Mage
問題:對
botFinding的存取缺乏防禦性檢查。建議:在
push之前增加防禦性檢查,確保物件完整性。@@ -0,0 +204,4 @@.trim().toLowerCase();}嚴重等級:🟡 警告
審查員:Leo
問題:正規化邏輯(
normalizeKey等)過於激進且未快取,既可能導致語意相近建議被誤判為相同,也在頻繁比較時造成效能浪費。建議:請評估目前的正規化規則,若發現誤判,放寬規則或加入關鍵字比對。將簽章產生邏輯抽離為獨立 Helper 函式,並在產生時進行快取(Memoize)以提升效能。
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 9 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +37,4 @@export function parseBotReviewComment(body) {if (typeof body !== 'string' || !body.includes('**')) return null;const normalized = body.replace(/\r\n/g, '\n');const levelRaw = fieldValue(normalized, '嚴重等級') || fieldValue(normalized, '等級');嚴重等級:🟡 警告
審查員:Maya
問題:函式
parseBotReviewComment缺少對levelRaw為空但其他欄位存在時的測試案例。程式碼有level: level || 'warning'處理,但此行為應被明確驗證。建議:請新增測試案例,模擬評論內文缺少
嚴重等級或等級欄位,但有審查員和問題/建議欄位時,確認level會正確地預設為warning。@@ -0,0 +38,4 @@if (typeof body !== 'string' || !body.includes('**')) return null;const normalized = body.replace(/\r\n/g, '\n');const levelRaw = fieldValue(normalized, '嚴重等級') || fieldValue(normalized, '等級');const role = fieldValue(normalized, '審查員');嚴重等級:🟡 警告
審查員:Maya
問題:函式
parseBotReviewComment缺少對problem存在但suggestion為空字串的測試案例。程式碼有suggestion: suggestion || problem || ''處理,但此行為應被明確驗證。建議:請新增測試案例,模擬評論內文只包含
問題欄位而無建議欄位時,確認suggestion會正確地使用problem的內容。@@ -0,0 +87,4 @@if (!content) return '';const lines = content.split('\n');const center = Number.isFinite(lineNum) && lineNum > 0 ? lineNum - 1 : 0;const start = Math.max(0, center - radius);嚴重等級:🟡 警告
審查員:Maya
問題:函式
judgeConversationsResolved缺少對chatFn拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。建議:請新增測試案例,模擬
chatFn拋出錯誤時,確認judgeConversationsResolved會正確地將錯誤向上拋出,以便呼叫者處理。@@ -0,0 +88,4 @@const lines = content.split('\n');const center = Number.isFinite(lineNum) && lineNum > 0 ? lineNum - 1 : 0;const start = Math.max(0, center - radius);const end = Math.min(lines.length, center + radius + 1);嚴重等級:🟡 警告
審查員:Maya
問題:函式
judgeConversationsResolved缺少對 AI 回傳結果中元素缺少idx或resolved欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。建議:請新增測試案例,模擬
chatFn回傳的陣列中,有些物件缺少idx或resolved屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。@@ -0,0 +141,4 @@* 4. 未解決且可解析為 bot finding 者,收集為「加回問題列表」清單。* 任一外部呼叫失敗都降級處理(保守視為未解決),不中斷整體 pipeline。*/export async function reconcileConversations(deps = {}) {嚴重等級:🟡 警告
審查員:Maya
問題:函式
reconcileConversations缺少對judge拋出錯誤情境的明確測試。雖然程式碼有try-catch處理,但應有專門的測試案例來驗證此失敗路徑的行為。建議:請新增測試案例,模擬
judge函式拋出錯誤時,確認reconcileConversations能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即verdicts應全部為resolved: false)。@@ -0,0 +12,4 @@/*** 把各平台回應中的 token usage 正規化成 { promptTokens, completionTokens, totalTokens }。* 支援:OpenAI 相容 usage、OpenAI Responses(input/output_tokens)、* Gemini usageMetadata、Ollama 原生 eval_count、OpenCode tokens。嚴重等級:🟡 警告
審查員:Mage
問題:extractUsage 中對於 OpenAI 相容格式的處理:
const total = num(u.total_tokens) || prompt + completion;。如果 API 回傳了total_tokens: 0(雖然極少見但非零可能),這裡的邏輯會觸發prompt + completion的計算,導致數值不準確。建議:應明確判斷
u.total_tokens != null而非僅檢查其 truthiness,以確保在 API 明確回傳 0 時能正確讀取。@@ -0,0 +163,4 @@*/export async function fetchAccountQuota(provider, config = {}, deps = {}) {const get = deps.get || axios.get;const strategy = QUOTA_STRATEGIES[provider];嚴重等級:🟡 警告
審查員:Assassin
問題:在
fetchAccountQuota中,使用axios.get直接請求傳入的baseURL。如果baseURL是由設定檔動態讀取,攻擊者可能會透過修改設定檔將其導向惡意伺服器(SSRF),進而竊取 API Key 或發送偽造請求。建議:應對
baseURL進行嚴格的白名單校驗,確保其僅能連線至合法的 API 提供商域名。不要信任外部設定檔中的 URL。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 9 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +74,4 @@if (!g.botFinding) {const finding = parseBotReviewComment(body);if (finding) g.botFinding = { ...finding, location: lineNum ? `${filePath}:${lineNum}` : filePath };}嚴重等級:🟡 警告
審查員:Maya
問題:在
codeWindow函數中,缺乏對輸入的邊界檢查,特別是當lineNum為 0 或負數,或是大於總行數時,可能導致行為不預期或 slice 產生錯誤。建議:建議在計算
start和end時,增加明確的邊界檢核與處理,確保即使lineNum異常時也能安全返回或處理。@@ -0,0 +14,4 @@* 支援:OpenAI 相容 usage、OpenAI Responses(input/output_tokens)、* Gemini usageMetadata、Ollama 原生 eval_count、OpenCode tokens。* 回應中沒有任何可辨識的 usage 時回傳 null。*/嚴重等級:🔴 嚴重
審查員:Mage
問題:在
extractUsage中,對於data.usage的屬性存取直接使用num(...),這在data.usage如果是null或其他 falsy 值但被typeof判斷通過時(JS 的typeof null === 'object'),會導致錯誤。建議:應明確檢查
u是否為嚴格的object且非null,例如if (u && typeof u === 'object' && !Array.isArray(u))。@@ -0,0 +103,4 @@}if (remaining == null || limit == null) return;rateLimit.hasData = true;嚴重等級:🔴 嚴重
審查員:Assassin
問題:函數
isOpenRouterBaseURL僅使用new URL(baseURL).hostname.endsWith('.openrouter.ai')來判斷,這極易受到偽造域名攻擊(如openrouter.ai.malicious.com),導致惡意主機被信任為 OpenRouter,進而洩漏 API Key。建議:應修改為嚴格比對,例如
hostname === 'openrouter.ai',且必須包含 protocol 檢查(如https),並建議採用白名單機制而非簡單的endsWith。@@ -0,0 +112,4 @@/** 取得最近一次的速率配額快照(複本)。 */export function getRateLimit() {return { ...rateLimit };}嚴重等級:🟡 警告
審查員:Mage
問題:在
recordRateLimit中,處理 Header 時將所有 Key 轉為小寫並存入物件h,如果原始 Header 中存在多個相同名稱但不同大小寫的 Header(雖然 HTTP 標準規定 Key 不區分大小寫,但某些實作可能會有不一致),可能會造成覆蓋。建議:雖然 HTTP 規範不區分,但為了安全起見,應先確認環境使用的 axios 版本對 Header 的處理方式,或確保在轉換前沒有遺漏必要資訊。
@@ -0,0 +129,4 @@* (如 `openrouter.ai.evil.com` 或 `evil.com/openrouter.ai`)矇騙而把 API key 送往惡意主機。*/function isOpenRouterBaseURL(baseURL) {try {嚴重等級:🔴 嚴重
審查員:Assassin
問題:在
QUOTA_STRATEGIES中,如果config.apiKeys是一個陣列,代碼只取[0]作為 API Key,但如果這個 key 是洩漏的或是環境配置錯誤,可能會導致敏感資訊在未經嚴格驗證的情況下被發送到baseURL指定的端點。建議:請務必確保所有的 API 請求都經過完整的信任邊界審核,不要僅憑環境變數就自動信任該 Key 具備查詢帳號額度的權限,並在傳輸前對 baseURL 進行嚴格的白名單檢查。
@@ -0,0 +163,4 @@if (isOpenRouterBaseURL(cfg.baseURL)) return fetchOpenRouterQuota(cfg, get);return { available: false, reason: 'OpenAI 帳號額度需 dashboard session 權限,API key 無法取得' };},claude: async () => ({ available: false, reason: 'Anthropic 額度需 Admin API 權限,一般 API key 無法取得' }),嚴重等級:🟡 警告
審查員:Assassin
問題:在
fetchAccountQuota中,使用axios.get直接請求傳入的baseURL。如果baseURL是由設定檔動態讀取,攻擊者可能會透過修改設定檔將其導向惡意伺服器(SSRF),進而竊取 API Key 或發送偽造請求。建議:應對
baseURL進行嚴格的白名單校驗,確保其僅能連線至合法的 API 提供商域名。不要信任外部設定檔中的 URL。@@ -0,0 +173,4 @@/*** 取得指定平台的帳號額度。任何失敗都降級為 { available: false, reason },不丟例外。* deps.get 可注入以利測試(預設 axios.get)。*/嚴重等級:🔴 嚴重
審查員:Mage
問題:在
fetchAccountQuota中,呼叫strategy時傳入的config物件,如果在特定strategy中被意外修改,會影響到全域的 config 狀態,且傳入的get函數來源若未被嚴格隔離,可能存在潛在的請求偽造風險。建議:傳入
strategy的 config 應進行淺拷貝(shallow copy),確保不可變性。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 9 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +37,4 @@export function parseBotReviewComment(body) {if (typeof body !== 'string' || !body.includes('**')) return null;const normalized = body.replace(/\r\n/g, '\n');const levelRaw = fieldValue(normalized, '嚴重等級') || fieldValue(normalized, '等級');嚴重等級:🟡 警告
審查員:Maya
問題:函式
parseBotReviewComment缺少對levelRaw為空但其他欄位存在時的測試案例。程式碼有level: level || 'warning'處理,但此行為應被明確驗證。建議:請新增測試案例,模擬評論內文缺少
嚴重等級或等級欄位,但有審查員和問題/建議欄位時,確認level會正確地預設為warning。@@ -0,0 +38,4 @@if (typeof body !== 'string' || !body.includes('**')) return null;const normalized = body.replace(/\r\n/g, '\n');const levelRaw = fieldValue(normalized, '嚴重等級') || fieldValue(normalized, '等級');const role = fieldValue(normalized, '審查員');嚴重等級:🟡 警告
審查員:Maya
問題:函式
parseBotReviewComment缺少對problem存在但suggestion為空字串的測試案例。程式碼有suggestion: suggestion || problem || ''處理,但此行為應被明確驗證。建議:請新增測試案例,模擬評論內文只包含
問題欄位而無建議欄位時,確認suggestion會正確地使用problem的內容。@@ -0,0 +86,4 @@export function codeWindow(content, lineNum, radius = CODE_WINDOW_RADIUS) {if (!content) return '';const lines = content.split('\n');const center = Number.isFinite(lineNum) && lineNum > 0 ? lineNum - 1 : 0;嚴重等級:🟡 警告
審查員:Maya
問題:在
judgeConversationsResolved函數中,AI 判斷回傳結構如果不符合預期(非陣列),雖有降級處理,但未驗證當 AI 回傳包含無效idx或缺少resolved欄位的物件時,對應邏輯是否正確過濾。建議:補測試案例,模擬 AI 回傳包含無效結構(如
idx為字串、缺少resolved)的 JSON,確保系統能正確忽略無效項並將其視為未解決。@@ -0,0 +87,4 @@if (!content) return '';const lines = content.split('\n');const center = Number.isFinite(lineNum) && lineNum > 0 ? lineNum - 1 : 0;const start = Math.max(0, center - radius);嚴重等級:🟡 警告
審查員:Maya
問題:函式
judgeConversationsResolved缺少對chatFn拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。建議:請新增測試案例,模擬
chatFn拋出錯誤時,確認judgeConversationsResolved會正確地將錯誤向上拋出,以便呼叫者處理。@@ -0,0 +88,4 @@const lines = content.split('\n');const center = Number.isFinite(lineNum) && lineNum > 0 ? lineNum - 1 : 0;const start = Math.max(0, center - radius);const end = Math.min(lines.length, center + radius + 1);嚴重等級:🟡 警告
審查員:Maya
問題:函式
judgeConversationsResolved缺少對 AI 回傳結果中元素缺少idx或resolved欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。建議:請新增測試案例,模擬
chatFn回傳的陣列中,有些物件缺少idx或resolved屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。@@ -0,0 +139,4 @@* 2. 取每個對話所在檔案的最新內容,請 AI 判斷問題是否已解決;* 3. 已解決者呼叫 Gitea resolve API 解決對話,並記錄其 finding(供移除舊問題);* 4. 未解決且可解析為 bot finding 者,收集為「加回問題列表」清單。* 任一外部呼叫失敗都降級處理(保守視為未解決),不中斷整體 pipeline。嚴重等級:🔴 嚴重
審查員:Maya
問題:
reconcileConversations核心流程中,對於getFileContent失敗或內容為空的處理邏輯,直接降級為空字串並視為未解決,但若檔案內容實際上非空且未解決,這可能導致判斷偏差。建議:補測試案例,模擬
getFileContent拋出錯誤時,reconcileConversations是否正確地將對話保留為未解決,且後續統計數字(carriedFindings)是否正確。@@ -0,0 +141,4 @@* 4. 未解決且可解析為 bot finding 者,收集為「加回問題列表」清單。* 任一外部呼叫失敗都降級處理(保守視為未解決),不中斷整體 pipeline。*/export async function reconcileConversations(deps = {}) {嚴重等級:🟡 警告
審查員:Maya
問題:函式
reconcileConversations缺少對judge拋出錯誤情境的明確測試。雖然程式碼有try-catch處理,但應有專門的測試案例來驗證此失敗路徑的行為。建議:請新增測試案例,模擬
judge函式拋出錯誤時,確認reconcileConversations能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即verdicts應全部為resolved: false)。@@ -0,0 +170,4 @@opencode: async () => ({ available: false, reason: '自架服務,無帳號額度概念' }),};/**嚴重等級:🟡 警告
審查員:Maya
問題:
fetchAccountQuota策略在處理 API key 時,假設apiKeys陣列存在並取第一個,若傳入的config.apiKeys為空陣列或 undefined,缺乏明確的防禦與測試。建議:補測試案例,模擬
config.apiKeys為空或無效的情境,確認系統降級行為是否符合預期。@@ -0,0 +208,4 @@/*** 計算「剩餘可用百分比」,依優先序擇一:* 1. 帳號額度(quota 有上限)→ 剩餘 credits / 上限;* 2. 速率配額(rate limit header)→ 當前視窗剩餘 / 上限;嚴重等級:🟡 警告
審查員:Maya
問題:
resolveRemainingPercent函數負責處理額度計算,但針對quota.limit為 0 的情況缺乏顯式處理,可能會導致除以零或錯誤的百分比計算結果。建議:補測試案例,模擬
quota.limit為 0 的情境,確認系統是否正確處理或返回錯誤訊息,避免計算偏差。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 9 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +4,4 @@const EMPTY = { resolvedFindings: [], carriedFindings: [], resolvedCount: 0, unresolvedCount: 0 };// 預先編譯各欄位標籤的擷取正則(靜態定義:避免每次呼叫重建,也排除以外部輸入動態組 regex 的風險)嚴重等級:🔵 建議
審查員:Bard
問題:
EMPTY常數命名過於通用,容易與其他模組中的同名變數衝突,且定義在模組頂層略顯突兀。建議:建議加上命名空間前綴,例如
RECONCILE_DEFAULT_STATE,以增加語義清晰度。@@ -0,0 +7,4 @@// 預先編譯各欄位標籤的擷取正則(靜態定義:避免每次呼叫重建,也排除以外部輸入動態組 regex 的風險)const FIELD_PATTERNS = {嚴重等級: /\*\*嚴重等級\*\*[::]\s*(.+)/,等級: /\*\*等級\*\*[::]\s*(.+)/,嚴重等級:🔵 建議
審查員:Bard
問題:
FIELD_PATTERNS的正則表達式對於冒號的定義同時包含了全形與半形,雖然容錯性高,但建議統一規範以維持風格一致性。建議:建議統一使用半形冒號,並在解析前進行正規化處理,而非在正則中處理所有可能性。
@@ -0,0 +120,4 @@rateLimit.remaining = null;rateLimit.limit = null;rateLimit.kind = null;}嚴重等級:🟡 警告
審查員:Bard
問題:
recordRateLimit函式中對於 headers 的處理,將所有 key 轉換為小寫後檢查,這雖然兼容了多種平台規範,但處理邏輯稍顯冗長,降低了程式碼的流暢度。建議:建議提取一個專門處理 header 正規化的工具函式,使主邏輯更簡潔。
@@ -0,0 +164,4 @@return { available: false, reason: 'OpenAI 帳號額度需 dashboard session 權限,API key 無法取得' };},claude: async () => ({ available: false, reason: 'Anthropic 額度需 Admin API 權限,一般 API key 無法取得' }),gemini: async () => ({ available: false, reason: 'Gemini 額度由 Google Cloud quota 管理,API key 無法直接查詢' }),嚴重等級:🔵 建議
審查員:Bard
問題:
fetchAccountQuota中的QUOTA_STRATEGIES物件定義龐大,將所有平台的策略硬編碼在此處,未來若新增更多 LLM 供應商,此處將變得難以維護。建議:建議將各供應商的額度查詢策略抽離至獨立的檔案或策略模式處理,以保持
usage.js的整潔。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 9 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 19 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -271,3 +273,3 @@REVIEW_SEVERITY_LABELS,['🟡 警告', '🔵 建議'],);assert.deepEqual(嚴重等級:🟡 警告
審查員:Maya
問題:新增了
usageSection功能,但測試案例中沒有驗證當usageSection為空字串或未傳入時,輸出的 body 是否正確排版(例如不會多出不必要的換行符號)。建議:補充測試案例,驗證當
usageSection為空時,輸出的 Markdown 結構是否如預期(沒有多餘的 `` 結尾)。
@@ -0,0 +243,4 @@falsePositiveCount += 1;if (c.botFinding) excludedFindings.push(toExclusion(c.botFinding));} else {openCount += 1;嚴重等級:🟡 警告
審查員:Maya
問題:
reconcileConversations中的reconcile流程包含多個步驟(取得 comments、group、判斷、resolve),一旦中間有外部呼叫失敗就降級。目前的測試案例主要覆蓋了「全部成功」或「特定某個失敗」,但缺乏對「部分 resolve 成功,部分 resolve 失敗」這種狀態的驗證。建議:補充測試案例,模擬部分
resolveComment成功、部分失敗的情境,驗證最終回傳的closedCount與resolvedFindings等統計數據是否正確計算。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 13 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 13 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 12 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -271,3 +273,3 @@REVIEW_SEVERITY_LABELS,['🟡 警告', '🔵 建議'],);assert.deepEqual(嚴重等級:🔴 嚴重
審查員:Maya
問題:新增了
usageSection功能,但測試案例中未針對該區段若包含惡意程式碼(例如注入## 🤖 AI 助理使用量)進行安全測試,若usageSection來源不可控,可能導致統計版面被偽造訊息覆蓋。建議:補充一個測試案例,傳入帶有惡意 Markdown 格式或假統計資料的
usageSection,確認最終產出的body結構是否如預期被正確組裝,而非被惡意內容竄改結構。嚴重等級:🟡 警告
審查員:Maya
問題:在
filterFalsePositivesWithAI的測試中,雖然模擬了平行處理,但並未測試當多個並行裁決(Promise.all)中,部分成功、部分失敗時的結果一致性(即確保失敗者保守保留)。建議:增加測試案例:模擬其中一個 sub-agent 拋出錯誤、另一個判為誤報、第三個判為成立,驗證最終結果是否正確地保留了「失敗者」與「成立者」,且只剔除「確認誤報者」。
🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 14 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -248,3 +247,4 @@it('posts inline comments only for new findings, not old ones', async () => {const reviewCalls = [];const findings = [{ level: 'info', role: 'Maya', location: 'app/c.js:30', suggestion: 'I', is_new: true },嚴重等級:🔴 嚴重
審查員:Maya
問題:新增的
postFindingsReview使用統計功能,但在測試中完全未驗證輸出內容。建議:應斷言
reviewCalls[0].body確實包含了預期的usageSection資訊與統計數據。嚴重等級:🔴 嚴重
審查員:Maya
問題:
filterFalsePositivesWithAI測試不足,缺乏對judgeFindingIsFalsePositive內部的獨立單元測試。建議:為內部函數
judgeFindingIsFalsePositive補寫測試,單獨驗證其對不同 Verdict 值(false_positive, confirmed, 異常值)的處理邏輯。@@ -142,6 +189,126 @@ describe('findings exclusions', () => {assert.ok(capturedUserContent.includes('"suggestion":"update tests"'));嚴重等級:🟡 警告
審查員:Maya
問題:未測試
resolveMissingLineNumbers當chatFn回傳無效行號時的處理。建議:補上測試案例:模擬
chatFn回傳無效行號,確保其進入 fallback 邏輯。@@ -0,0 +18,4 @@const f = parseBotReviewComment(reviewBody('🔴 嚴重', 'Assassin', '可能空指標', '加上 null 檢查'));assert.deepEqual(f, { level: 'critical', role: 'Assassin', problem: '可能空指標', suggestion: '加上 null 檢查' });});嚴重等級:🟡 警告
審查員:Maya
問題:在
parseBotReviewComment的測試中,沒有驗證解析失敗時的行為。建議:補上邊界測試:輸入不完整的內容,驗證函數是否正確回傳
null。@@ -0,0 +130,4 @@const verdicts = await judgeConversations(items, chatFn);assert.deepEqual(verdicts, [{ idx: 0, verdict: 'open' },{ idx: 1, verdict: 'resolved' },嚴重等級:🔵 建議
審查員:Maya
問題:
judgeConversations的測試中,未對「AI 回傳空陣列」或「所有 Verdict 皆為空」的情境進行邊界驗證。建議:補上邊界測試,驗證該情境下是否將所有對話歸類為
open(保守保留)。🤖 AI Code Review 團隊
AI Code Review 統計
🤖 AI 助理使用量
本次審查(opencode / gemini-2.5-flash,共 10 次呼叫)
剩餘可用
剩餘可用:無法計算百分比(自架服務,無帳號額度概念)
@@ -0,0 +143,4 @@}}/**嚴重等級:🟡 警告
審查員:Rogue
問題:在
recordRateLimit中頻繁呼叫lowerCaseKeys,這會對每個請求的 headers 進行複製與轉換,增加記憶體分配開銷。建議:建議直接存取 headers 時改用不區分大小寫的存取函式,避免複製整個物件。
@@ -0,0 +237,4 @@it('summarises tokens and remaining percent on one line', () => {const line = formatUsageStatsLine('openai', 'gpt-4o-mini', usage, { available: true, used: 1, limit: 10, remaining: 9, currency: 'USD' }, null);assert.equal(line, '本次 openai/gpt-4o-mini: 提示100 + 回應20 = 120 token(3 次呼叫);剩餘可用: 90%(帳號額度 USD 9/USD 10)');嚴重等級:🟡 警告
審查員:Maya
問題:
formatUsageStatsLine測試案例中,僅驗證了單一平台的格式,缺失了當quota或rate資料缺失或包含無效數字(如NaN)時的處理測試。建議:補充針對
quota或rate傳入異常資料(如limit: NaN)的測試,驗證formatUsageStatsLine是否能產生安全的預設文字,而非輸出NaN或破壞版面。