feat: AI 调用稳定性增强(可重试错误分类 + 指数退避 + 修复链路错误透传)

- providers/base.ts 引入 ApiRequestError 与 isRetryableError,按 429/5xx/超时/网络错误/401 分类
- chatWithRetry 升级为最多 3 次指数退避(1s/2s/4s),非重试错误立即上抛;新增 setRetryBaseDelayForTest 钩子
- aiFixEngine/customFixEngine 不再吞错,修复失败原因透传为 ai-error: <原因>
- 扩展 ai-empty-response/customFixEngine 测试覆盖重试与分类
This commit is contained in:
范智鹏
2026-08-31 21:57:54 +08:00
parent 458647d82e
commit d6b8a3c897
11 changed files with 287 additions and 55 deletions
+23 -7
View File
@@ -1,5 +1,5 @@
import * as vscode from 'vscode';
import { EmptyContentError } from './providers/base';
import { isRetryableError } from './providers/base';
import type { AIProvider, ChatOptions } from './providers/base';
import { createProvider } from './factory';
import { getAIProvider, getAIModel, getAIBaseUrl, getAITemperature, getAITimeout, getAIMaxTokens, getAIOutputLanguage, getApiKey } from '../config';
@@ -83,20 +83,36 @@ export function parseJsonResponse(raw: string): object {
}
}
const RETRY_MAX_ATTEMPTS = 3;
const RETRY_BASE_DELAY_MS = 1000;
let retryBaseDelayMs = RETRY_BASE_DELAY_MS;
export function setRetryBaseDelayForTest(ms: number): void {
retryBaseDelayMs = ms;
}
function sleep(ms: number): Promise<void> {
return new Promise(resolve => setTimeout(resolve, ms));
}
export async function chatWithRetry(
provider: AIProvider,
systemPrompt: string,
userPrompt: string,
options: ChatOptions
): Promise<string> {
try {
return await provider.chat(systemPrompt, userPrompt, options);
} catch (err) {
if (err instanceof EmptyContentError) {
return provider.chat(systemPrompt, userPrompt, options);
for (let attempt = 0; attempt <= RETRY_MAX_ATTEMPTS; attempt++) {
try {
return await provider.chat(systemPrompt, userPrompt, options);
} catch (err) {
if (attempt === RETRY_MAX_ATTEMPTS || !isRetryableError(err)) {
throw err;
}
await sleep(retryBaseDelayMs * 2 ** attempt);
}
throw err;
}
throw new Error('unreachable');
}
function buildCustomRuleSystemPrompt(): string {