diff --git a/app/llm.js b/app/llm.js index bd63588..71f06cb 100644 --- a/app/llm.js +++ b/app/llm.js @@ -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); } /** diff --git a/app/main.js b/app/main.js index ec3974e..ca1b967 100644 --- a/app/main.js +++ b/app/main.js @@ -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)})`); diff --git a/app/test/llm.test.js b/app/test/llm.test.js index b99dd94..a23122f 100644 --- a/app/test/llm.test.js +++ b/app/test/llm.test.js @@ -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); }); });