feat(ai-review 對話收斂): 讀 PR review 留言判斷解決狀態並收斂 findings #42

Merged
jiantw83 merged 68 commits from ai-review-resolve/20260623-110950 into develop 2026-06-23 08:29:38 +00:00
3 changed files with 307 additions and 1 deletions
Showing only changes of commit a99163468b - Show all commits
+73
View File
@@ -153,3 +153,76 @@ export async function postPullReview({ body, comments = [] }) {
); );
return resp.data; return resp.data;
} }
/**
* 取得 PR 上所有的 review(每個 review 可含多個行內 comment)。
*/
export async function listPullReviews() {
const resp = await axios.get(
api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}/reviews`),
{ headers: headers(), timeout: 30000, httpsAgent },
);
return Array.isArray(resp.data) ? resp.data : [];
}
/**
* 取得單一 review 底下的所有行內 comment。
*/
export async function getPullReviewComments(reviewId) {
const resp = await axios.get(
api(`/repos/${GITEA_REPOSITORY}/pulls/${PR_NUMBER}/reviews/${reviewId}/comments`),
{ headers: headers(), timeout: 30000, httpsAgent },
);
return Array.isArray(resp.data) ? resp.data : [];
}
/**
* 取得 PR 上所有 review 的行內 comment,展平成單一陣列。
* 單一 review 取 comment 失敗時記錄警告並略過,不中斷整體流程。
*/
export async function listAllReviewComments() {
const reviews = await listPullReviews();
const all = [];
for (const review of reviews) {
if (!review?.id) continue;
try {
all.push(...await getPullReviewComments(review.id));
} catch (e) {
warn(`取得 review #${review.id} 的 comments 失敗(略過): ${e.message}`);
}
}
line(`取得 PR review comments: reviews=${reviews.length} comments=${all.length}`);
return all;
}
/**
* 解決(resolve)一個 review comment 所屬的對話。
* 對應 Gitea 官方 APIPOST /repos/{repo}/pulls/comments/{id}/resolve。
*/
export async function resolvePullReviewComment(commentId) {
const resp = await axios.post(
api(`/repos/${GITEA_REPOSITORY}/pulls/comments/${commentId}/resolve`),
{},
{ headers: headers(GITEA_COMMENT_TOKEN || GITEA_TOKEN), timeout: 30000, httpsAgent },
);
return resp.data;
}
/**
* 取得指定 ref(預設 PR head)下某檔案的最新文字內容;
* Gitea contents API 回傳 base64,這裡解碼成字串。檔案不存在或非文字時回傳空字串。
*/
export async function getFileContentAtRef(filePath, ref = PR_HEAD_SHA || PR_HEAD_BRANCH) {
try {
const resp = await axios.get(
api(`/repos/${GITEA_REPOSITORY}/contents/${encodeURIComponent(filePath).replace(/%2F/g, '/')}`),
{ headers: headers(), params: ref ? { ref } : undefined, timeout: 30000, httpsAgent },
);
const { content, encoding } = resp.data || {};
if (typeof content !== 'string') return '';
return encoding === 'base64' ? Buffer.from(content, 'base64').toString('utf8') : content;
} catch (e) {
warn(`取得檔案內容失敗(視為空): ${filePath}@${ref || 'head'} error=${e.message}`);
return '';
}
}
+16 -1
View File
@@ -3,6 +3,7 @@ import { GITEA_REPOSITORY, PR_NUMBER, PR_HEAD_BRANCH, PR_BASE_BRANCH, getLLMConf
import { loadRoles, getRoleIntro } from './roles.js'; import { loadRoles, getRoleIntro } from './roles.js';
import { getPRDiff, postComment, getCommitMessageBySha, getBotReviewOutcome, shouldSkipBotCommit } from './gitea.js'; import { getPRDiff, postComment, getCommitMessageBySha, getBotReviewOutcome, shouldSkipBotCommit } from './gitea.js';
import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js'; import { analyzeWithRole, loadOldFindings, mergeFindings, sortByLevel, deduplicateWithAI, loadExclusions, applyExclusions, filterFalsePositivesWithAI } from './findings.js';
import { reconcileConversations, dropResolvedFindings, addCarriedFindings } from './resolve.js';
import { saveFindings, postFindingsReview, formatFindingsStatsLine } from './comments.js'; import { saveFindings, postFindingsReview, formatFindingsStatsLine } from './comments.js';
import { cloneRepo, commitAndPush, getRepoState } from './git.js'; import { cloneRepo, commitAndPush, getRepoState } from './git.js';
import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js'; import { validateJSONArrayFile, ensureJSONArrayFileExists } from './json.js';
@@ -43,6 +44,15 @@ async function main() {
process.exit(0); process.exit(0);
} }
step('Step2', 'PR 對話收斂');
let reconcile = { resolvedFindings: [], carriedFindings: [], resolvedCount: 0, unresolvedCount: 0 };
try {
reconcile = await reconcileConversations();
ok(`Step2 完成: resolved=${reconcile.resolvedCount} unresolved=${reconcile.unresolvedCount} 加回=${reconcile.carriedFindings.length}`);
} catch (e) {
warn(`Step2 對話收斂失敗(繼續執行): ${e.message}`);
}
const { provider, baseURL, model } = getLLMConfig(); const { provider, baseURL, model } = getLLMConfig();
if (!provider) { if (!provider) {
error('未設定任何 LLM API Key,請檢查 action inputs'); error('未設定任何 LLM API Key,請檢查 action inputs');
@@ -99,8 +109,13 @@ async function main() {
if (repoState) { if (repoState) {
line(`repo 狀態: branch=${repoState.branch || 'detached'} commit=${repoState.shortSha || 'unknown'} commit_time=${repoState.commitTime || 'unknown'} path=${repoState.repoDir}`); line(`repo 狀態: branch=${repoState.branch || 'detached'} commit=${repoState.shortSha || 'unknown'} commit_time=${repoState.commitTime || 'unknown'} path=${repoState.repoDir}`);
} }
const oldFindings = loadOldFindings(repoDir || WORKSPACE); let oldFindings = loadOldFindings(repoDir || WORKSPACE);
logFindingsStats('Step4 舊 findings 統計', oldFindings); logFindingsStats('Step4 舊 findings 統計', oldFindings);
const beforeReconcile = oldFindings.length;
oldFindings = dropResolvedFindings(oldFindings, reconcile.resolvedFindings);
oldFindings = addCarriedFindings(oldFindings, reconcile.carriedFindings);
line(`Step4 對話收斂套用: ${beforeReconcile} -> ${oldFindings.length} 筆(移除已解決 ${reconcile.resolvedFindings.length}、加回未解決 ${reconcile.carriedFindings.length}`);
logFindingsStats('Step4 收斂後舊 findings 統計', oldFindings);
logFindingsStats('Step4 新 findings 統計', newFindings); logFindingsStats('Step4 新 findings 統計', newFindings);
const mergedFindings = mergeFindings(oldFindings, newFindings); const mergedFindings = mergeFindings(oldFindings, newFindings);
ok(`Step4 merged findings total=${mergedFindings.length}`); ok(`Step4 merged findings total=${mergedFindings.length}`);
+218
View File
@@ -0,0 +1,218 @@
import { chatJSON } from './llm.js';
import { listAllReviewComments, resolvePullReviewComment, getFileContentAtRef } from './gitea.js';
import { line, ok, warn } from './log.js';
const EMPTY = { resolvedFindings: [], carriedFindings: [], resolvedCount: 0, unresolvedCount: 0 };
/** 取出 "**label**value" 這一行的 value(單行)。 */
admin marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Bard
問題EMPTY 常數命名過於通用,容易與其他模組中的同名變數衝突,且定義在模組頂層略顯突兀。
建議:建議加上命名空間前綴,例如 RECONCILE_DEFAULT_STATE,以增加語義清晰度。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:`EMPTY` 常數命名過於通用,容易與其他模組中的同名變數衝突,且定義在模組頂層略顯突兀。 **建議**:建議加上命名空間前綴,例如 `RECONCILE_DEFAULT_STATE`,以增加語義清晰度。
function fieldValue(body, label) {
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔵 建議
審查員:Bard
問題:RegExp 在函式內部重複建立,造成不必要的效能損耗。
建議:將正則表達式移至函式外部宣告為常數。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:RegExp 在函式內部重複建立,造成不必要的效能損耗。 **建議**:將正則表達式移至函式外部宣告為常數。
const m = body.match(new RegExp(`\\*\\*${label}\\*\\*[:]\\s*(.+)`));
return m ? m[1].trim() : '';
admin marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Bard
問題FIELD_PATTERNS 的正則表達式對於冒號的定義同時包含了全形與半形,雖然容錯性高,但建議統一規範以維持風格一致性。
建議:建議統一使用半形冒號,並在解析前進行正規化處理,而非在正則中處理所有可能性。

**嚴重等級**:🔵 建議 **審查員**:Bard **問題**:`FIELD_PATTERNS` 的正則表達式對於冒號的定義同時包含了全形與半形,雖然容錯性高,但建議統一規範以維持風格一致性。 **建議**:建議統一使用半形冒號,並在解析前進行正規化處理,而非在正則中處理所有可能性。
}
function levelToKey(raw) {
if (!raw) return null;
if (raw.includes('嚴重')) return 'critical';
if (raw.includes('警告')) return 'warning';
if (raw.includes('建議')) return 'info';
return null;
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Assassin
問題:函式 parseBotReviewComment 動態產生正規表達式,且輸入來源 body 為外部輸入,存在 Regex Injection 風險。
建議:將正規表達式改為靜態定義,並透過 String.raw 或更安全的字串處理方式來匹配標籤,確保輸入不包含特殊 regex 字元。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:函式 `parseBotReviewComment` 動態產生正規表達式,且輸入來源 `body` 為外部輸入,存在 Regex Injection 風險。 **建議**:將正規表達式改為靜態定義,並透過 `String.raw` 或更安全的字串處理方式來匹配標籤,確保輸入不包含特殊 regex 字元。
}
/**
* 嘗試把一則 review comment 內文解析回 bot 產生的 finding 欄位。
* 同時支援 review comment(嚴重等級/審查員/問題/建議)與行內 critical comment(等級/審查員/建議)格式。
* 不符合格式(例如人工自由留言)時回傳 null。
*/
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, '審查員');
const problem = fieldValue(normalized, '問題');
const suggestion = fieldValue(normalized, '建議');
const level = levelToKey(levelRaw);
if (!level && !role) return null;
if (!suggestion && !problem) return null;
return {
level: level || 'warning',
role: role || 'AI Review',
problem: problem || '',
suggestion: suggestion || problem || '',
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 parseBotReviewComment 缺少對 levelRaw 為空但其他欄位存在時的測試案例。程式碼有 level: level || 'warning' 處理,但此行為應被明確驗證。
建議:請新增測試案例,模擬評論內文缺少 嚴重等級等級 欄位,但有 審查員問題/建議 欄位時,確認 level 會正確地預設為 warning

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `parseBotReviewComment` 缺少對 `levelRaw` 為空但其他欄位存在時的測試案例。程式碼有 `level: level || 'warning'` 處理,但此行為應被明確驗證。 **建議**:請新增測試案例,模擬評論內文缺少 `嚴重等級` 或 `等級` 欄位,但有 `審查員` 和 `問題`/`建議` 欄位時,確認 `level` 會正確地預設為 `warning`。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 parseBotReviewComment 缺少對 levelRaw 為空但其他欄位存在時的測試案例。程式碼有 level: level || 'warning' 處理,但此行為應被明確驗證。
建議:請新增測試案例,模擬評論內文缺少 嚴重等級等級 欄位,但有 審查員問題/建議 欄位時,確認 level 會正確地預設為 warning

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `parseBotReviewComment` 缺少對 `levelRaw` 為空但其他欄位存在時的測試案例。程式碼有 `level: level || 'warning'` 處理,但此行為應被明確驗證。 **建議**:請新增測試案例,模擬評論內文缺少 `嚴重等級` 或 `等級` 欄位,但有 `審查員` 和 `問題`/`建議` 欄位時,確認 `level` 會正確地預設為 `warning`。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 parseBotReviewComment 缺少對 levelRaw 為空但其他欄位存在時的測試案例。程式碼有 level: level || 'warning' 處理,但此行為應被明確驗證。
建議:請新增測試案例,模擬評論內文缺少 嚴重等級等級 欄位,但有 審查員問題/建議 欄位時,確認 level 會正確地預設為 warning

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `parseBotReviewComment` 缺少對 `levelRaw` 為空但其他欄位存在時的測試案例。程式碼有 `level: level || 'warning'` 處理,但此行為應被明確驗證。 **建議**:請新增測試案例,模擬評論內文缺少 `嚴重等級` 或 `等級` 欄位,但有 `審查員` 和 `問題`/`建議` 欄位時,確認 `level` 會正確地預設為 `warning`。
};
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 parseBotReviewComment 缺少對 problem 存在但 suggestion 為空字串的測試案例。程式碼有 suggestion: suggestion || problem || '' 處理,但此行為應被明確驗證。
建議:請新增測試案例,模擬評論內文只包含 問題 欄位而無 建議 欄位時,確認 suggestion 會正確地使用 problem 的內容。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `parseBotReviewComment` 缺少對 `problem` 存在但 `suggestion` 為空字串的測試案例。程式碼有 `suggestion: suggestion || problem || ''` 處理,但此行為應被明確驗證。 **建議**:請新增測試案例,模擬評論內文只包含 `問題` 欄位而無 `建議` 欄位時,確認 `suggestion` 會正確地使用 `problem` 的內容。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 parseBotReviewComment 缺少對 problem 存在但 suggestion 為空字串的測試案例。程式碼有 suggestion: suggestion || problem || '' 處理,但此行為應被明確驗證。
建議:請新增測試案例,模擬評論內文只包含 問題 欄位而無 建議 欄位時,確認 suggestion 會正確地使用 problem 的內容。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `parseBotReviewComment` 缺少對 `problem` 存在但 `suggestion` 為空字串的測試案例。程式碼有 `suggestion: suggestion || problem || ''` 處理,但此行為應被明確驗證。 **建議**:請新增測試案例,模擬評論內文只包含 `問題` 欄位而無 `建議` 欄位時,確認 `suggestion` 會正確地使用 `problem` 的內容。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 parseBotReviewComment 缺少對 problem 存在但 suggestion 為空字串的測試案例。程式碼有 suggestion: suggestion || problem || '' 處理,但此行為應被明確驗證。
建議:請新增測試案例,模擬評論內文只包含 問題 欄位而無 建議 欄位時,確認 suggestion 會正確地使用 problem 的內容。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `parseBotReviewComment` 缺少對 `problem` 存在但 `suggestion` 為空字串的測試案例。程式碼有 `suggestion: suggestion || problem || ''` 處理,但此行為應被明確驗證。 **建議**:請新增測試案例,模擬評論內文只包含 `問題` 欄位而無 `建議` 欄位時,確認 `suggestion` 會正確地使用 `problem` 的內容。
}
/**
* 把 PR 上的行內 review comment 依「檔案路徑 + 行號」收斂成對話(同一處的留言與回覆視為一段對話)。
* 對話只要任一則 comment 帶有 resolver 即視為已解決;同時嘗試解析出該對話對應的 bot finding。
*/
export function groupConversations(comments) {
const groups = new Map();
for (const c of comments || []) {
const filePath = typeof c?.path === 'string' ? c.path : '';
const lineNum = Number(c?.position) || Number(c?.original_position) || 0;
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Assassin
問題:在 parseBotReviewComment 函式中,從 Gitea comment 內文解析出的 problemsuggestion 欄位,若包含惡意 HTML 或 JavaScript 程式碼,且這些內容在後續的處理或顯示中未經適當的輸出編碼,可能導致跨網站指令碼(XSS)攻擊。
建議:確保所有從外部來源解析出的字串(特別是 problemsuggestion)在任何將其渲染到網頁或其他使用者介面的地方,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼、JavaScript 字串編碼等),以防止 XSS 攻擊。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:在 `parseBotReviewComment` 函式中,從 Gitea comment 內文解析出的 `problem` 和 `suggestion` 欄位,若包含惡意 HTML 或 JavaScript 程式碼,且這些內容在後續的處理或顯示中未經適當的輸出編碼,可能導致跨網站指令碼(XSS)攻擊。 **建議**:確保所有從外部來源解析出的字串(特別是 `problem` 和 `suggestion`)在任何將其渲染到網頁或其他使用者介面的地方,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼、JavaScript 字串編碼等),以防止 XSS 攻擊。
Outdated
Review

嚴重等級🟡 警告
審查員:Assassin
問題:在 parseBotReviewComment 函式中,從 Gitea comment 內文解析出的 problemsuggestion 欄位若包含惡意內容且未經適當輸出編碼,可能導致 XSS 攻擊。
建議:確保所有從外部來源解析出的字串在渲染到任何介面時,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼),以防止 XSS 攻擊。

