Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9e70bb2245 | ||
|
|
fa9be791ee |
+21
-1
@@ -39,10 +39,30 @@ function cliArgs({ provider, model, promptFile = null, prompt = null }) {
|
|||||||
throw new Error(`不支援的 AI 助理 CLI: ${provider}`);
|
throw new Error(`不支援的 AI 助理 CLI: ${provider}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 從 CLI 輸出中抽出「真正有意義的錯誤」。
|
||||||
|
*
|
||||||
|
* 像 codex 這類 CLI 會先印出一大段 banner(workdir/model/...)與回顯的 prompt,
|
||||||
|
* 真正的失敗原因(例如 401、token 失效、額度不足)通常落在**尾端**。直接取前段
|
||||||
|
* 會被 banner/prompt 洗掉,因此改為:先抽出看起來像錯誤的行;抽不到再退取尾段。
|
||||||
|
*
|
||||||
|
* @param {string} raw - CLI 的原始輸出(stderr 或 stdout)。
|
||||||
|
* @param {number} [limit=1000] - 回傳字串長度上限。
|
||||||
|
* @returns {string} 最能說明失敗原因的片段。
|
||||||
|
*/
|
||||||
|
export function extractMeaningfulError(raw, limit = 1000) {
|
||||||
|
const text = String(raw || '').trim();
|
||||||
|
const errorLines = text
|
||||||
|
.split('\n')
|
||||||
|
.filter(l => /\bERROR\b|error:|unauthorized|invalidated|revoked|forbidden|\b40[13]\b|rate.?limit|quota|insufficient/i.test(l));
|
||||||
|
const picked = (errorLines.length ? errorLines.join('\n') : text).trim();
|
||||||
|
return picked.length > limit ? picked.slice(-limit) : picked;
|
||||||
|
}
|
||||||
|
|
||||||
function summarizeCliError(e) {
|
function summarizeCliError(e) {
|
||||||
const stderr = String(e.stderr || '').trim();
|
const stderr = String(e.stderr || '').trim();
|
||||||
const stdout = String(e.stdout || '').trim();
|
const stdout = String(e.stdout || '').trim();
|
||||||
return (stderr || stdout || e.message || String(e)).slice(0, 1000);
|
return extractMeaningfulError(stderr || stdout || e.message || String(e));
|
||||||
}
|
}
|
||||||
|
|
||||||
async function runAssistantCLI({ provider, command, model }, prompt) {
|
async function runAssistantCLI({ provider, command, model }, prompt) {
|
||||||
|
|||||||
+37
-1
@@ -3,7 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import { mkdtemp, writeFile, chmod, rm, readFile } from 'fs/promises';
|
import { mkdtemp, writeFile, chmod, rm, readFile } from 'fs/promises';
|
||||||
import { tmpdir } from 'os';
|
import { tmpdir } from 'os';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { extractBalancedJSON, extractJSONText } from '../llm.js';
|
import { extractBalancedJSON, extractJSONText, extractMeaningfulError } from '../llm.js';
|
||||||
|
|
||||||
const ENV_KEYS = [
|
const ENV_KEYS = [
|
||||||
'AI_ASSISTANT_CLI', 'MODEL', 'OPENCODE_MODEL', 'PATH', 'AI_ASSISTANT_TIMEOUT_MS', 'AI_ASSISTANT_MAX_BUFFER',
|
'AI_ASSISTANT_CLI', 'MODEL', 'OPENCODE_MODEL', 'PATH', 'AI_ASSISTANT_TIMEOUT_MS', 'AI_ASSISTANT_MAX_BUFFER',
|
||||||
@@ -237,3 +237,39 @@ describe('extractJSONText', () => {
|
|||||||
assert.equal(result, 'not json at all');
|
assert.equal(result, 'not json at all');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('extractMeaningfulError', () => {
|
||||||
|
it('抽出尾端真正的錯誤,而非開頭的 codex banner/回顯 prompt', () => {
|
||||||
|
const raw = [
|
||||||
|
'OpenAI Codex v0.142.5',
|
||||||
|
'--------',
|
||||||
|
'workdir: /workspace/actions/ai-code-review',
|
||||||
|
'model: gpt-5.4-mini',
|
||||||
|
'reasoning effort: none',
|
||||||
|
'--------',
|
||||||
|
'user',
|
||||||
|
'請依照以下系統指示處理使用者內容,並只輸出要求的最終結果。',
|
||||||
|
'ERROR codex_api::endpoint::responses_websocket: failed to connect to websocket: HTTP error: 401 Unauthorized',
|
||||||
|
'ERROR: Your access token could not be refreshed because your refresh token was revoked. Please log out and sign in again.',
|
||||||
|
].join('\n');
|
||||||
|
|
||||||
|
const result = extractMeaningfulError(raw);
|
||||||
|
|
||||||
|
assert.match(result, /401 Unauthorized/);
|
||||||
|
assert.match(result, /refresh token was revoked/);
|
||||||
|
assert.doesNotMatch(result, /workdir:/);
|
||||||
|
assert.doesNotMatch(result, /請依照以下系統指示/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('抽不到錯誤行時退取尾段(不取開頭)', () => {
|
||||||
|
const raw = 'A'.repeat(1200) + '\nTAIL-CONTENT';
|
||||||
|
const result = extractMeaningfulError(raw, 100);
|
||||||
|
assert.ok(result.length <= 100);
|
||||||
|
assert.match(result, /TAIL-CONTENT$/);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('容錯處理空輸入', () => {
|
||||||
|
assert.equal(extractMeaningfulError(''), '');
|
||||||
|
assert.equal(extractMeaningfulError(null), '');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user