- providers/base.ts 引入 ApiRequestError 与 isRetryableError,按 429/5xx/超时/网络错误/401 分类 - chatWithRetry 升级为最多 3 次指数退避(1s/2s/4s),非重试错误立即上抛;新增 setRetryBaseDelayForTest 钩子 - aiFixEngine/customFixEngine 不再吞错,修复失败原因透传为 ai-error: <原因> - 扩展 ai-empty-response/customFixEngine 测试覆盖重试与分类
65 lines
1.9 KiB
TypeScript
65 lines
1.9 KiB
TypeScript
import { AIProvider, ChatOptions, EmptyContentError, ApiRequestError } from './base';
|
|
import { t } from '../../i18n/messages';
|
|
|
|
export class ClaudeProvider extends AIProvider {
|
|
id = 'claude';
|
|
name = 'Anthropic Claude';
|
|
|
|
async chat(systemPrompt: string, userPrompt: string, options: ChatOptions): Promise<string> {
|
|
const url = `${this.baseUrl}/messages`;
|
|
|
|
const body = JSON.stringify({
|
|
model: options.model,
|
|
max_tokens: options.maxTokens,
|
|
temperature: options.temperature,
|
|
system: systemPrompt,
|
|
messages: [
|
|
{ role: 'user', content: userPrompt },
|
|
],
|
|
});
|
|
|
|
const controller = new AbortController();
|
|
const timeout = setTimeout(() => controller.abort(), options.timeoutMs);
|
|
|
|
try {
|
|
const response = await fetch(url, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
'x-api-key': this.apiKey,
|
|
'anthropic-version': '2023-06-01',
|
|
},
|
|
body,
|
|
signal: controller.signal,
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const errorText = await response.text();
|
|
if (response.status === 401) {
|
|
throw new ApiRequestError('API Key 无效,请重新设置', 401);
|
|
}
|
|
throw new ApiRequestError(`Claude API 请求失败 (${response.status}): ${errorText}`, response.status);
|
|
}
|
|
|
|
const data = await response.json() as {
|
|
content?: Array<{ text?: string }>;
|
|
stop_reason?: string;
|
|
error?: { message?: string };
|
|
};
|
|
|
|
const text = data.content?.[0]?.text;
|
|
if (text === undefined || text === null || text.trim() === '') {
|
|
const parts = [`stop_reason=${data.stop_reason ?? 'unknown'}`];
|
|
if (data.error?.message) {
|
|
parts.push(data.error.message);
|
|
}
|
|
throw new EmptyContentError(t('adapter.emptyContent', { 0: parts.join(', ') }));
|
|
}
|
|
|
|
return text;
|
|
} finally {
|
|
clearTimeout(timeout);
|
|
}
|
|
}
|
|
}
|