feat(provider): 添加独立的 Provider 模块管理模型提供商

实现可扩展的 Provider 系统,支持动态注册自定义提供商:

Core 模块 (packages/core/src/provider/):
- types.ts: Provider 相关类型定义
- builtin/: 内置提供商 (Anthropic, OpenAI, DeepSeek)
- registry.ts: ProviderRegistry 单例类
- config.ts: 配置持久化 (~/.ai-terminal-assistant/providers.json)
- utils.ts: 连接测试等工具函数

Server API (packages/server/src/routes/providers.ts):
- GET/POST/PUT/DELETE /providers 提供商管理
- POST /providers/:id/test 连接测试
- 自定义模型管理接口

Frontend (packages/ui/):
- ProvidersPanel 组件用于管理提供商
- API client 函数和类型定义

主要功能:
- 支持动态注册 OpenAI 兼容服务 (Ollama, vLLM 等)
- 每个提供商独立的 API Key 配置
- 预设模型列表 + 自定义模型输入
- 连接测试验证
This commit is contained in:
2025-12-13 01:50:27 +08:00
parent 1d69fd876d
commit 6ec6fe2f9f
24 changed files with 2609 additions and 342 deletions
+110
View File
@@ -0,0 +1,110 @@
/**
* Provider Configuration Persistence
*
* 提供商配置持久化
*/
import { existsSync } from 'node:fs';
import { readFile, writeFile, mkdir } from 'node:fs/promises';
import { join } from 'node:path';
import { homedir } from 'node:os';
import type { ProvidersConfigFile, CustomProviderDefinition, ProviderConfig } from './types.js';
/** 配置目录名 */
const CONFIG_DIR = '.ai-terminal-assistant';
/** 配置文件名 */
const CONFIG_FILE = 'providers.json';
/**
* 获取配置目录路径
*/
export function getConfigDir(workdir?: string): string {
return workdir ? join(workdir, CONFIG_DIR) : join(homedir(), CONFIG_DIR);
}
/**
* 获取配置文件路径
*/
export function getConfigPath(workdir?: string): string {
return join(getConfigDir(workdir), CONFIG_FILE);
}
/**
* 加载提供商配置
*/
export async function loadProvidersConfig(workdir?: string): Promise<ProvidersConfigFile> {
const configPath = getConfigPath(workdir);
if (!existsSync(configPath)) {
return { providers: {}, configs: {} };
}
try {
const content = await readFile(configPath, 'utf-8');
const config = JSON.parse(content) as ProvidersConfigFile;
return {
providers: config.providers ?? {},
configs: config.configs ?? {},
};
} catch {
// 解析失败返回空配置
return { providers: {}, configs: {} };
}
}
/**
* 保存提供商配置
*/
export async function saveProvidersConfig(
config: ProvidersConfigFile,
workdir?: string
): Promise<void> {
const configDir = getConfigDir(workdir);
const configPath = getConfigPath(workdir);
// 确保目录存在
if (!existsSync(configDir)) {
await mkdir(configDir, { recursive: true });
}
await writeFile(configPath, JSON.stringify(config, null, 2), 'utf-8');
}
/**
* 获取提供商的 API Key
* 优先从环境变量获取,其次从配置获取
*/
export function resolveApiKey(config?: ProviderConfig, envVar?: string): string | undefined {
// 优先使用配置中指定的环境变量
if (config?.apiKeyEnvVar) {
const key = process.env[config.apiKeyEnvVar];
if (key) return key;
}
// 其次使用默认环境变量
if (envVar) {
const key = process.env[envVar];
if (key) return key;
}
// 最后使用直接配置的 apiKey
return config?.apiKey;
}
/**
* 合并自定义提供商定义和配置
*/
export function mergeProviderConfig(
definition: CustomProviderDefinition,
config?: ProviderConfig
): CustomProviderDefinition & { enabled: boolean } {
return {
...definition,
// 配置可以覆盖 baseUrl
baseUrl: config?.baseUrl ?? definition.baseUrl,
// 合并模型列表
models: [...(definition.models ?? []), ...(config?.customModels ?? [])],
enabled: config?.enabled ?? true,
};
}