**嚴重等級**:🟡 警告 **審查員**:Assassin **問題**:在 `parseBotReviewComment` 函式中,從 Gitea comment 內文解析出的 `problem` 和 `suggestion` 欄位若包含惡意內容且未經適當輸出編碼,可能導致 XSS 攻擊。 **建議**:確保所有從外部來源解析出的字串在渲染到任何介面時,都必須經過嚴格的上下文相關輸出編碼(例如 HTML 實體編碼),以防止 XSS 攻擊。
const key = `${filePath}|${lineNum}`;
if (!groups.has(key)) {
groups.set(key, { key, path: filePath, line: lineNum, commentIds: [], bodies: [], resolved: false, botFinding: null });
}
const g = groups.get(key);
if (c?.id != null) g.commentIds.push(c.id);
const body = typeof c?.body === 'string' ? c.body : '';
if (body) g.bodies.push(body);
if (c?.resolver) g.resolved = true;
if (!g.botFinding) {
const finding = parseBotReviewComment(body);
if (finding) g.botFinding = { ...finding, location: lineNum ? `${filePath}:${lineNum}` : filePath };
}
}
return [...groups.values()].map(g => ({ ...g, thread: g.bodies.join('\n---\n') }));
}
/** 取目標行附近的程式碼片段(含行號),讓 AI 對照判斷問題是否已解決。 */
admin marked this conversation as resolved
Review

