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:
+126
-110
@@ -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 的 Provider(fallback)
|
||||
* 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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user