autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
753 lines (744 loc) • 32.8 kB
JavaScript
/**
* AiProvider - AI 提供商抽象基类
* 所有具体 Provider 必须实现这3个方法
*/
import { LanguageService } from '../../shared/LanguageService.js';
export class AiProvider {
_activeRequests;
_circuitCooldownMs;
_circuitFailures;
_circuitOpenedAt;
_circuitState;
_circuitThreshold;
_maxConcurrency;
_rateLimitedUntil;
_requestQueue;
apiKey;
baseUrl;
logger = null;
maxRetries;
model;
name;
timeout;
_fallbackFrom;
/**
* Token 用量回调 — 每次 API 调用后触发(包括 chat / chatWithStructuredOutput / chatWithTools)
* 由外部(如 DI 容器)注入以实现全局 token 计量。
*/
_onTokenUsage = null;
constructor(config = {}) {
this.model = config.model || '';
this.apiKey = config.apiKey || '';
this.baseUrl = config.baseUrl || '';
this.timeout = config.timeout || 300_000; // 5min
this.maxRetries = config.maxRetries || 3;
this.name = 'abstract';
// ── CircuitBreaker 状态 ──
this._circuitState = 'CLOSED'; // 'CLOSED' | 'OPEN' | 'HALF_OPEN'
this._circuitFailures = 0; // 连续失败计数
this._circuitThreshold = config.circuitThreshold || 5; // 触发熔断的连续失败次数
this._circuitOpenedAt = 0; // 熔断打开时间
this._circuitCooldownMs = 30_000; // 初始冷却 30 秒
// ── Provider 级全局并发闸门 + 429 冷却窗 ──
this._maxConcurrency = Math.max(1, Number(config.maxConcurrency || process.env.ASD_AI_MAX_CONCURRENCY || 4));
this._activeRequests = 0;
this._requestQueue = [];
this._rateLimitedUntil = 0;
}
async _acquireRequestSlot() {
if (this._activeRequests < this._maxConcurrency) {
this._activeRequests += 1;
return;
}
await new Promise((resolve) => this._requestQueue.push(() => resolve()));
this._activeRequests += 1;
}
_releaseRequestSlot() {
this._activeRequests = Math.max(0, this._activeRequests - 1);
const next = this._requestQueue.shift();
if (next) {
next();
}
}
async _waitForRateLimitWindow() {
const waitMs = (this._rateLimitedUntil || 0) - Date.now();
if (waitMs > 0) {
await new Promise((r) => setTimeout(r, waitMs));
}
}
_setRateLimitWindow(waitMs) {
const safeWait = Math.max(0, Number(waitMs) || 0);
if (safeWait <= 0) {
return;
}
const until = Date.now() + safeWait;
if (until > (this._rateLimitedUntil || 0)) {
this._rateLimitedUntil = until;
this._log?.('warn', `[RateLimit] ${this.name} enters cooldown ${Math.round(safeWait / 1000)}s (global)`);
}
}
/**
* 对话 - 发送 prompt + context,返回文本响应
* @param context {history: [], temperature, maxTokens}
*/
async chat(prompt, context = {}) {
throw new Error(`${this.name}.chat() not implemented`);
}
/**
* 从 API 原始响应中提取 token 用量并触发回调。
* 子类在 chat() / chatWithStructuredOutput() 中调用。
*/
_emitTokenUsage(usage, source) {
if (!usage || !this._onTokenUsage) {
return;
}
const total = (usage.inputTokens || 0) + (usage.outputTokens || 0);
if (total === 0) {
return;
}
try {
this._onTokenUsage({ ...usage, source });
}
catch {
/* token tracking should never break execution */
}
}
/** 摘要 - 对代码/文档生成结构化摘要 */
async summarize(code) {
throw new Error(`${this.name}.summarize() not implemented`);
}
/** 向量嵌入 - 返回浮点数组 */
async embed(text) {
throw new Error(`${this.name}.embed() not implemented`);
}
/**
* 探测 provider 是否可用(轻量级 API 调用验证连接性)
* 子类可覆盖实现更具体的探测逻辑
*/
async probe() {
const result = await this.chat('ping', { maxTokens: 16, temperature: 0 });
return !!result;
}
/** 检查是否支持 embedding */
supportsEmbedding() {
return true;
}
/**
* 是否支持原生结构化函数调用(非文本解析)
* 子类(如 GoogleGeminiProvider)覆盖返回 true
*/
get supportsNativeToolCalling() {
return false;
}
/**
* 带工具声明的结构化对话 — 原生函数调用 API
*
* 支持原生函数调用的 Provider(Gemini / OpenAI / Claude)覆盖此方法,
* 返回结构化 functionCall 而非文本,AgentRuntime 据此跳过正则解析。
*
* 默认实现降级为 chat(),由 AgentRuntime 进行文本解析。
*
* 统一消息格式 (Provider-Agnostic):
* - { role: 'user', content: 'text' }
* - { role: 'assistant', content: 'text or null', toolCalls: [{id, name, args}] }
* - { role: 'tool', toolCallId: 'id', name: 'tool_name', content: 'result string' }
*
* @param prompt 用户消息(仅在 messages 为空时使用)
* @param opts.messages 统一格式消息历史
* @param opts.toolSchemas [{name, description, parameters}]
* @param opts.toolChoice 'auto' | 'required' | 'none'
* @param [opts.systemPrompt] 系统指令
* @returns >|null}>}
*/
async chatWithTools(prompt, opts = {}) {
// 默认降级: 忽略 tools/toolChoice,走纯文本 chat()
const messages = (opts.messages || []);
const history = messages
.filter((m) => m.role === 'user' || m.role === 'assistant')
.map((m) => ({
role: m.role,
content: m.content || '',
}));
const text = await this.chat(prompt, {
history,
systemPrompt: opts.systemPrompt,
temperature: opts.temperature,
maxTokens: opts.maxTokens,
});
return { text, functionCalls: null };
}
/**
* Structured Output — 请求 AI 返回严格 JSON 格式响应
*
* 子类覆盖以利用原生 JSON mode:
* - Gemini: responseMimeType: 'application/json' + responseSchema
* - OpenAI: response_format: { type: 'json_object' }
* - Claude: 无原生支持,使用默认实现 (chat + extractJSON)
*
* @param prompt 完整提示词(应包含返回 JSON 的指令)
* @param [opts.schema] JSON Schema(Gemini/OpenAI 的 structured output 用)
* @param [opts.openChar='{'] extractJSON 边界起始符(fallback 用)
* @param [opts.closeChar='}'] extractJSON 边界终止符
* @param [opts.systemPrompt] 可选系统指令
* @returns 解析后的 JSON 对象/数组,解析失败返回 null
*/
async chatWithStructuredOutput(prompt, opts = {}) {
const response = await this.chat(prompt, {
temperature: opts.temperature ?? 0.3,
maxTokens: opts.maxTokens ?? 32768,
systemPrompt: opts.systemPrompt,
});
if (!response || response.trim().length === 0) {
return null;
}
const openChar = opts.openChar || '{';
const closeChar = opts.closeChar || '}';
return this.extractJSON(response, openChar, closeChar);
}
/** 内部日志辅助(子类可通过 this.logger 覆盖) */
_log(level, message) {
try {
if (this.logger && typeof this.logger[level] === 'function') {
this.logger[level](message);
}
else {
}
}
catch {
/* best effort */
}
}
/**
* 根据用户语言偏好生成输出语言指令
* @param [lang] 语言代码,如 'zh', 'en'
* @returns 语言指令段落(为空则返回空字符串)
*/
_buildLangInstruction(lang) {
if (!lang || lang === 'en') {
return '';
}
if (lang === 'zh') {
return `
# 输出语言要求
用户使用中文,请用**中文**书写以下字段的内容:
- title(标题)
- description(描述)
- doClause(做什么)
- dontClause(不要做什么)
- whenClause(适用场景)
- topicHint(分组标签)
- content.markdown(使用指南)
- content.rationale(设计原因)
- reasoning.whyStandard(为什么是最佳实践)
- aiInsight(核心洞察)
- constraints 中的 preconditions / sideEffects / boundaries
以下字段保持英文或代码原文,不要翻译:
- trigger(@快捷方式)
- content.pattern(源代码)
- coreCode(代码骨架)
- headers(import 语句)
- tags(搜索关键词,可中英混合)
- kind / knowledgeType / complexity / scope / category / language
`;
}
// 其他语言通用指令
return `\n# Output Language\nThe user's preferred language is "${lang}". Write all human-readable text fields (title, description, doClause, dontClause, whenClause, topicHint, content.markdown, content.rationale, reasoning.whyStandard, aiInsight, constraints text) in "${lang}". Keep code fields (trigger, content.pattern, coreCode, headers, tags) in their original language.\n`;
}
/** 根据文件扩展名检测语言特征,返回提示词适配参数 */
_detectLanguageProfile(filesContent) {
const extCounts = {};
for (const f of filesContent) {
const ext = (f.name || '').split('.').pop()?.toLowerCase() || '';
extCounts[ext] = (extCounts[ext] || 0) + 1;
}
// 使用 LanguageService 推断主语言
const primaryLang = LanguageService.detectPrimary(extCounts);
const dominant = Object.entries(extCounts).sort((a, b) => b[1] - a[1])[0]?.[0] || '';
// iOS/macOS (Swift / Objective-C)
if (primaryLang === 'swift' || primaryLang === 'objectivec') {
return {
primaryLanguage: primaryLang,
role: 'Senior iOS/macOS Architect',
patternExamples: 'how to set up a ViewController, configure a TableView with delegate/datasource, build a login UI, handle network responses',
extractionExamples: `Examples of good extractions:
- Complete \`init\` method with all tabBarItem/navigationItem configuration
- Complete \`viewDidLoad\` with all setup calls
- Complete \`setupUI\` method with subview creation and layout
- Complete UITableViewDataSource implementation
- Complete action handler method (e.g. loginButtonTapped)`,
categories: 'View | Service | Tool | Model | Network | Storage | UI | Utility',
};
}
// JavaScript / TypeScript
if (primaryLang === 'javascript' || primaryLang === 'typescript') {
return {
primaryLanguage: primaryLang,
role: 'Senior Software Engineer',
patternExamples: 'Express/Koa middleware, React component patterns, service class with dependency injection, data processing pipeline, error handling wrapper, factory/strategy patterns',
extractionExamples: `Examples of good extractions:
- Complete class with constructor and key methods
- Express route handler with validation and error handling
- Utility function with edge case handling
- React component with hooks and event handlers
- Service method with retries and fallback logic`,
categories: 'Service | Utility | Middleware | Component | Model | Config | Handler | Route',
};
}
// Python
if (primaryLang === 'python') {
return {
primaryLanguage: 'python',
role: 'Senior Python Engineer',
patternExamples: 'Django/Flask views, data processing with pandas, async handlers, decorator patterns, class-based services',
extractionExamples: `Examples of good extractions:
- Complete class with __init__ and key methods
- Decorator factory function
- API endpoint handler with request validation
- Data processing pipeline function
- Context manager implementation`,
categories: 'Service | Utility | Model | View | Handler | Middleware | Config | Pipeline',
};
}
// Go
if (primaryLang === 'go') {
return {
primaryLanguage: 'go',
role: 'Senior Go Engineer',
patternExamples: 'HTTP handler with middleware, goroutine patterns, interface implementations, struct methods with error handling',
extractionExamples: `Examples of good extractions:
- Complete struct with constructor and methods
- HTTP handler function with error propagation
- Middleware function with context usage
- Interface implementation with all required methods`,
categories: 'Service | Handler | Middleware | Model | Utility | Repository | Config',
};
}
// Kotlin / Java
if (primaryLang === 'kotlin' || primaryLang === 'java') {
return {
primaryLanguage: primaryLang,
role: 'Senior Android/Backend Engineer',
patternExamples: 'Activity/Fragment lifecycle, repository pattern, ViewModel with LiveData, Retrofit service, dependency injection setup',
extractionExamples: `Examples of good extractions:
- Complete class with constructor and key methods
- Repository with CRUD operations
- ViewModel with state management
- API service interface definition
- Custom view with measurement and drawing`,
categories: 'View | Service | Repository | Model | Network | Storage | UI | Utility',
};
}
// Rust
if (primaryLang === 'rust') {
return {
primaryLanguage: 'rust',
role: 'Senior Rust Engineer',
patternExamples: 'trait implementations, error handling with Result, async functions, builder patterns, iterator chains',
extractionExamples: `Examples of good extractions:
- Complete impl block with key methods
- Trait implementation with all required methods
- Error type definition with From implementations
- Builder pattern struct and methods
- Async function with proper error handling`,
categories: 'Service | Trait | Model | Handler | Utility | Config | Error | Pipeline',
};
}
// Vue
if (dominant === 'vue') {
return {
primaryLanguage: 'vue',
role: 'Senior Frontend Engineer',
patternExamples: 'Vue component with composition API, composable functions, Vuex/Pinia store modules, router guards',
extractionExamples: `Examples of good extractions:
- Complete Vue component with setup/template
- Composable function with reactive state
- Store module with actions and getters
- Custom directive implementation`,
categories: 'Component | Composable | Store | Directive | Service | Utility | Config',
};
}
// Ruby
if (primaryLang === 'ruby') {
return {
primaryLanguage: 'ruby',
role: 'Senior Ruby Engineer',
patternExamples: 'Rails controller actions, model concerns, service objects, background jobs, API serializers',
extractionExamples: `Examples of good extractions:
- Complete controller with CRUD actions
- Service object with call method
- Model with validations and scopes
- Concern module with included block`,
categories: 'Controller | Service | Model | Concern | Job | Serializer | Utility | Config',
};
}
// Default / mixed
return {
primaryLanguage: dominant || 'unknown',
role: 'Senior Software Engineer',
patternExamples: 'design patterns, service abstractions, data flow handling, error management, configuration setup',
extractionExamples: `Examples of good extractions:
- Complete class/function with full implementation
- Service method with error handling and retries
- Configuration setup with all options
- Data processing pipeline`,
categories: 'Service | Utility | Model | Handler | Config | Component | Pipeline',
};
}
/**
* AI 语义字段补全 — 分析候选代码,填补缺失的语义字段
* @param candidates 候选对象数组,每项至少含 {code, language, title?}
* @returns enriched 候选数组(仅含补全的字段)
*/
async enrichCandidates(candidates, options = {}) {
const prompt = this._buildEnrichPrompt(candidates, options);
const parsed = await this.chatWithStructuredOutput(prompt, {
openChar: '[',
closeChar: ']',
temperature: 0.3,
});
return Array.isArray(parsed) ? parsed : [];
}
/** 构建 enrichCandidates 提示词 */
_buildEnrichPrompt(candidates, options = {}) {
const items = candidates
.map((c, i) => {
const existing = [];
if (c.rationale) {
existing.push(`rationale: ${c.rationale}`);
}
if (c.knowledgeType) {
existing.push(`knowledgeType: ${c.knowledgeType}`);
}
if (c.complexity) {
existing.push(`complexity: ${c.complexity}`);
}
if (c.scope) {
existing.push(`scope: ${c.scope}`);
}
if (c.steps?.length) {
existing.push(`steps: [${c.steps.length} steps already]`);
}
if (c.constraints?.preconditions?.length) {
existing.push(`preconditions: [${c.constraints.preconditions.length} items]`);
}
const existingStr = existing.length > 0
? `\nAlready filled: ${existing.join(', ')}`
: '\nNo semantic fields filled yet.';
return `--- CANDIDATE #${i + 1} ---
Title: ${c.title || '(untitled)'}
Language: ${c.language || 'unknown'}
Category: ${c.category || ''}
Description: ${c.description || c.summary || ''}
${existingStr}
Code:
${(c.code || '').substring(0, 2000)}`;
})
.join('\n\n');
return `# Role
You are a Senior Software Architect performing deep semantic analysis on code candidates.
# Goal
For each candidate below, analyze the code and fill in MISSING semantic fields only.
Do NOT overwrite fields that are already filled (listed under "Already filled").
# Fields to Fill (only if missing)
1. **rationale** (string): Why this pattern exists; what design intent or problem it solves. 2-3 sentences.
2. **knowledgeType** (string): One of: "code-standard", "code-pattern", "architecture", "best-practice", "code-relation", "inheritance", "call-chain", "data-flow", "module-dependency", "boundary-constraint", "code-style", "solution", "anti-pattern".
3. **complexity** (string): "beginner" | "intermediate" | "advanced". Evaluate usage difficulty.
4. **scope** (string): "universal" (reusable anywhere) | "project-specific" (specific to this project) | "target-specific" (specific to one module/target).
5. **steps** (array): Implementation steps. Each: { "title": "Step N title", "description": "What to do", "code": "optional code" }.
6. **constraints** (object): { "preconditions": ["iOS 15+", "需先配置 X", ...], "boundaries": ["Cannot be used with Y"], "sideEffects": ["Modifies global state"] }.
# Output Schema
Return a JSON array with one object per candidate. Each object contains ONLY the fields that were missing and you have now filled.
Include an "index" field (0-based) to match each result to its candidate.
Example:
[
{ "index": 0, "rationale": "...", "steps": [...], "constraints": { "preconditions": [...] } },
{ "index": 1, "knowledgeType": "architecture", "complexity": "advanced" }
]
Return ONLY a JSON array. No markdown, no explanation.
${this._buildLangInstruction(options.lang)}
# Candidates
${items}`;
}
// ─── 网络 / 代理 ────────────────────────────
/**
* 解析当前 Provider 应使用的代理 URL。
* 优先级(从高到低):
* 1. Provider 专属: ASD_{PROVIDER}_PROXY_HTTPS / ASD_{PROVIDER}_PROXY_HTTP
* 2. 全局 ASD 专属: ASD_AI_PROXY
* 3. 系统通用: HTTPS_PROXY / HTTP_PROXY / ALL_PROXY
*
* Provider 名称映射: google-gemini → GOOGLE, openai → OPENAI, claude → CLAUDE, deepseek → DEEPSEEK
*/
_resolveProxyUrl() {
// Provider-specific vars: ASD_GOOGLE_PROXY_HTTPS, ASD_OPENAI_PROXY_HTTPS, etc.
const tag = (this.name || '')
.replace(/-gemini$/, '') // google-gemini → google
.replace(/-/g, '_') // 其他连字符 → 下划线
.toUpperCase(); // google → GOOGLE
if (tag) {
const specific = process.env[`ASD_${tag}_PROXY_HTTPS`] || process.env[`ASD_${tag}_PROXY_HTTP`];
if (specific) {
return specific;
}
}
return (process.env.ASD_AI_PROXY ||
process.env.HTTPS_PROXY ||
process.env.https_proxy ||
process.env.HTTP_PROXY ||
process.env.http_proxy ||
process.env.ALL_PROXY ||
process.env.all_proxy ||
'');
}
/**
* 代理感知的 fetch — 自动检测代理并使用 undici ProxyAgent。
* 子类的 _post() 应调用此方法替代全局 fetch()。
*/
async _fetch(url, options = {}) {
const proxyUrl = this._resolveProxyUrl();
if (proxyUrl) {
try {
const undici = await import('undici');
options.dispatcher = new undici.ProxyAgent(proxyUrl);
return await undici.fetch(url, options);
}
catch {
// undici 不可用,fallback 到全局 fetch
}
}
return globalThis.fetch(url, options);
}
// ─── 工具方法 ─────────────────────────────
/**
* 从 LLM 响应提取 JSON (extractJSON kept below)
* 支持截断修复:当 AI 输出被 token 限制截断时,尝试关闭未完成的 JSON 结构
*/
extractJSON(text, openChar = '{', closeChar = '}') {
if (!text) {
return null;
}
// 去除 markdown 代码块
const cleaned = text.replace(/```(?:json)?\s*/gi, '').replace(/```/g, '');
const start = cleaned.indexOf(openChar);
if (start === -1) {
return null;
}
const end = cleaned.lastIndexOf(closeChar);
// 1. 常规路径:找到完整的 JSON 边界
if (end > start) {
try {
let jsonStr = cleaned.slice(start, end + 1);
jsonStr = jsonStr.replace(/,\s*([}\]])/g, '$1');
return JSON.parse(jsonStr);
}
catch {
// 常规解析失败,尝试截断修复
}
}
// 2. 截断修复:AI 输出被 token 限制截断,尝试回收已完成的条目
if (openChar === '[') {
return this._repairTruncatedArray(cleaned.slice(start));
}
return null;
}
/**
* 修复被截断的 JSON 数组 — 回收已完成的对象
* 策略 1(主路径): 字符级解析找到最后一个完整的顶层 {...} 对象
* 策略 2(回退路径): 正则 + 渐进 JSON.parse 尝试(应对代码段中未转义引号导致 inString 追踪失效)
*/
_repairTruncatedArray(text) {
// ── 策略 1:字符级深度追踪 ──
const charResult = this._repairByCharTracking(text);
if (charResult) {
return charResult;
}
// ── 策略 2:正则回退 — 找所有 "}," 或 "}\n" 位置,从后向前逐一尝试 JSON.parse ──
const regexResult = this._repairByRegexFallback(text);
if (regexResult) {
return regexResult;
}
return null;
}
/** 字符级深度追踪修复(原逻辑,处理标准 JSON) */
_repairByCharTracking(text) {
let depth = 0;
let inString = false;
let isEscaped = false;
let lastCompleteObjEnd = -1;
for (let i = 0; i < text.length; i++) {
const ch = text[i];
if (isEscaped) {
isEscaped = false;
continue;
}
if (ch === '\\' && inString) {
isEscaped = true;
continue;
}
if (ch === '"') {
inString = !inString;
continue;
}
if (inString) {
continue;
}
if (ch === '{' || ch === '[') {
depth++;
}
else if (ch === '}' || ch === ']') {
depth--;
// depth === 1 表示回到数组顶层,刚关闭了一个完整对象
if (depth === 1 && ch === '}') {
lastCompleteObjEnd = i;
}
}
}
if (lastCompleteObjEnd === -1) {
return null;
}
return this._tryRepairAt(text, lastCompleteObjEnd);
}
/**
* 正则回退修复 — 不依赖 inString 追踪
* 寻找所有 "},\s*{" 或 "}\s*]" 边界,从后往前尝试 JSON.parse
*/
_repairByRegexFallback(text) {
// 收集所有 "}" 后跟 "," 或空白的位置(可能是对象边界)
const candidates = [];
const re = /\}[\s,]*(?=\s*[[{]|$)/g;
let m;
while ((m = re.exec(text)) !== null) {
candidates.push(m.index); // "}" 的位置
}
// 从后往前尝试
for (let i = candidates.length - 1; i >= 0; i--) {
const result = this._tryRepairAt(text, candidates[i]);
if (result) {
return result;
}
}
return null;
}
/** 在指定位置截断并尝试闭合 JSON 数组 */
_tryRepairAt(text, endPos) {
let repaired = text.slice(0, endPos + 1);
// 去掉尾逗号
repaired = repaired.replace(/,\s*$/, '');
repaired += ']';
// 修复尾逗号(对象/数组末尾多余逗号)
repaired = repaired.replace(/,\s*([}\]])/g, '$1');
try {
const result = JSON.parse(repaired);
if (Array.isArray(result) && result.length > 0) {
this._log('warn', `[extractJSON] Repaired truncated JSON array: recovered ${result.length} items from truncated response`);
return result;
}
}
catch {
/* this position didn't work, try next */
}
return null;
}
/**
* 指数退避重试 + 熔断器(受 Cline 三级错误恢复启发)
*
* 熔断器三态:
* CLOSED — 正常工作,计数连续失败
* OPEN — 连续 N 次失败,直接拒绝请求(快速失败),持续 cooldownMs
* HALF_OPEN — 冷却期后尝试一次,成功则恢复,失败则重新 OPEN
*
* 这避免了 AI 服务宕机时无意义的重试风暴。
*/
async _withRetry(fn, retries = this.maxRetries, baseDelay = 2000) {
// ── 熔断器检查 ──
if (this._circuitState === 'OPEN') {
const elapsed = Date.now() - (this._circuitOpenedAt || 0);
if (elapsed < (this._circuitCooldownMs || 30000)) {
const err = new Error(`AI 服务熔断中 (连续 ${this._circuitFailures} 次失败),${Math.ceil(((this._circuitCooldownMs || 30000) - elapsed) / 1000)}s 后恢复`);
err.code = 'CIRCUIT_OPEN';
throw err;
}
// 冷却期结束 → HALF_OPEN
this._circuitState = 'HALF_OPEN';
}
for (let attempt = 0; attempt <= retries; attempt++) {
let slotAcquired = false;
try {
await this._waitForRateLimitWindow();
await this._acquireRequestSlot();
slotAcquired = true;
const result = await fn();
// 成功 → 完全重置熔断器(包括冷却时间)
this._circuitFailures = 0;
this._circuitState = 'CLOSED';
this._circuitCooldownMs = 30_000; // 重置冷却时间
return result;
}
catch (err) {
const e = err;
// AbortError — 外部主动中止(如 PipelineStrategy hard timeout),不重试直接抛出
if (e.name === 'AbortError' || e.cause?.name === 'AbortError') {
throw e;
}
// ── 综合判断是否为可重试的网络/服务端错误 ──
const causeCode = e.cause?.code || '';
// 网络级错误:无 HTTP status,底层连接失败
const isNetworkError = !e.status &&
(e.message === 'fetch failed' ||
e.code === 'ECONNRESET' ||
causeCode === 'ECONNRESET' ||
e.code === 'ECONNREFUSED' ||
causeCode === 'ECONNREFUSED' ||
e.code === 'ENOTFOUND' ||
causeCode === 'ENOTFOUND' ||
e.code === 'ECONNABORTED' ||
causeCode === 'ECONNABORTED' ||
e.code === 'ETIMEDOUT' ||
causeCode === 'ETIMEDOUT' ||
e.code === 'UND_ERR_CONNECT_TIMEOUT' ||
causeCode === 'UND_ERR_CONNECT_TIMEOUT' ||
e.code === 'UND_ERR_SOCKET' ||
causeCode === 'UND_ERR_SOCKET');
const isRetryable = e.status === 429 || (e.status ?? 0) >= 500 || isNetworkError;
// 429:触发 provider 级冷却窗,抑制并发重试风暴
if (e.status === 429) {
const retryAfterMs = Number(e.retryAfterMs || 0);
const adaptiveCooldown = Math.max(retryAfterMs, Math.round(baseDelay * 2 ** attempt * 1.5 + Math.random() * 1000));
this._setRateLimitWindow(adaptiveCooldown);
}
// 首次失败记录详细诊断(含 cause)
if (attempt === 0 && (isNetworkError || e.cause)) {
this._log?.('warn', `[_withRetry] ${e.message} — cause: ${e.cause?.message || causeCode || 'unknown'}`);
}
if (attempt >= retries || !isRetryable) {
// 只有服务端错误 / 网络错误才累计熔断计数
// 客户端错误 (4xx 非 429) 不应触发熔断 — 那是请求本身的问题
const isServerError = isNetworkError || e.status === 429 || (e.status ?? 0) >= 500 || !e.status;
if (isServerError) {
this._circuitFailures = (this._circuitFailures || 0) + 1;
if (this._circuitFailures >= (this._circuitThreshold || 5)) {
this._circuitState = 'OPEN';
this._circuitOpenedAt = Date.now();
// 先用当前冷却值,再递增给下次: 30s → 60s → 120s(最大 5 分钟)
const cooldown = this._circuitCooldownMs || 30_000;
this._log?.('warn', `[CircuitBreaker] OPEN — ${this._circuitFailures} consecutive failures, cooldown ${cooldown / 1000}s`);
this._circuitCooldownMs = Math.min(cooldown * 2, 300_000);
}
}
throw e;
}
const delay = baseDelay * 2 ** attempt + Math.random() * 1000;
this._log?.('info', `[_withRetry] attempt ${attempt + 1} failed (${e.message}), retrying in ${Math.round(delay / 1000)}s…`);
await new Promise((r) => setTimeout(r, delay));
}
finally {
if (slotAcquired) {
this._releaseRequestSlot();
}
}
}
// Should never reach here — last iteration either returns or throws
throw new Error('_withRetry: unexpected exhaustion');
}
}
export default AiProvider;