嚴重等級🔵 建議
審查員:Mage
問題:在 groupConversations 函式中,若行內 review comment 缺乏 pathposition/original_position 資訊,它們將會被歸類到一個共同的 key (例如 |0)。這可能導致多個實際上不相關的、缺乏位置資訊的留言被錯誤地歸類為同一個對話群組。雖然這類留言通常不屬於「行內」評論,且 parseBotReviewComment 可能會將其視為非 bot 留言,但這種歸類方式可能與預期不符。
建議:考慮是否應明確地過濾掉缺乏 path 或有效 position 的留言,或為這些留言提供一個更具區分性的預設 key,以避免不相關的留言被意外地歸併。例如,可以在迴圈開始時增加判斷:if (!c?.path || (!c?.position && !c?.original_position)) continue;

**嚴重等級**:🔵 建議 **審查員**:Mage **問題**:在 `groupConversations` 函式中,若行內 review comment 缺乏 `path` 或 `position`/`original_position` 資訊,它們將會被歸類到一個共同的 `key` (例如 `|0`)。這可能導致多個實際上不相關的、缺乏位置資訊的留言被錯誤地歸類為同一個對話群組。雖然這類留言通常不屬於「行內」評論,且 `parseBotReviewComment` 可能會將其視為非 bot 留言,但這種歸類方式可能與預期不符。 **建議**:考慮是否應明確地過濾掉缺乏 `path` 或有效 `position` 的留言,或為這些留言提供一個更具區分性的預設 `key`,以避免不相關的留言被意外地歸併。例如,可以在迴圈開始時增加判斷:`if (!c?.path || (!c?.position && !c?.original_position)) continue;`。
Review

