fix(LLM 審查流程): 避免單一角色失敗中止 pipeline
CI / AI Code Review (pull_request) Failing after 7s

This commit is contained in:
2026-06-26 09:26:12 +00:00
parent a62bfbd4b8
commit 6c3e7b9d37
3 changed files with 41 additions and 12 deletions
+23 -6
View File
@@ -1,7 +1,7 @@
import axios from 'axios';
import { getLLMConfig, getOpenCodeHttpsAgent } from './config.js';
import { recordUsage } from './usage.js';
import { line, error } from './log.js';
import { line } from './log.js';
/**
* 將模型識別字串解析為 OpenCode API 所需的 provider 與 model 識別碼。
@@ -49,6 +49,23 @@ function extractOpenCodeContent(data) {
.join('');
}
function summarizeErrorResponse(data) {
if (data == null) return '';
if (typeof data === 'string') return data.slice(0, 500);
try {
return JSON.stringify(data).slice(0, 500);
} catch {
return String(data).slice(0, 500);
}
}
function formatOpenCodeError(e) {
const status = e.response?.status;
const response = summarizeErrorResponse(e.response?.data);
const statusText = status ? `HTTP ${status}` : e.message;
return response ? `${statusText}: ${response}` : statusText;
}
/**
* 對 OpenCode server 執行一次完整對話:建立 session 後送出訊息並回傳結果。
*
@@ -91,8 +108,8 @@ async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers)
* 對 OpenCode server 送出一次對話請求並回傳模型純文字回應。
*
* 從設定取得 provider/baseURL/model;未設定 provider 時拋錯。成功時記錄
* usage 並回傳內容。OpenCode 呼叫失敗時會記錄錯誤並以 `process.exit(1)`
* 終止整個行程(不會回傳)
* usage 並回傳內容。OpenCode 呼叫失敗時會記錄錯誤並向外拋出,讓呼叫端
* 決定是否降級、略過單一角色或終止整體流程
*
* @param {string} systemPrompt - 系統提示詞。
* @param {string} userContent - 使用者輸入內容。
@@ -112,10 +129,10 @@ export async function chat(systemPrompt, userContent) {
recordUsage(data);
return content;
} catch (e) {
line(`[LLM] OpenCode 呼叫失敗: ${e.message}`);
const message = formatOpenCodeError(e);
line(`[LLM] OpenCode 呼叫失敗: ${message}`);
throw new Error(message);
}
error('[LLM] OpenCode 呼叫失敗,終止流程');
process.exit(1);
}
/**
+9 -1
View File
@@ -122,10 +122,18 @@ async function main() {
}
const analyses = await Promise.allSettled(roles.map(role => analyzeWithRole(role, diff)));
const newFindings = [];
let fulfilledAnalyses = 0;
for (let i = 0; i < analyses.length; i++) {
if (analyses[i].status === 'fulfilled') newFindings.push(...analyses[i].value);
if (analyses[i].status === 'fulfilled') {
fulfilledAnalyses += 1;
newFindings.push(...analyses[i].value);
}
else warn(`[${roles[i].name}] 分析失敗(跳過): ${analyses[i].reason?.message}`);
}
if (fulfilledAnalyses === 0) {
result(false, '所有角色分析皆失敗,終止流程以避免誤判為審查通過');
process.exit(1);
}
// 對只有檔名、缺行號的問題,反問原角色補上行號(最多重試數次),確保後續能行內標註
await resolveMissingLineNumbers(newFindings, diff);
output(`新 findings ${newFindings.length} 筆(${formatFindingsStatsLine(newFindings)}`);
+9 -5
View File
@@ -85,14 +85,18 @@ describe('chat - OpenCode', async () => {
assert.equal(result, 'hello world');
});
it('calls process.exit(1) when OpenCode fails', async () => {
it('throws an error when OpenCode fails instead of exiting the process', async () => {
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
mock.method(axios, 'post', async () => { throw new Error('fail'); });
const exitMock = mock.method(process, 'exit', () => { throw new Error('exit:1'); });
mock.method(axios, 'post', async () => {
const err = new Error('Request failed with status code 500');
err.response = { status: 500, data: { error: 'provider overloaded' } };
throw err;
});
const exitMock = mock.method(process, 'exit', () => { throw new Error('exit should not be called'); });
await assert.rejects(() => chat('sys', 'user'), /exit:1/);
await assert.rejects(() => chat('sys', 'user'), /HTTP 500.*provider overloaded/);
assert.equal(exitMock.mock.calls[0].arguments[0], 1);
assert.equal(exitMock.mock.calls.length, 0);
});
});