This commit is contained in:
+27
-1
@@ -32,6 +32,10 @@ function opencodeAxiosOptions(headers) {
|
||||
};
|
||||
}
|
||||
|
||||
function sleep(ms) {
|
||||
return new Promise(resolve => setTimeout(resolve, ms));
|
||||
}
|
||||
|
||||
/**
|
||||
* 從 OpenCode 訊息回應中抽取並串接所有文字片段。
|
||||
*
|
||||
@@ -66,6 +70,11 @@ function formatOpenCodeError(e) {
|
||||
return response ? `${statusText}: ${response}` : statusText;
|
||||
}
|
||||
|
||||
function isTransientOpenCodeError(e) {
|
||||
const status = e.response?.status;
|
||||
return status === 500 || status === 502 || status === 503 || status === 504 || status === 429;
|
||||
}
|
||||
|
||||
/**
|
||||
* 對 OpenCode server 執行一次完整對話:建立 session 後送出訊息並回傳結果。
|
||||
*
|
||||
@@ -104,6 +113,23 @@ async function chatOpenCode(baseURL, model, systemPrompt, userContent, headers)
|
||||
return { content: extractOpenCodeContent(resp.data), data: resp.data };
|
||||
}
|
||||
|
||||
async function chatOpenCodeWithRetry(baseURL, model, systemPrompt, userContent, headers) {
|
||||
const maxAttempts = Number(process.env.OPENCODE_RETRY_ATTEMPTS || 3);
|
||||
let lastError;
|
||||
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
||||
try {
|
||||
return await chatOpenCode(baseURL, model, systemPrompt, userContent, headers);
|
||||
} catch (e) {
|
||||
lastError = e;
|
||||
if (!isTransientOpenCodeError(e) || attempt === maxAttempts) throw e;
|
||||
const delay = Math.min(1000 * 2 ** (attempt - 1), 8000);
|
||||
line(`[LLM] OpenCode 暫時性錯誤,${delay}ms 後重試 (${attempt}/${maxAttempts}): ${formatOpenCodeError(e)}`);
|
||||
await sleep(delay);
|
||||
}
|
||||
}
|
||||
throw lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 對 OpenCode server 送出一次對話請求並回傳模型純文字回應。
|
||||
*
|
||||
@@ -125,7 +151,7 @@ export async function chat(systemPrompt, userContent) {
|
||||
const headers = { 'Content-Type': 'application/json' };
|
||||
|
||||
try {
|
||||
const { content, data } = await chatOpenCode(baseURL, model, systemPrompt, userContent, headers);
|
||||
const { content, data } = await chatOpenCodeWithRetry(baseURL, model, systemPrompt, userContent, headers);
|
||||
recordUsage(data);
|
||||
return content;
|
||||
} catch (e) {
|
||||
|
||||
+6
-5
@@ -120,15 +120,16 @@ async function main() {
|
||||
} catch (e) {
|
||||
warn(`角色介紹 comment 發布失敗(繼續執行): ${e.message}`);
|
||||
}
|
||||
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') {
|
||||
for (const role of roles) {
|
||||
try {
|
||||
const findings = await analyzeWithRole(role, diff);
|
||||
fulfilledAnalyses += 1;
|
||||
newFindings.push(...analyses[i].value);
|
||||
newFindings.push(...findings);
|
||||
} catch (e) {
|
||||
warn(`[${role.name}] 分析失敗(跳過): ${e.message}`);
|
||||
}
|
||||
else warn(`[${roles[i].name}] 分析失敗(跳過): ${analyses[i].reason?.message}`);
|
||||
}
|
||||
if (fulfilledAnalyses === 0) {
|
||||
result(false, '所有角色分析皆失敗,終止流程以避免誤判為審查通過');
|
||||
|
||||
+23
-1
@@ -4,7 +4,7 @@ import axios from 'axios';
|
||||
import { extractBalancedJSON, extractJSONText } from '../llm.js';
|
||||
|
||||
const ENV_KEYS = [
|
||||
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER',
|
||||
'OPENCODE_BASE_URL', 'OPENCODE_MODEL', 'OPENCODE_PROVIDER', 'OPENCODE_RETRY_ATTEMPTS',
|
||||
];
|
||||
|
||||
let saved = {};
|
||||
@@ -87,6 +87,7 @@ describe('chat - OpenCode', async () => {
|
||||
|
||||
it('throws an error when OpenCode fails instead of exiting the process', async () => {
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
process.env.OPENCODE_RETRY_ATTEMPTS = '1';
|
||||
mock.method(axios, 'post', async () => {
|
||||
const err = new Error('Request failed with status code 500');
|
||||
err.response = { status: 500, data: { error: 'provider overloaded' } };
|
||||
@@ -98,6 +99,27 @@ describe('chat - OpenCode', async () => {
|
||||
|
||||
assert.equal(exitMock.mock.calls.length, 0);
|
||||
});
|
||||
|
||||
it('retries transient OpenCode failures before returning content', async () => {
|
||||
process.env.OPENCODE_BASE_URL = 'http://opencode.local:4096';
|
||||
process.env.OPENCODE_RETRY_ATTEMPTS = '2';
|
||||
let messageAttempts = 0;
|
||||
mock.method(axios, 'post', async (url) => {
|
||||
if (url.endsWith('/session')) return { data: { id: 'ses_test' } };
|
||||
messageAttempts += 1;
|
||||
if (messageAttempts === 1) {
|
||||
const err = new Error('Request failed with status code 500');
|
||||
err.response = { status: 500, data: { error: 'temporary failure' } };
|
||||
throw err;
|
||||
}
|
||||
return { data: { parts: [{ type: 'text', text: 'ok after retry' }] } };
|
||||
});
|
||||
|
||||
const result = await chat('sys', 'user');
|
||||
|
||||
assert.equal(result, 'ok after retry');
|
||||
assert.equal(messageAttempts, 2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chatJSON', async () => {
|
||||
|
||||
Reference in New Issue
Block a user