嚴重等級🔵 建議
審查員:Mage
問題:在 groupConversations 函式中,若行內 review comment 缺乏 pathposition/original_position 資訊,它們將會被歸類到一個共同的 key (例如 |0)。這可能導致多個實際上不相關的、缺乏位置資訊的留言被錯誤地歸類為同一個對話群組。雖然這類留言通常不屬於「行內」評論,且 parseBotReviewComment 可能會將其視為非 bot 留言,但這種歸類方式可能與預期不符。
建議:考慮是否應明確地過濾掉缺乏 path 或有效 position 的留言,或為這些留言提供一個更具區分性的預設 key,以避免不相關的留言被意外地歸併。例如,可以在迴圈開始時增加判斷:if (!c?.path || (!c?.position && !c?.original_position)) continue;

**嚴重等級**:🔵 建議 **審查員**:Mage **問題**:在 `groupConversations` 函式中,若行內 review comment 缺乏 `path` 或 `position`/`original_position` 資訊,它們將會被歸類到一個共同的 `key` (例如 `|0`)。這可能導致多個實際上不相關的、缺乏位置資訊的留言被錯誤地歸類為同一個對話群組。雖然這類留言通常不屬於「行內」評論,且 `parseBotReviewComment` 可能會將其視為非 bot 留言,但這種歸類方式可能與預期不符。 **建議**:考慮是否應明確地過濾掉缺乏 `path` 或有效 `position` 的留言,或為這些留言提供一個更具區分性的預設 `key`,以避免不相關的留言被意外地歸併。例如,可以在迴圈開始時增加判斷:`if (!c?.path || (!c?.position && !c?.original_position)) continue;`。
Review

嚴重等級🔵 建議
審查員:Mage
問題:缺乏位置資訊的留言會被歸類到同一個預設 key,可能導致不相關留言被錯誤歸併。
建議:明確過濾缺乏 pathposition 的留言,或提供更具區分性的預設 key。

**嚴重等級**:🔵 建議 **審查員**:Mage **問題**:缺乏位置資訊的留言會被歸類到同一個預設 key,可能導致不相關留言被錯誤歸併。 **建議**:明確過濾缺乏 `path` 或 `position` 的留言,或提供更具區分性的預設 key。
export function codeWindow(content, lineNum, radius = 20) {
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);
const end = Math.min(lines.length, center + radius + 1);
return lines.slice(start, end).map((text, i) => `${start + i + 1}: ${text}`).join('\n');
admin marked this conversation as resolved
Review

嚴重等級🔴 嚴重
審查員:Mage
問題:在 judgeConversationsResolved 函式中,對 chatFn 的結果結構缺乏足夠的嚴格檢查。若回傳結構不符合預期,可能導致所有對話被錯誤判定為「未解決」。
建議:增加對 result 結構的嚴格檢查。如果 result 不是預期的陣列結構,應拋出例外或進行更謹慎的錯誤處理,而不是默默地將所有對話視為未解決。

**嚴重等級**:🔴 嚴重 **審查員**:Mage **問題**:在 `judgeConversationsResolved` 函式中,對 `chatFn` 的結果結構缺乏足夠的嚴格檢查。若回傳結構不符合預期,可能導致所有對話被錯誤判定為「未解決」。 **建議**:增加對 `result` 結構的嚴格檢查。如果 `result` 不是預期的陣列結構,應拋出例外或進行更謹慎的錯誤處理,而不是默默地將所有對話視為未解決。
Review

嚴重等級🟡 警告
審查員:Leo
問題:程式碼片段定位邏輯(如字串拼接行號)與上下文擷取策略(如 radius)寫死在函式內,擴展性與維護性不足。
建議:建立明確的 Location 物件封裝定位資訊,並將 radius 或擷取策略抽離為配置參數或常數。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:程式碼片段定位邏輯(如字串拼接行號)與上下文擷取策略(如 radius)寫死在函式內,擴展性與維護性不足。 **建議**:建立明確的 `Location` 物件封裝定位資訊,並將 `radius` 或擷取策略抽離為配置參數或常數。
Review

嚴重等級🟡 警告
審查員:Rogue
問題:大量使用字串拼接產生暫存物件,以及並行請求未限制數量,在高負載下可能導致 GC 壓力或觸發 API 限流。
建議:對於大量 comments,考慮使用複合物件或分層 Map 結構。引入請求並行限制(如 p-limit)來確保系統穩定性。

**嚴重等級**:🟡 警告 **審查員**:Rogue **問題**:大量使用字串拼接產生暫存物件,以及並行請求未限制數量,在高負載下可能導致 GC 壓力或觸發 API 限流。 **建議**:對於大量 comments,考慮使用複合物件或分層 Map 結構。引入請求並行限制(如 `p-limit`)來確保系統穩定性。
Review

