/** * Provider Adapter — 基类 * * 所有 Provider 适配器共享的基类逻辑: * - 请求超时处理 * - 错误映射到 MetonaError * - 流式事件标准化 */ import type { IMetonaProviderAdapter, AdapterConfig, MetonaRequest, MetonaResponse, MetonaStreamEvent, MetonaError, } from '../types'; import { MetonaErrorCode } from '../types'; import type { MetonaModelInfo } from '../types/metona-adapter'; export abstract class BaseAdapter implements IMetonaProviderAdapter { // H-2 修复: provider → providerId(规范要求) abstract readonly providerId: string; abstract readonly supportedModels: string[]; abstract readonly supportsToolCalling: boolean; abstract readonly supportsThinking: boolean; /** * C-2 修复: 外部注入的 AbortSignal(来自 Engine 的 abortController) * * 用户点击中断时,Engine 调用 abortController.abort(),此信号触发后, * 正在进行的 fetch 会被立即中断,避免资源泄漏。 */ private externalAbortSignal: AbortSignal | undefined; constructor(protected config: AdapterConfig) {} // H-2 修复: chat → send(规范要求) abstract send(request: MetonaRequest): Promise; // H-2 修复: chatStream → sendStream(规范要求) abstract sendStream(request: MetonaRequest): AsyncIterable; /** * H-2 修复: 获取上下文窗口大小(规范要求) * * 默认实现从 config 读取 contextWindow,子类可覆盖以支持动态查询。 * Engine 用此值估算上下文使用率,决定是否触发压缩。 * * @returns 上下文窗口大小(token 数) */ getContextWindow(): number { // 优先使用 AdapterConfig.contextWindow(如果存在) const ctx = (this.config as AdapterConfig & { contextWindow?: number }).contextWindow; if (typeof ctx === 'number' && ctx > 0) return ctx; // 默认 1M(保守值,子类应覆盖) return 1_000_000; } /** * C-2 修复: 注入外部 AbortSignal * Engine 在调用 send/sendStream 前调用此方法,关联 abortController */ setAbortSignal(signal: AbortSignal | undefined): void { this.externalAbortSignal = signal; } /** * C-2 修复: 合并外部 abort signal 和 timeout signal * * 使用 AbortSignal.any() 合并两个信号,任一触发都会中断 fetch: * - timeout signal:防止请求挂起 * - external abort signal:用户主动中断 * * @param timeoutMs 超时时间(毫秒) * @returns 合并后的 AbortSignal */ protected getFetchSignal(timeoutMs: number): AbortSignal { const timeoutSignal = AbortSignal.timeout(timeoutMs); // 如果没有外部信号,直接使用 timeout signal if (!this.externalAbortSignal) { return timeoutSignal; } // 如果外部信号已经 abort,直接返回它 if (this.externalAbortSignal.aborted) { return this.externalAbortSignal; } // 合并两个信号 — 任一触发都会 abort // Node.js 20+ / Electron 35+ 支持 AbortSignal.any() return AbortSignal.any([timeoutSignal, this.externalAbortSignal]); } async healthCheck(): Promise { try { await this.listModels(); return true; } catch { return false; } } /** * H-2 修复: 返回 MetonaModelInfo[](规范要求) * * 默认实现将 supportedModels 映射为 MetonaModelInfo[], * 子类可覆盖以从 API 获取完整元信息。 */ async listModels(): Promise { return this.supportedModels.map((id) => ({ id })); } /** * P1-4 修复: 构造带 HTTP status 属性的 Error 并抛出 * engine.isRetryableError 依赖 error.status 判断是否可重试(429/5xx) */ protected async throwHttpError(response: Response, context: string): Promise { let errorBody = ''; try { errorBody = await response.text(); } catch { /* body 可能已消费或为 null */ } const error = new Error(`${context}: ${response.status} ${response.statusText}${errorBody ? ` - ${errorBody}` : ''}`); (error as Error & { status: number }).status = response.status; throw error; } /** * 将原生错误映射为 MetonaError */ protected mapError(error: unknown): MetonaError { if (error instanceof Error) { const msg = error.message.toLowerCase(); if (msg.includes('timeout') || msg.includes('ETIMEDOUT')) { return { code: MetonaErrorCode.NETWORK_TIMEOUT, message: error.message, provider: this.providerId, retryable: true, retryAfterMs: 3000, }; } if (msg.includes('ECONNREFUSED') || msg.includes('ENOTFOUND') || msg.includes('ECONNRESET')) { return { code: MetonaErrorCode.NETWORK_ERROR, message: error.message, provider: this.providerId, retryable: true, retryAfterMs: 3000, }; } if (msg.includes('401') || msg.includes('unauthorized')) { return { code: MetonaErrorCode.AUTH_INVALID, message: 'API key 无效或已过期', provider: this.providerId, retryable: false, }; } if (msg.includes('429') || msg.includes('rate limit')) { return { code: MetonaErrorCode.RATE_LIMITED, message: '请求过于频繁,请稍后重试', provider: this.providerId, retryable: true, retryAfterMs: 5000, }; } } return { code: MetonaErrorCode.UNKNOWN, message: error instanceof Error ? error.message : 'Unknown error', provider: this.providerId, retryable: false, }; } }