Files
metona-ai-desktop/electron/harness/adapters/base-adapter.ts
T
thzxx e4d81d8247 feat: 升级至 v0.3.1 — 全量代码审计修复 + 安全增强
本次升级基于完整代码审查,修复 Critical/High/Medium/Low 四级共 96 项问题,
并通过返工审计修复 10 项遗留问题,tsc 双端类型检查零错误。

Critical (10/10 完成):
- C-4: command.ts 接入 shell-quote 进行 token-level 注入检测,替代原有正则匹配
  可防御 r"m" -rf /、$'rm'、$(echo rm) 等字符串拼接绕过

High (11/11 完成):
- 竞态保护、Promise.allSettled、AbortController 资源泄漏、IPC 参数校验等

Medium (55/55 完成):
- 事务保护、敏感数据脱敏、枚举校验、MUI v9 Stack prop 迁移、
  React 组件 cancelled 标志、类型收窄等

Low (20/20 完成):
- 辅助方法提取(flushToolCallBuffer/scoreAndPushMemory/tryAddColumn 等)
- nanoid 统一替代 Date.now()+Math.random()
- confirm() 替换为 MUI Dialog、useMemo 缓存、魔法数字命名化等

返工审计修复 (10/10 完成):
- L-11: LogsSettings 残留的原生 confirm()/alert() 全部替换为 MUI Dialog/Alert
- M-53: MemoryViewer handleSearch 独立 ref,修复 searching 状态卡死
- M-42: 脱敏短值(length <= 4)泄露修复
- M-47: tasks:update 补全 title/description 类型校验
- L-9: ollama.adapter 非流式路径 nanoid 统一
- M-45: audit:query limit 策略与 memory:listAll 一致化
- SettingsModal handleConfirmRemove 补全 try/catch + loadServers cleanup
- L-15: CommandPalette useMemo 补全 sessions 响应式依赖
- useAgentStream 事件类型补全 seq/timestamp 字段

新增依赖: shell-quote + @types/shell-quote
版本号: 0.3.0 -> 0.3.1
2026-07-13 22:36:58 +08:00

169 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<MetonaResponse>;
// H-2 修复: chatStream → sendStream(规范要求)
abstract sendStream(request: MetonaRequest): AsyncIterable<MetonaStreamEvent>;
/**
* 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<boolean> {
try {
await this.listModels();
return true;
} catch {
return false;
}
}
/**
* H-2 修复: 返回 MetonaModelInfo[](规范要求)
*
* 默认实现将 supportedModels 映射为 MetonaModelInfo[]
* 子类可覆盖以从 API 获取完整元信息。
*/
async listModels(): Promise<MetonaModelInfo[]> {
return this.supportedModels.map((id) => ({ id }));
}
/**
* 将原生错误映射为 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,
};
}
}