refactor(core): 统一配置系统,移除 config.json

- 移除 config.json,所有配置统一从 agents.json 和 providers.json 读取
- config-loader.ts 从全局目录 ~/.ai-terminal-assistant/ 加载配置
- loadConfig() 从 agentRegistry.getGlobalConfig() 获取 defaults.model
- 添加 loadVisionConfig() 支持 Vision 模型配置
- Tavily API Key 仅从环境变量读取
- UI AgentDefaultsEditor 添加 Vision 模型配置界面
- 更新相关测试
This commit is contained in:
2025-12-16 00:33:29 +08:00
parent 76b1ae1573
commit 9376887995
14 changed files with 414 additions and 643 deletions
+126 -110
View File
@@ -1,8 +1,14 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
/**
* Agent 配置加载
*
* 从 agents.json 的 defaults.model 读取全局模型配置
* 从 providers.json 读取 API Key 和 baseUrl
*/
import type { AgentConfig, ProviderType } from '../types/index.js';
import { providerRegistry, resolveApiKey } from '../provider/index.js';
import { agentRegistry } from '../agent/registry.js';
import type { AgentModelConfig } from '../agent/types.js';
/**
* 配置错误异常
@@ -21,32 +27,6 @@ export class ConfigurationError extends Error {
}
}
const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
interface StoredConfig {
provider?: ProviderType;
model?: string;
maxTokens?: number;
tavilyApiKey?: string;
/** 自定义 API 基础 URL(用于 OpenAI 兼容服务,如阿里云百炼) */
baseUrl?: string;
// Vision 配置
visionProvider?: ProviderType;
visionModel?: string;
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
visionBaseUrl?: string;
}
// Vision 配置接口
export interface VisionConfig {
provider: ProviderType;
apiKey: string;
model: string;
/** 自定义 Base URL(用于 OpenAI 兼容的 Vision 服务) */
baseUrl?: string;
}
// 默认模型配置
const DEFAULT_MODELS: Record<ProviderType, string> = {
anthropic: 'claude-sonnet-4-20250514',
@@ -54,13 +34,6 @@ const DEFAULT_MODELS: Record<ProviderType, string> = {
openai: 'gpt-4o',
};
// 默认 Vision 模型(需要支持图片理解)
const DEFAULT_VISION_MODELS: Record<ProviderType, string> = {
anthropic: 'claude-sonnet-4-20250514',
deepseek: 'deepseek-chat', // DeepSeek 暂不支持 vision,占位用
openai: 'gpt-4o',
};
// 默认系统提示词
const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手。你可以帮助用户:
- 读取和写入文件
@@ -81,116 +54,159 @@ const DEFAULT_SYSTEM_PROMPT = `你是一个运行在终端中的 AI 编程助手
当前工作目录: ${process.cwd()}
操作系统: ${process.platform}`;
// 获取原始配置(包含所有字段)
export function getConfig(): StoredConfig {
if (fs.existsSync(CONFIG_FILE)) {
try {
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
return JSON.parse(content);
} catch {
return {};
/**
* 查找第一个已配置 API Key 的 Provider
* 作为未配置 defaults.model 时的 fallback
*/
function findFirstConfiguredProvider(): ProviderType | null {
const providers: ProviderType[] = ['anthropic', 'openai', 'deepseek'];
for (const providerId of providers) {
const config = providerRegistry.getConfig(providerId);
const apiKey = resolveApiKey(config);
if (apiKey) {
return providerId;
}
}
return {};
return null;
}
// 加载配置
/**
* 加载 Agent 配置
*
* 配置来源优先级:
* 1. agents.json 的 defaults.model(用户配置的全局默认模型)
* 2. 第一个已配置 API Key 的 Providerfallback
* 3. 抛出 ConfigurationError(无可用配置)
*/
export function loadConfig(): AgentConfig {
// 从配置文件读取
const storedConfig = getConfig();
// 1. 从 AgentRegistry 获取 defaults.model 配置
const globalConfig = agentRegistry.getGlobalConfig();
const modelConfig = globalConfig?.model;
// 检查是否配置了 provider
const hasProviderConfig = !!storedConfig.provider;
const finalProvider = storedConfig.provider || 'anthropic';
// 2. 确定 provider(优先使用配置,否则使用第一个有 API Key 的 provider
let provider: ProviderType;
let model: string;
// 通过 ProviderRegistry 获取 API Key
const providerConfig = providerRegistry.getConfig(finalProvider);
const finalApiKey = resolveApiKey(providerConfig);
if (!finalApiKey) {
// 根据是否已选择 provider 给出不同的提示
const message = hasProviderConfig
? `请在 Providers 面板配置 ${finalProvider} 的 API Key`
: '请先在 Providers 面板选择并配置一个模型提供商';
throw new ConfigurationError(message, finalProvider, 'apiKey');
if (modelConfig?.provider) {
// 用户配置了 defaults.model.provider
provider = modelConfig.provider;
model = modelConfig.model || DEFAULT_MODELS[provider];
} else {
// 未配置,尝试找第一个有 API Key 的 provider
const fallbackProvider = findFirstConfiguredProvider();
if (!fallbackProvider) {
throw new ConfigurationError(
'请先在 Providers 面板配置一个模型提供商的 API Key',
'unknown',
'apiKey'
);
}
provider = fallbackProvider;
model = DEFAULT_MODELS[provider];
}
// 确定模型
const finalModel = storedConfig.model || DEFAULT_MODELS[finalProvider];
// 3. 从 ProviderRegistry 获取 API Key 和 baseUrl
const providerConfig = providerRegistry.getConfig(provider);
const apiKey = resolveApiKey(providerConfig);
// 确定 baseUrl
const finalBaseUrl = storedConfig.baseUrl || providerConfig?.baseUrl;
if (!apiKey) {
throw new ConfigurationError(
`请在 Providers 面板配置 ${provider} 的 API Key`,
provider,
'apiKey'
);
}
// 获取模型的 contextWindow(从 ProviderRegistry 查询)
const modelInfo = providerRegistry.getModelInfo(finalProvider, finalModel);
// 4. 获取模型的 contextWindow
const modelInfo = providerRegistry.getModelInfo(provider, model);
const contextWindow = modelInfo?.contextWindow;
// 5. 获取 maxTokens(从 modelConfig 或默认值)
const maxTokens = modelConfig?.maxTokens || 4096;
return {
provider: finalProvider,
apiKey: finalApiKey,
model: finalModel,
maxTokens: storedConfig.maxTokens || 4096,
provider,
apiKey,
model,
maxTokens,
systemPrompt: DEFAULT_SYSTEM_PROMPT,
baseUrl: finalBaseUrl,
baseUrl: providerConfig?.baseUrl,
contextWindow,
};
}
// Vision 配置接口
export interface VisionConfig {
provider: ProviderType;
apiKey: string;
model: string;
baseUrl?: string;
}
/**
* 加载 Vision 配置
*
* 从 agents.json 的 defaults.vision 读取配置
* Vision 用于图片理解,当主模型不支持 vision 时使用
* 通过 ProviderRegistry 获取 API Key
*/
export function loadVisionConfig(): VisionConfig | null {
// 从配置文件读取
const storedConfig = getConfig();
// 从 AgentRegistry 获取 defaults.vision 配置
const globalConfig = agentRegistry.getGlobalConfig();
const visionConfig = globalConfig?.vision;
// 确定 vision provider(默认使用 anthropic,因为 Claude 支持 vision
const finalProvider = storedConfig.visionProvider || 'anthropic';
// 通过 ProviderRegistry 获取 API Key
const providerConfig = providerRegistry.getConfig(finalProvider);
const finalApiKey = resolveApiKey(providerConfig);
// 如果没有 API Key,返回 null
if (!finalApiKey) {
if (!visionConfig?.provider) {
// 未配置 vision,返回 null
return null;
}
// 确定模型
const finalModel = storedConfig.visionModel || DEFAULT_VISION_MODELS[finalProvider];
// 从 ProviderRegistry 获取 API Key
const providerConfig = providerRegistry.getConfig(visionConfig.provider);
const apiKey = resolveApiKey(providerConfig);
// 确定 baseUrl
const finalBaseUrl = storedConfig.visionBaseUrl || providerConfig?.baseUrl;
if (!apiKey) {
// 没有 API Key,返回 null
return null;
}
// 确定模型(使用配置的或默认值)
const model = visionConfig.model || DEFAULT_MODELS[visionConfig.provider];
return {
provider: finalProvider,
apiKey: finalApiKey,
model: finalModel,
baseUrl: finalBaseUrl,
provider: visionConfig.provider,
apiKey,
model,
baseUrl: providerConfig?.baseUrl,
};
}
// 保存配置
export function saveConfig(config: Partial<StoredConfig>): void {
// 确保目录存在
if (!fs.existsSync(CONFIG_DIR)) {
fs.mkdirSync(CONFIG_DIR, { recursive: true });
/**
* 从 AgentModelConfig 构建完整配置
* 用于 Task 工具动态构建 Agent 配置
*/
export function buildConfigFromModelConfig(modelConfig: AgentModelConfig): AgentConfig | null {
if (!modelConfig.provider) {
return null;
}
// 读取现有配置
let existingConfig: StoredConfig = {};
if (fs.existsSync(CONFIG_FILE)) {
try {
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
existingConfig = JSON.parse(content);
} catch {
// 忽略
}
const providerConfig = providerRegistry.getConfig(modelConfig.provider);
const apiKey = resolveApiKey(providerConfig);
if (!apiKey) {
return null;
}
// 合并并保存
const newConfig = { ...existingConfig, ...config };
fs.writeFileSync(CONFIG_FILE, JSON.stringify(newConfig, null, 2));
const model = modelConfig.model || DEFAULT_MODELS[modelConfig.provider];
const modelInfo = providerRegistry.getModelInfo(modelConfig.provider, model);
return {
provider: modelConfig.provider,
apiKey,
model,
maxTokens: modelConfig.maxTokens || 4096,
systemPrompt: DEFAULT_SYSTEM_PROMPT,
baseUrl: providerConfig?.baseUrl,
contextWindow: modelInfo?.contextWindow,
};
}