嚴重等級🟡 警告
審查員:Maya
問題:在 codeWindow 函數中,缺乏對輸入的邊界檢查,特別是當 lineNum 為 0 或負數,或是大於總行數時,可能導致行為不預期或 slice 產生錯誤。
建議:建議在計算 startend 時,增加明確的邊界檢核與處理,確保即使 lineNum 異常時也能安全返回或處理。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在 `codeWindow` 函數中,缺乏對輸入的邊界檢查,特別是當 `lineNum` 為 0 或負數,或是大於總行數時,可能導致行為不預期或 slice 產生錯誤。 **建議**:建議在計算 `start` 和 `end` 時,增加明確的邊界檢核與處理,確保即使 `lineNum` 異常時也能安全返回或處理。
}
/**
* 批次請 AI 判斷每個對話指出的問題在最新程式碼中是否已解決。
* 回傳與輸入等長、依 idx 對齊的 [{ idx, resolved }];無法判斷一律視為未解決(寧可保留)。
*/
export async function judgeConversationsResolved(items, chatFn = chatJSON) {
if (!items || items.length === 0) return [];
const systemPrompt = `你是 🛡️ Paladin(聖騎士),公正的裁判。下面是一批 PR review 對話(JSON 陣列),每個對話包含:曾被指出的問題(thread)、問題所在檔案 path 與行號 line、以及該位置最新的程式碼片段 code。請逐一判斷「該對話指出的問題在最新程式碼中是否已被解決」。只回傳 JSON 陣列,每個元素為 {"idx": 數字, "resolved": true 或 false},不要有其他文字。若資訊不足以判斷,resolved 一律填 false。`;
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(
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:在 judgeConversationsResolved 函數中,AI 判斷回傳結構如果不符合預期(非陣列),雖有降級處理,但未驗證當 AI 回傳包含無效 idx 或缺少 resolved 欄位的物件時,對應邏輯是否正確過濾。
建議:補測試案例,模擬 AI 回傳包含無效結構(如 idx 為字串、缺少 resolved)的 JSON,確保系統能正確忽略無效項並將其視為未解決。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:在 `judgeConversationsResolved` 函數中,AI 判斷回傳結構如果不符合預期(非陣列),雖有降級處理,但未驗證當 AI 回傳包含無效 `idx` 或缺少 `resolved` 欄位的物件時,對應邏輯是否正確過濾。 **建議**:補測試案例,模擬 AI 回傳包含無效結構(如 `idx` 為字串、缺少 `resolved`)的 JSON,確保系統能正確忽略無效項並將其視為未解決。
(Array.isArray(result) ? result : [])
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 judgeConversationsResolved 缺少對 chatFn 拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。
建議:請新增測試案例,模擬 chatFn 拋出錯誤時,確認 judgeConversationsResolved 會正確地將錯誤向上拋出,以便呼叫者處理。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `judgeConversationsResolved` 缺少對 `chatFn` 拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。 **建議**:請新增測試案例,模擬 `chatFn` 拋出錯誤時,確認 `judgeConversationsResolved` 會正確地將錯誤向上拋出,以便呼叫者處理。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 judgeConversationsResolved 缺少對 chatFn 拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。
建議:請新增測試案例,模擬 chatFn 拋出錯誤時,確認 judgeConversationsResolved 會正確地將錯誤向上拋出,以便呼叫者處理。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `judgeConversationsResolved` 缺少對 `chatFn` 拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。 **建議**:請新增測試案例,模擬 `chatFn` 拋出錯誤時,確認 `judgeConversationsResolved` 會正確地將錯誤向上拋出,以便呼叫者處理。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 judgeConversationsResolved 缺少對 chatFn 拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。
建議:請新增測試案例,模擬 chatFn 拋出錯誤時,確認 judgeConversationsResolved 會正確地將錯誤向上拋出,以便呼叫者處理。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `judgeConversationsResolved` 缺少對 `chatFn` 拋出錯誤情境的測試。雖然上層呼叫者有處理,但此函式本身的錯誤行為應被驗證。 **建議**:請新增測試案例,模擬 `chatFn` 拋出錯誤時,確認 `judgeConversationsResolved` 會正確地將錯誤向上拋出,以便呼叫者處理。
.filter(r => Number.isInteger(r?.idx))
admin marked this conversation as resolved
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 judgeConversationsResolved 缺少對 AI 回傳結果中元素缺少 idxresolved 欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。
建議:請新增測試案例,模擬 chatFn 回傳的陣列中,有些物件缺少 idxresolved 屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `judgeConversationsResolved` 缺少對 AI 回傳結果中元素缺少 `idx` 或 `resolved` 欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。 **建議**:請新增測試案例,模擬 `chatFn` 回傳的陣列中,有些物件缺少 `idx` 或 `resolved` 屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 judgeConversationsResolved 缺少對 AI 回傳結果中元素缺少 idxresolved 欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。
建議:請新增測試案例,模擬 chatFn 回傳的陣列中,有些物件缺少 idxresolved 屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `judgeConversationsResolved` 缺少對 AI 回傳結果中元素缺少 `idx` 或 `resolved` 欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。 **建議**:請新增測試案例,模擬 `chatFn` 回傳的陣列中,有些物件缺少 `idx` 或 `resolved` 屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 judgeConversationsResolved 缺少對 AI 回傳結果中元素缺少 idxresolved 欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。
建議:請新增測試案例,模擬 chatFn 回傳的陣列中,有些物件缺少 idxresolved 屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `judgeConversationsResolved` 缺少對 AI 回傳結果中元素缺少 `idx` 或 `resolved` 欄位的測試案例。雖然程式碼有過濾處理,但此邊界條件應被明確驗證。 **建議**:請新增測試案例,模擬 `chatFn` 回傳的陣列中,有些物件缺少 `idx` 或 `resolved` 屬性時,確認這些無效的結果會被正確過濾,且其他有效結果能被正確處理。
.map(r => [r.idx, r.resolved === true]),
);
return items.map(it => ({ idx: it.idx, resolved: byIdx.get(it.idx) === true }));
}
function pushCarried(target, conversation) {
if (!conversation.botFinding) return;
target.push({ ...conversation.botFinding, is_new: false });
}
/**
* 對話收斂主流程:
* 1. 取得 PR 所有行內 review comment,收斂成對話,跳過已 resolve 的;
* 2. 取每個對話所在檔案的最新內容,請 AI 判斷問題是否已解決;
* 3. 已解決者呼叫 Gitea resolve API 解決對話,並記錄其 finding(供移除舊問題);
* 4. 未解決且可解析為 bot finding 者,收集為「加回問題列表」清單。
* 任一外部呼叫失敗都降級處理(保守視為未解決),不中斷整體 pipeline。
*/
export async function reconcileConversations(deps = {}) {
const {
listComments = listAllReviewComments,
resolveComment = resolvePullReviewComment,
getFileContent = getFileContentAtRef,
judge = judgeConversationsResolved,
} = deps;
let comments;
try {
comments = await listComments();
} catch (e) {
warn(`取得 PR review comments 失敗,跳過對話收斂: ${e.message}`);
return { ...EMPTY };
}
const conversations = groupConversations(comments);
const open = conversations.filter(c => !c.resolved && c.commentIds.length > 0);
const alreadyResolved = conversations.length - open.length;
line(`對話收斂: 對話總數=${conversations.length} 已解決/不可處理=${alreadyResolved} 待判斷=${open.length}`);
if (open.length === 0) return { ...EMPTY };
const fileCache = new Map();
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Maya
問題:函式 reconcileConversations 在取得單一檔案內容 (getFileContent) 失敗時,會中斷整個對話收斂流程。這會導致即使只有一個檔案出錯,整個 PR 的收斂都無法完成。
建議:請修改 reconcileConversations,在 fileCache.set(filePath, await getFileContent(filePath)) 的迴圈中,為 getFileContent 加上 try-catch 區塊。當單一檔案取得失敗時,應記錄警告並將該檔案的內容視為空字串,而不是中斷整個流程,以確保其他檔案的處理不受影響。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:函式 `reconcileConversations` 在取得單一檔案內容 (`getFileContent`) 失敗時,會中斷整個對話收斂流程。這會導致即使只有一個檔案出錯,整個 PR 的收斂都無法完成。 **建議**:請修改 `reconcileConversations`,在 `fileCache.set(filePath, await getFileContent(filePath))` 的迴圈中,為 `getFileContent` 加上 `try-catch` 區塊。當單一檔案取得失敗時,應記錄警告並將該檔案的內容視為空字串,而不是中斷整個流程,以確保其他檔案的處理不受影響。
for (const filePath of [...new Set(open.map(c => c.path).filter(Boolean))]) {
fileCache.set(filePath, await getFileContent(filePath));
}
const items = open.map((c, idx) => ({
idx,
path: c.path,
line: c.line,
thread: c.thread,
code: codeWindow(fileCache.get(c.path) || '', c.line),
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Maya
問題reconcileConversations 核心流程中,對於 getFileContent 失敗或內容為空的處理邏輯,直接降級為空字串並視為未解決,但若檔案內容實際上非空且未解決,這可能導致判斷偏差。
建議:補測試案例,模擬 getFileContent 拋出錯誤時,reconcileConversations 是否正確地將對話保留為未解決,且後續統計數字(carriedFindings)是否正確。

**嚴重等級**:🔴 嚴重 **審查員**:Maya **問題**:`reconcileConversations` 核心流程中,對於 `getFileContent` 失敗或內容為空的處理邏輯,直接降級為空字串並視為未解決,但若檔案內容實際上非空且未解決,這可能導致判斷偏差。 **建議**:補測試案例,模擬 `getFileContent` 拋出錯誤時,`reconcileConversations` 是否正確地將對話保留為未解決,且後續統計數字(`carriedFindings`)是否正確。
}));
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 reconcileConversations 缺少對 judge 拋出錯誤情境的明確測試。雖然程式碼有 try-catch 處理,但應有專門的測試案例來驗證此失敗路徑的行為。
建議:請新增測試案例,模擬 judge 函式拋出錯誤時,確認 reconcileConversations 能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即 verdicts 應全部為 resolved: false)。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `reconcileConversations` 缺少對 `judge` 拋出錯誤情境的明確測試。雖然程式碼有 `try-catch` 處理,但應有專門的測試案例來驗證此失敗路徑的行為。 **建議**:請新增測試案例,模擬 `judge` 函式拋出錯誤時,確認 `reconcileConversations` 能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即 `verdicts` 應全部為 `resolved: false`)。
Outdated
Review

嚴重等級🟡 警告
審查員:Maya
問題:缺少關鍵邊界條件與異常路徑的測試案例。包含 judge 拋出錯誤、chatFn 解析異常、levelRawsuggestion 空值、getFileContent 失敗以及混合正確/錯誤的判斷數據等場景。
建議:請在 app/resolve.test.js 中新增這些邊界條件的測試案例,確保系統在面對 AI 異常輸出、API 失敗、或輸入欄位缺失時,仍能穩健處理並符合預期行為。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:缺少關鍵邊界條件與異常路徑的測試案例。包含 `judge` 拋出錯誤、`chatFn` 解析異常、`levelRaw` 或 `suggestion` 空值、`getFileContent` 失敗以及混合正確/錯誤的判斷數據等場景。 **建議**:請在 `app/resolve.test.js` 中新增這些邊界條件的測試案例,確保系統在面對 AI 異常輸出、API 失敗、或輸入欄位缺失時,仍能穩健處理並符合預期行為。
Outdated
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 reconcileConversations 缺少對 judge 拋出錯誤情境的明確測試。雖然程式碼有 try-catch 處理,但應有專門的測試案例來驗證此失敗路徑的行為。
建議:請新增測試案例,模擬 judge 函式拋出錯誤時,確認 reconcileConversations 能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即 verdicts 應全部為 resolved: false)。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `reconcileConversations` 缺少對 `judge` 拋出錯誤情境的明確測試。雖然程式碼有 `try-catch` 處理,但應有專門的測試案例來驗證此失敗路徑的行為。 **建議**:請新增測試案例,模擬 `judge` 函式拋出錯誤時,確認 `reconcileConversations` 能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即 `verdicts` 應全部為 `resolved: false`)。
Outdated
Review

嚴重等級🟡 警告
審查員:Maya
問題:函式 reconcileConversations 缺少對 judge 拋出錯誤情境的明確測試。雖然程式碼有 try-catch 處理,但應有專門的測試案例來驗證此失敗路徑的行為。
建議:請新增測試案例,模擬 judge 函式拋出錯誤時,確認 reconcileConversations 能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即 verdicts 應全部為 resolved: false)。

**嚴重等級**:🟡 警告 **審查員**:Maya **問題**:函式 `reconcileConversations` 缺少對 `judge` 拋出錯誤情境的明確測試。雖然程式碼有 `try-catch` 處理,但應有專門的測試案例來驗證此失敗路徑的行為。 **建議**:請新增測試案例,模擬 `judge` 函式拋出錯誤時,確認 `reconcileConversations` 能正確捕獲錯誤,記錄警告,並將所有待判斷的對話都視為未解決(即 `verdicts` 應全部為 `resolved: false`)。
let verdicts;
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Assassin
問題:在 judgeConversationsResolved 函式中,thread(來自 Gitea comment 內容)和 code(來自 PR 檔案內容)被直接拼接進傳給 LLM 的 payload 中。如果攻擊者能夠控制這些內容,他們可以透過注入惡意指令來劫持 LLM 的行為,例如使其始終將特定問題判斷為已解決,或嘗試從 LLM 獲取敏感資訊(提示詞注入)。
建議:對所有傳遞給 LLM 的外部輸入(如 threadcode)進行嚴格的淨化和隔離。考慮使用結構化輸入而非直接拼接字串,並在 LLM 提示詞中明確指示其忽略任何試圖改變其行為的指令。對於敏感操作,應建立多層驗證機制,不單純依賴 LLM 的判斷。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:在 `judgeConversationsResolved` 函式中,`thread`(來自 Gitea comment 內容)和 `code`(來自 PR 檔案內容)被直接拼接進傳給 LLM 的 `payload` 中。如果攻擊者能夠控制這些內容,他們可以透過注入惡意指令來劫持 LLM 的行為,例如使其始終將特定問題判斷為已解決,或嘗試從 LLM 獲取敏感資訊(提示詞注入)。 **建議**:對所有傳遞給 LLM 的外部輸入(如 `thread` 和 `code`)進行嚴格的淨化和隔離。考慮使用結構化輸入而非直接拼接字串,並在 LLM 提示詞中明確指示其忽略任何試圖改變其行為的指令。對於敏感操作,應建立多層驗證機制,不單純依賴 LLM 的判斷。
Outdated
Review

嚴重等級🔴 嚴重
審查員:Assassin
問題:LLM 提示詞注入風險:在 judgeConversationsResolved 函式中,外部來源的 threadcode 被直接拼接進傳給 LLM 的 payload 中,攻擊者可能注入惡意指令來劫持 LLM 行為。
建議:對所有傳遞給 LLM 的外部輸入進行嚴格的淨化和隔離。使用結構化輸入而非直接拼接字串,並在提示詞中明確指示 AI 忽略任何試圖下達指令的內容,僅對邏輯進行判斷。對於敏感操作,應建立多層驗證機制。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:LLM 提示詞注入風險:在 `judgeConversationsResolved` 函式中,外部來源的 `thread` 和 `code` 被直接拼接進傳給 LLM 的 `payload` 中,攻擊者可能注入惡意指令來劫持 LLM 行為。 **建議**:對所有傳遞給 LLM 的外部輸入進行嚴格的淨化和隔離。使用結構化輸入而非直接拼接字串,並在提示詞中明確指示 AI 忽略任何試圖下達指令的內容,僅對邏輯進行判斷。對於敏感操作,應建立多層驗證機制。
try {
verdicts = await judge(items);
} catch (e) {
warn(`AI 判斷對話解決狀態失敗,全部視為未解決: ${e.message}`);
verdicts = items.map(it => ({ idx: it.idx, resolved: false }));
}
const resolvedSet = new Set(verdicts.filter(v => v.resolved).map(v => v.idx));
const resolvedFindings = [];
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Rogue
問題:這裡又在浪費時間!reconcileConversations 函式在取得所有獨特的檔案路徑後,又在迴圈裡對每個檔案路徑依序呼叫 getFileContent。如果有很多檔案需要檢查,這會導致 F 次遠端 API 呼叫依序執行,嚴重拖慢整體流程。
建議:改用 Promise.allPromise.allSettled 來並行發送所有 getFileContent 的請求。這樣可以大幅減少等待時間,讓檔案內容的取得幾乎同時完成。

**嚴重等級**:🔴 嚴重 **審查員**:Rogue **問題**:這裡又在浪費時間!`reconcileConversations` 函式在取得所有獨特的檔案路徑後,又在迴圈裡對每個檔案路徑依序呼叫 `getFileContent`。如果有很多檔案需要檢查,這會導致 `F` 次遠端 API 呼叫依序執行,嚴重拖慢整體流程。 **建議**:改用 `Promise.all` 或 `Promise.allSettled` 來並行發送所有 `getFileContent` 的請求。這樣可以大幅減少等待時間,讓檔案內容的取得幾乎同時完成。
const carriedFindings = [];
let resolvedCount = 0;
for (let i = 0; i < open.length; i++) {
const c = open[i];
if (resolvedSet.has(i)) {
try {
await resolveComment(c.commentIds[0]);
resolvedCount += 1;
if (c.botFinding) resolvedFindings.push({ ...c.botFinding, is_new: false });
ok(`對話已解決並 resolve: ${c.path}:${c.line}`);
continue;
} catch (e) {
warn(`resolve 對話失敗(保留為未解決): ${c.path}:${c.line} error=${e.message}`);
}
}
pushCarried(carriedFindings, c);
}
const unresolvedCount = open.length - resolvedCount;
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Rogue
問題:又來了!reconcileConversations 函式在迴圈裡對每個需要解決的對話依序呼叫 resolveComment。這又是一個 N+1 查詢問題,如果有很多對話需要解決,會導致 N_open 次遠端 API 呼叫依序執行,效率極差。
建議:改用 Promise.allSettled 來並行發送所有 resolveComment 的請求。這樣可以大幅減少等待時間,讓對話的解決幾乎同時完成,即使部分失敗也不會中斷其他請求。

**嚴重等級**:🔴 嚴重 **審查員**:Rogue **問題**:又來了!`reconcileConversations` 函式在迴圈裡對每個需要解決的對話依序呼叫 `resolveComment`。這又是一個 N+1 查詢問題,如果有很多對話需要解決,會導致 `N_open` 次遠端 API 呼叫依序執行,效率極差。 **建議**:改用 `Promise.allSettled` 來並行發送所有 `resolveComment` 的請求。這樣可以大幅減少等待時間,讓對話的解決幾乎同時完成,即使部分失敗也不會中斷其他請求。
Outdated
Review

嚴重等級🔵 建議
審查員:Rogue
問題Promise.allSettled 的結果處理邏輯過於冗長,產生不必要的中間變數。
建議:優化處理邏輯,直接在迴圈內處理或使用更緊湊的寫法。

**嚴重等級**:🔵 建議 **審查員**:Rogue **問題**:`Promise.allSettled` 的結果處理邏輯過於冗長,產生不必要的中間變數。 **建議**:優化處理邏輯,直接在迴圈內處理或使用更緊湊的寫法。
ok(`對話收斂完成: resolved=${resolvedCount} unresolved=${unresolvedCount} 加回 findings=${carriedFindings.length}`);
return { resolvedFindings, carriedFindings, resolvedCount, unresolvedCount };
}
function fileOf(location) {
return String(location || '').split(':')[0].trim();
}
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔴 嚴重
審查員:Assassin
問題:在 reconcileConversations 函式中,從外部 Gitea comment 取得的 c.path(檔案路徑)未經額外驗證或淨化,直接傳遞給了 getFileContent(即 getFileContentAtRef)。由於 getFileContentAtRef 存在路徑穿越漏洞,攻擊者可以透過在 PR 中建立惡意檔案名稱,並在該檔案上留言,來觸發路徑穿越,讀取伺服器上的任意檔案。
建議:在將 c.path 傳遞給 getFileContent 之前,必須對其進行嚴格的白名單驗證,確保它只包含預期的檔案名稱字元,且不包含任何路徑穿越序列(例如 ../)。或者,確保 getFileContentAtRef 的路徑處理是絕對安全的,不允許任何形式的路徑穿越。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:在 `reconcileConversations` 函式中,從外部 Gitea comment 取得的 `c.path`(檔案路徑)未經額外驗證或淨化,直接傳遞給了 `getFileContent`(即 `getFileContentAtRef`)。由於 `getFileContentAtRef` 存在路徑穿越漏洞,攻擊者可以透過在 PR 中建立惡意檔案名稱,並在該檔案上留言,來觸發路徑穿越,讀取伺服器上的任意檔案。 **建議**:在將 `c.path` 傳遞給 `getFileContent` 之前,必須對其進行嚴格的白名單驗證,確保它只包含預期的檔案名稱字元,且不包含任何路徑穿越序列(例如 `..` 或 `/`)。或者,確保 `getFileContentAtRef` 的路徑處理是絕對安全的,不允許任何形式的路徑穿越。
Outdated
Review

嚴重等級🔴 嚴重
審查員:Assassin
問題:在 reconcileConversations 函式中,從外部 Gitea comment 取得的 c.path(檔案路徑)未經額外驗證或淨化,直接傳遞給了 getFileContent(即 getFileContentAtRef)。由於 getFileContentAtRef 存在路徑穿越漏洞,攻擊者可以透過在 PR 中建立惡意檔案名稱,並在該檔案上留言,來觸發路徑穿越,讀取伺服器上的任意檔案。
建議:在將 c.path 傳遞給 getFileContent 之前,必須對其進行嚴格的白名單驗證,確保它只包含預期的檔案名稱字元,且不包含任何路徑穿越序列(例如 ../)。或者,確保 getFileContentAtRef 的路徑處理是絕對安全的,不允許任何形式的路徑穿越。

**嚴重等級**:🔴 嚴重 **審查員**:Assassin **問題**:在 `reconcileConversations` 函式中,從外部 Gitea comment 取得的 `c.path`(檔案路徑)未經額外驗證或淨化,直接傳遞給了 `getFileContent`(即 `getFileContentAtRef`)。由於 `getFileContentAtRef` 存在路徑穿越漏洞,攻擊者可以透過在 PR 中建立惡意檔案名稱,並在該檔案上留言,來觸發路徑穿越,讀取伺服器上的任意檔案。 **建議**:在將 `c.path` 傳遞給 `getFileContent` 之前,必須對其進行嚴格的白名單驗證,確保它只包含預期的檔案名稱字元,且不包含任何路徑穿越序列(例如 `..` 或 `/`)。或者,確保 `getFileContentAtRef` 的路徑處理是絕對安全的,不允許任何形式的路徑穿越。
function normalizeKey(text) {
return String(text || '')
.normalize('NFKC')
.replace(/[\p{P}\p{S}\s]+/gu, '')
.trim()
.toLowerCase();
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Mage
問題:在 reconcileConversations 函式中,並行(Promise.all)呼叫 resolveComment,即使個別呼叫失敗,也僅在 settled 中記錄為 rejected 並印出 warn。然而,若 resolveComment 失敗是因為 Authorization token 過期或權限不足,後續所有的 resolve 呼叫都會失敗,此時程式碼沒有對這些特定的錯誤進行分類處理。
建議:應判斷 outcome.reason 的錯誤類型。若是連線/權限相關的嚴重錯誤,應立即停止後續的 resolve 嘗試,避免在已知無法成功的情況下發出無效請求。

**嚴重等級**:🟡 警告 **審查員**:Mage **問題**:在 `reconcileConversations` 函式中,並行(`Promise.all`)呼叫 `resolveComment`,即使個別呼叫失敗,也僅在 `settled` 中記錄為 `rejected` 並印出 `warn`。然而,若 `resolveComment` 失敗是因為 `Authorization` token 過期或權限不足,後續所有的 `resolve` 呼叫都會失敗,此時程式碼沒有對這些特定的錯誤進行分類處理。 **建議**:應判斷 `outcome.reason` 的錯誤類型。若是連線/權限相關的嚴重錯誤,應立即停止後續的 `resolve` 嘗試,避免在已知無法成功的情況下發出無效請求。
}
/** 以「檔案路徑 + 正規化建議內容」為簽章,對 line 漂移與標點差異穩定。 */
function findingSig(f) {
return `${fileOf(f?.location)}|${normalizeKey(f?.suggestion)}`;
}
/**
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🔵 建議
審查員:Mage
問題:對 botFinding 的存取缺乏防禦性檢查。
建議:在 push 之前增加防禦性檢查,確保物件完整性。

**嚴重等級**:🔵 建議 **審查員**:Mage **問題**:對 `botFinding` 的存取缺乏防禦性檢查。 **建議**:在 `push` 之前增加防禦性檢查,確保物件完整性。
* 從 findings 中移除「已解決對話」對應的問題(以檔案路徑+建議內容比對,避免行號漂移誤判)。
*/
export function dropResolvedFindings(findings, resolvedFindings = []) {
if (!resolvedFindings || resolvedFindings.length === 0) return findings;
const resolved = new Set(resolvedFindings.map(findingSig));
return findings.filter(f => !resolved.has(findingSig(f)));
}
/**
* 把「未解決對話」對應、但目前 findings 清單中已遺漏的問題加回(去重以檔案路徑+建議內容為準)。
*/
export function addCarriedFindings(findings, carriedFindings = []) {
admin marked this conversation as resolved Outdated
Outdated
Review

嚴重等級🟡 警告
審查員:Leo
問題:函式 normalizeKey 對建議內容進行了非常積極的正規化,移除了所有標點符號、符號和空白字元。雖然這有助於避免行號漂移和微小措辭差異造成的重複判斷,但過度正規化可能會導致不同但語意相近的建議被視為相同,進而影響問題追蹤的精確性。
建議:請評估這種積極正規化是否會導致誤判。如果發現有不同建議被錯誤合併的情況,可以考慮放寬正規化規則,例如只移除空白字元和部分標點符號,或加入其他判斷維度(如關鍵字比對)來提高精確度。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:函式 `normalizeKey` 對建議內容進行了非常積極的正規化,移除了所有標點符號、符號和空白字元。雖然這有助於避免行號漂移和微小措辭差異造成的重複判斷,但過度正規化可能會導致不同但語意相近的建議被視為相同,進而影響問題追蹤的精確性。 **建議**:請評估這種積極正規化是否會導致誤判。如果發現有不同建議被錯誤合併的情況,可以考慮放寬正規化規則,例如只移除空白字元和部分標點符號,或加入其他判斷維度(如關鍵字比對)來提高精確度。
Outdated
Review

嚴重等級🟡 警告
審查員:Leo
問題:正規化邏輯(normalizeKey 等)過於激進且未快取,既可能導致語意相近建議被誤判為相同,也在頻繁比較時造成效能浪費。
建議:請評估目前的正規化規則,若發現誤判,放寬規則或加入關鍵字比對。將簽章產生邏輯抽離為獨立 Helper 函式,並在產生時進行快取(Memoize)以提升效能。

**嚴重等級**:🟡 警告 **審查員**:Leo **問題**:正規化邏輯(`normalizeKey` 等)過於激進且未快取,既可能導致語意相近建議被誤判為相同,也在頻繁比較時造成效能浪費。 **建議**:請評估目前的正規化規則,若發現誤判,放寬規則或加入關鍵字比對。將簽章產生邏輯抽離為獨立 Helper 函式,並在產生時進行快取(Memoize)以提升效能。
if (!carriedFindings || carriedFindings.length === 0) return findings;
const seen = new Set(findings.map(findingSig));
const additions = carriedFindings.filter(f => {
const sig = findingSig(f);
if (seen.has(sig)) return false;
seen.add(sig);
return true;
});
if (additions.length > 0) ok(`加回未解決問題: ${additions.length}`);
return [...findings, ...additions];
}