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:
@@ -1,67 +1,39 @@
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import * as os from 'os';
|
||||
import type { AgentConfigFile } from './types.js';
|
||||
|
||||
/**
|
||||
* 配置文件搜索路径
|
||||
* 全局配置目录
|
||||
*/
|
||||
const CONFIG_PATHS = [
|
||||
'.ai-assist/agents.yaml',
|
||||
'.ai-assist/agents.yml',
|
||||
'.ai-assist/agents.json',
|
||||
'.ai-assist.yaml',
|
||||
'.ai-assist.yml',
|
||||
];
|
||||
|
||||
/**
|
||||
* 解析 YAML 内容
|
||||
*/
|
||||
async function parseYaml(content: string): Promise<unknown> {
|
||||
try {
|
||||
const yaml = await import('js-yaml');
|
||||
return yaml.load(content);
|
||||
} catch {
|
||||
console.warn('解析 YAML 配置文件失败');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
const GLOBAL_CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
|
||||
|
||||
/**
|
||||
* 加载用户自定义 Agent 配置
|
||||
* @param workdir 工作目录
|
||||
* 从 ~/.ai-terminal-assistant/agents.json 读取
|
||||
*
|
||||
* @param _workdir 工作目录(保留参数以兼容现有调用,但不再使用)
|
||||
* @returns 配置对象或 null
|
||||
*/
|
||||
export async function loadAgentConfig(workdir: string): Promise<AgentConfigFile | null> {
|
||||
for (const configPath of CONFIG_PATHS) {
|
||||
const fullPath = path.join(workdir, configPath);
|
||||
export async function loadAgentConfig(_workdir: string): Promise<AgentConfigFile | null> {
|
||||
const configPath = path.join(GLOBAL_CONFIG_DIR, 'agents.json');
|
||||
|
||||
try {
|
||||
if (!fs.existsSync(fullPath)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const content = await fs.promises.readFile(fullPath, 'utf-8');
|
||||
|
||||
let config: unknown;
|
||||
|
||||
if (configPath.endsWith('.json')) {
|
||||
config = JSON.parse(content);
|
||||
} else if (configPath.endsWith('.yaml') || configPath.endsWith('.yml')) {
|
||||
config = await parseYaml(content);
|
||||
if (!config) continue;
|
||||
} else {
|
||||
continue;
|
||||
}
|
||||
|
||||
// 验证配置格式
|
||||
if (isValidAgentConfig(config)) {
|
||||
return config;
|
||||
} else {
|
||||
console.warn(`Agent 配置格式无效: ${fullPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`加载 Agent 配置失败: ${fullPath}`, error);
|
||||
try {
|
||||
if (!fs.existsSync(configPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await fs.promises.readFile(configPath, 'utf-8');
|
||||
const config = JSON.parse(content);
|
||||
|
||||
// 验证配置格式
|
||||
if (isValidAgentConfig(config)) {
|
||||
return config;
|
||||
} else {
|
||||
console.warn(`Agent 配置格式无效: ${configPath}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn(`加载 Agent 配置失败: ${configPath}`, error);
|
||||
}
|
||||
|
||||
return null;
|
||||
@@ -92,39 +64,24 @@ function isValidAgentConfig(config: unknown): config is AgentConfigFile {
|
||||
|
||||
/**
|
||||
* 保存 Agent 配置到文件
|
||||
* @param workdir 工作目录
|
||||
* 保存到 ~/.ai-terminal-assistant/agents.json
|
||||
*
|
||||
* @param _workdir 工作目录(保留参数以兼容现有调用,但不再使用)
|
||||
* @param config 配置对象
|
||||
* @param format 文件格式
|
||||
* @param _format 文件格式(保留参数以兼容现有调用,但只使用 json)
|
||||
*/
|
||||
export async function saveAgentConfig(
|
||||
workdir: string,
|
||||
_workdir: string,
|
||||
config: AgentConfigFile,
|
||||
format: 'json' | 'yaml' = 'json'
|
||||
_format: 'json' | 'yaml' = 'json'
|
||||
): Promise<void> {
|
||||
const dir = path.join(workdir, '.ai-assist');
|
||||
|
||||
// 确保目录存在
|
||||
if (!fs.existsSync(dir)) {
|
||||
await fs.promises.mkdir(dir, { recursive: true });
|
||||
if (!fs.existsSync(GLOBAL_CONFIG_DIR)) {
|
||||
await fs.promises.mkdir(GLOBAL_CONFIG_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
const filename = format === 'json' ? 'agents.json' : 'agents.yaml';
|
||||
const fullPath = path.join(dir, filename);
|
||||
|
||||
let content: string;
|
||||
|
||||
if (format === 'json') {
|
||||
content = JSON.stringify(config, null, 2);
|
||||
} else {
|
||||
try {
|
||||
const yaml = await import('js-yaml');
|
||||
content = yaml.dump(config, { indent: 2, lineWidth: 120 });
|
||||
} catch {
|
||||
// 回退到 JSON
|
||||
content = JSON.stringify(config, null, 2);
|
||||
console.warn('保存 YAML 失败,已保存为 JSON 格式');
|
||||
}
|
||||
}
|
||||
const fullPath = path.join(GLOBAL_CONFIG_DIR, 'agents.json');
|
||||
const content = JSON.stringify(config, null, 2);
|
||||
|
||||
await fs.promises.writeFile(fullPath, content, 'utf-8');
|
||||
}
|
||||
|
||||
@@ -109,16 +109,26 @@ export interface AgentInfo {
|
||||
maxSteps?: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 配置文件默认配置
|
||||
*/
|
||||
export interface AgentConfigFileDefaults {
|
||||
/** 最大执行步数 */
|
||||
maxSteps?: number;
|
||||
/** 默认模型配置(全局 Provider/Model 选择) */
|
||||
model?: AgentModelConfig;
|
||||
/** Vision 模型配置(用于图片理解) */
|
||||
vision?: AgentModelConfig;
|
||||
/** 默认权限配置 */
|
||||
permission?: AgentPermission;
|
||||
}
|
||||
|
||||
/**
|
||||
* Agent 配置文件格式(用户自定义)
|
||||
*/
|
||||
export interface AgentConfigFile {
|
||||
/** 全局默认配置 */
|
||||
defaults?: {
|
||||
maxSteps?: number;
|
||||
model?: AgentModelConfig;
|
||||
permission?: AgentPermission;
|
||||
};
|
||||
defaults?: AgentConfigFileDefaults;
|
||||
/** Agent 定义 */
|
||||
agents?: Record<string, Omit<AgentInfo, 'name'>>;
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ export {
|
||||
} from './core/doom-loop.js';
|
||||
export type { DoomLoopDetector } from './core/doom-loop.js';
|
||||
export { toolRegistry, todoManager, initTaskContext, updateTaskDescription, updateSkillDescription } from './tools/index.js';
|
||||
export { loadConfig, saveConfig, getConfig, loadVisionConfig, ConfigurationError } from './utils/config.js';
|
||||
export { loadConfig, loadVisionConfig, buildConfigFromModelConfig, ConfigurationError } from './utils/config.js';
|
||||
export type { VisionConfig } from './utils/config.js';
|
||||
|
||||
// Context compression
|
||||
|
||||
@@ -2,7 +2,6 @@ import { tavily } from '@tavily/core';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getConfig } from '../../utils/config.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
export const webExtractTool: ToolWithMetadata = {
|
||||
@@ -73,15 +72,14 @@ export const webExtractTool: ToolWithMetadata = {
|
||||
};
|
||||
}
|
||||
|
||||
// 获取 Tavily API Key
|
||||
const config = getConfig();
|
||||
const apiKey = process.env.TAVILY_API_KEY || config.tavilyApiKey;
|
||||
// 获取 Tavily API Key(从环境变量)
|
||||
const apiKey = process.env.TAVILY_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY 或在配置文件中添加 tavilyApiKey。',
|
||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY。',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ import { tavily } from '@tavily/core';
|
||||
import type { ToolResult } from '../../types/index.js';
|
||||
import type { ToolWithMetadata } from '../types.js';
|
||||
import { loadDescription } from '../load_description.js';
|
||||
import { getConfig } from '../../utils/config.js';
|
||||
import { getPermissionManager } from '../../permission/index.js';
|
||||
|
||||
export const webSearchTool: ToolWithMetadata = {
|
||||
@@ -75,15 +74,14 @@ export const webSearchTool: ToolWithMetadata = {
|
||||
};
|
||||
}
|
||||
|
||||
// 获取 Tavily API Key
|
||||
const config = getConfig();
|
||||
const apiKey = process.env.TAVILY_API_KEY || config.tavilyApiKey;
|
||||
// 获取 Tavily API Key(从环境变量)
|
||||
const apiKey = process.env.TAVILY_API_KEY;
|
||||
|
||||
if (!apiKey) {
|
||||
return {
|
||||
success: false,
|
||||
output: '',
|
||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY 或在配置文件中添加 tavilyApiKey。',
|
||||
error: '未配置 Tavily API Key。请设置环境变量 TAVILY_API_KEY。',
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
+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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,8 @@ vi.mock('@tavily/core', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock config
|
||||
vi.mock('../../../../src/utils/config.js', () => ({
|
||||
getConfig: vi.fn(() => ({
|
||||
tavilyApiKey: 'test-api-key',
|
||||
})),
|
||||
}));
|
||||
// Mock environment variable for Tavily API Key
|
||||
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
|
||||
|
||||
// Mock permission manager
|
||||
vi.mock('../../../../src/permission/index.js', () => ({
|
||||
@@ -32,7 +28,6 @@ vi.mock('../../../../src/tools/load_description.js', () => ({
|
||||
|
||||
import { webExtractTool } from '../../../../src/tools/web/web_extract.js';
|
||||
import { getPermissionManager } from '../../../../src/permission/index.js';
|
||||
import { getConfig } from '../../../../src/utils/config.js';
|
||||
|
||||
describe('webExtractTool - 网页内容提取工具', () => {
|
||||
beforeEach(() => {
|
||||
@@ -198,9 +193,7 @@ describe('webExtractTool - 网页内容提取工具', () => {
|
||||
});
|
||||
|
||||
it('无 API Key 返回错误', async () => {
|
||||
vi.mocked(getConfig).mockReturnValue({} as any);
|
||||
const originalEnv = process.env.TAVILY_API_KEY;
|
||||
delete process.env.TAVILY_API_KEY;
|
||||
vi.unstubAllEnvs();
|
||||
|
||||
const result = await webExtractTool.execute({
|
||||
urls: ['https://example.com'],
|
||||
@@ -209,7 +202,7 @@ describe('webExtractTool - 网页内容提取工具', () => {
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('未配置 Tavily API Key');
|
||||
|
||||
process.env.TAVILY_API_KEY = originalEnv;
|
||||
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
|
||||
});
|
||||
|
||||
it('权限被拒绝时返回错误', async () => {
|
||||
|
||||
@@ -8,12 +8,8 @@ vi.mock('@tavily/core', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock config
|
||||
vi.mock('../../../../src/utils/config.js', () => ({
|
||||
getConfig: vi.fn(() => ({
|
||||
tavilyApiKey: 'test-api-key',
|
||||
})),
|
||||
}));
|
||||
// Mock environment variable for Tavily API Key
|
||||
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
|
||||
|
||||
// Mock permission manager
|
||||
vi.mock('../../../../src/permission/index.js', () => ({
|
||||
@@ -32,7 +28,6 @@ vi.mock('../../../../src/tools/load_description.js', () => ({
|
||||
|
||||
import { webSearchTool } from '../../../../src/tools/web/web_search.js';
|
||||
import { getPermissionManager } from '../../../../src/permission/index.js';
|
||||
import { getConfig } from '../../../../src/utils/config.js';
|
||||
|
||||
describe('webSearchTool - 网络搜索工具', () => {
|
||||
beforeEach(() => {
|
||||
@@ -131,16 +126,14 @@ describe('webSearchTool - 网络搜索工具', () => {
|
||||
});
|
||||
|
||||
it('无 API Key 返回错误', async () => {
|
||||
vi.mocked(getConfig).mockReturnValue({} as any);
|
||||
const originalEnv = process.env.TAVILY_API_KEY;
|
||||
delete process.env.TAVILY_API_KEY;
|
||||
vi.unstubAllEnvs();
|
||||
|
||||
const result = await webSearchTool.execute({ query: 'test' });
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('未配置 Tavily API Key');
|
||||
|
||||
process.env.TAVILY_API_KEY = originalEnv;
|
||||
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
|
||||
});
|
||||
|
||||
it('权限被拒绝时返回错误', async () => {
|
||||
|
||||
@@ -1,243 +0,0 @@
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
||||
import * as fs from 'fs';
|
||||
import * as os from 'os';
|
||||
import * as path from 'path';
|
||||
|
||||
// Mock fs module
|
||||
vi.mock('fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock provider registry
|
||||
vi.mock('../../../src/provider/index.js', () => ({
|
||||
providerRegistry: {
|
||||
getInfo: vi.fn(),
|
||||
getConfig: vi.fn(),
|
||||
},
|
||||
resolveApiKey: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock process.exit
|
||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||
throw new Error('process.exit called');
|
||||
});
|
||||
|
||||
// Mock console.error
|
||||
const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
|
||||
describe('Config - 配置管理扩展测试', () => {
|
||||
const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
|
||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
mockExit.mockClear();
|
||||
mockConsoleError.mockClear();
|
||||
});
|
||||
|
||||
describe('getConfig - 获取原始配置', () => {
|
||||
it('配置文件不存在返回空对象', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
vi.resetModules();
|
||||
const { getConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
const config = getConfig();
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
|
||||
it('配置文件存在返回解析后的内容', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
}));
|
||||
|
||||
vi.resetModules();
|
||||
const { getConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
const config = getConfig();
|
||||
expect(config.provider).toBe('anthropic');
|
||||
expect(config.model).toBe('claude-sonnet-4-20250514');
|
||||
});
|
||||
|
||||
it('配置文件解析错误返回空对象', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('invalid json');
|
||||
|
||||
vi.resetModules();
|
||||
const { getConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
const config = getConfig();
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadConfig - 加载配置', () => {
|
||||
it('通过 ProviderRegistry 获取 API Key', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'anthropic',
|
||||
}));
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
vi.mocked(resolveApiKey).mockReturnValue('resolved-api-key');
|
||||
|
||||
const config = loadConfig();
|
||||
expect(config.apiKey).toBe('resolved-api-key');
|
||||
});
|
||||
|
||||
it('使用配置文件中的 provider', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'deepseek',
|
||||
}));
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
vi.mocked(resolveApiKey).mockReturnValue('deepseek-key');
|
||||
|
||||
const config = loadConfig();
|
||||
expect(config.provider).toBe('deepseek');
|
||||
expect(config.apiKey).toBe('deepseek-key');
|
||||
});
|
||||
|
||||
it('使用默认模型', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||
|
||||
const config = loadConfig();
|
||||
expect(config.model).toBe('claude-sonnet-4-20250514');
|
||||
});
|
||||
|
||||
it('无 API Key 时退出程序', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||
|
||||
expect(() => loadConfig()).toThrow('process.exit called');
|
||||
expect(mockConsoleError).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadVisionConfig - 加载 Vision 配置', () => {
|
||||
it('返回 null 当没有 API Key', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||
|
||||
const config = loadVisionConfig();
|
||||
expect(config).toBeNull();
|
||||
});
|
||||
|
||||
it('从配置文件获取 Vision 设置', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
visionProvider: 'openai',
|
||||
visionModel: 'gpt-4o',
|
||||
visionBaseUrl: 'https://config-vision.example.com',
|
||||
}));
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
vi.mocked(resolveApiKey).mockReturnValue('config-vision-key');
|
||||
|
||||
const config = loadVisionConfig();
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.provider).toBe('openai');
|
||||
expect(config!.model).toBe('gpt-4o');
|
||||
expect(config!.apiKey).toBe('config-vision-key');
|
||||
});
|
||||
|
||||
it('默认使用 anthropic provider', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
vi.resetModules();
|
||||
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
vi.mocked(resolveApiKey).mockReturnValue('anthropic-key');
|
||||
|
||||
const config = loadVisionConfig();
|
||||
expect(config).not.toBeNull();
|
||||
expect(config!.provider).toBe('anthropic');
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveConfig - 保存配置', () => {
|
||||
it('创建配置目录(如果不存在)', async () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
vi.resetModules();
|
||||
const { saveConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
saveConfig({ provider: 'anthropic' });
|
||||
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(CONFIG_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
it('合并现有配置', async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'anthropic',
|
||||
}));
|
||||
|
||||
vi.resetModules();
|
||||
const { saveConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
saveConfig({ model: 'claude-opus-4-20250514' });
|
||||
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
CONFIG_FILE,
|
||||
expect.stringContaining('"model": "claude-opus-4-20250514"')
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
CONFIG_FILE,
|
||||
expect.stringContaining('"provider": "anthropic"')
|
||||
);
|
||||
});
|
||||
|
||||
it('覆盖相同字段', async () => {
|
||||
vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'anthropic',
|
||||
model: 'old-model',
|
||||
}));
|
||||
|
||||
vi.resetModules();
|
||||
const { saveConfig } = await import('../../../src/utils/config.js');
|
||||
|
||||
saveConfig({ model: 'new-model' });
|
||||
|
||||
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
||||
const savedConfig = JSON.parse(writeCall[1] as string);
|
||||
expect(savedConfig.model).toBe('new-model');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,108 +1,103 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Mock fs
|
||||
vi.mock('fs', () => ({
|
||||
existsSync: vi.fn(),
|
||||
readFileSync: vi.fn(),
|
||||
writeFileSync: vi.fn(),
|
||||
mkdirSync: vi.fn(),
|
||||
}));
|
||||
|
||||
// Mock provider registry
|
||||
vi.mock('../../../src/provider/index.js', () => ({
|
||||
providerRegistry: {
|
||||
getInfo: vi.fn(),
|
||||
getConfig: vi.fn(),
|
||||
getModelInfo: vi.fn(),
|
||||
},
|
||||
resolveApiKey: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as fs from 'fs';
|
||||
// Mock agent registry
|
||||
vi.mock('../../../src/agent/registry.js', () => ({
|
||||
agentRegistry: {
|
||||
getGlobalConfig: vi.fn(),
|
||||
},
|
||||
}));
|
||||
|
||||
import { providerRegistry, resolveApiKey } from '../../../src/provider/index.js';
|
||||
import { getConfig, loadConfig, saveConfig, loadVisionConfig } from '../../../src/utils/config.js';
|
||||
import { agentRegistry } from '../../../src/agent/registry.js';
|
||||
import { loadConfig, loadVisionConfig, ConfigurationError } from '../../../src/utils/config.js';
|
||||
|
||||
describe('Config - 配置管理', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getConfig - 获取原始配置', () => {
|
||||
it('配置文件存在时返回内容', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'anthropic',
|
||||
model: 'claude-3-opus',
|
||||
}));
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
expect(config.provider).toBe('anthropic');
|
||||
expect(config.model).toBe('claude-3-opus');
|
||||
});
|
||||
|
||||
it('配置文件不存在时返回空对象', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
|
||||
it('配置文件解析错误时返回空对象', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('invalid json');
|
||||
|
||||
const config = getConfig();
|
||||
|
||||
expect(config).toEqual({});
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadConfig - 加载完整配置', () => {
|
||||
it('通过 ProviderRegistry 获取 Anthropic 配置', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
|
||||
vi.mocked(resolveApiKey).mockReturnValue('resolved-anthropic-key');
|
||||
it('从 AgentRegistry defaults.model 获取配置', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
model: {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
maxTokens: 8192,
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({
|
||||
id: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
});
|
||||
vi.mocked(resolveApiKey).mockReturnValue('openai-api-key');
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
expect(config.provider).toBe('anthropic');
|
||||
expect(config.apiKey).toBe('resolved-anthropic-key');
|
||||
expect(config.model).toBe('claude-sonnet-4-20250514');
|
||||
});
|
||||
|
||||
it('从配置文件获取 DeepSeek provider', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'deepseek',
|
||||
}));
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
|
||||
vi.mocked(resolveApiKey).mockReturnValue('resolved-deepseek-key');
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
expect(config.provider).toBe('deepseek');
|
||||
expect(config.apiKey).toBe('resolved-deepseek-key');
|
||||
expect(config.model).toBe('deepseek-chat');
|
||||
});
|
||||
|
||||
it('配置文件中的 model 和 maxTokens', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
model: 'custom-model',
|
||||
maxTokens: 8192,
|
||||
}));
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
|
||||
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
expect(config.model).toBe('custom-model');
|
||||
expect(config.provider).toBe('openai');
|
||||
expect(config.model).toBe('gpt-4o');
|
||||
expect(config.apiKey).toBe('openai-api-key');
|
||||
expect(config.maxTokens).toBe(8192);
|
||||
expect(config.baseUrl).toBe('https://api.openai.com/v1');
|
||||
});
|
||||
|
||||
it('未配置 defaults.model 时使用第一个有 API Key 的 provider', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
|
||||
// anthropic 没有 API Key
|
||||
vi.mocked(providerRegistry.getConfig).mockImplementation((id) => {
|
||||
if (id === 'openai') {
|
||||
return { id: 'openai' };
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
vi.mocked(resolveApiKey).mockImplementation((config) => {
|
||||
if (config?.id === 'openai') {
|
||||
return 'openai-fallback-key';
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
expect(config.provider).toBe('openai');
|
||||
expect(config.apiKey).toBe('openai-fallback-key');
|
||||
expect(config.model).toBe('gpt-4o'); // 默认模型
|
||||
});
|
||||
|
||||
it('没有任何 provider 配置 API Key 时抛出错误', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
|
||||
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||
|
||||
expect(() => loadConfig()).toThrow(ConfigurationError);
|
||||
expect(() => loadConfig()).toThrow('请先在 Providers 面板配置一个模型提供商的 API Key');
|
||||
});
|
||||
|
||||
it('配置了 provider 但没有 API Key 时抛出错误', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
model: {
|
||||
provider: 'deepseek',
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'deepseek' });
|
||||
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||
|
||||
expect(() => loadConfig()).toThrow(ConfigurationError);
|
||||
expect(() => loadConfig()).toThrow('请在 Providers 面板配置 deepseek 的 API Key');
|
||||
});
|
||||
|
||||
it('包含系统提示词', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
|
||||
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||
|
||||
const config = loadConfig();
|
||||
@@ -112,7 +107,12 @@ describe('Config - 配置管理', () => {
|
||||
});
|
||||
|
||||
it('默认 maxTokens 为 4096', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
model: {
|
||||
provider: 'anthropic',
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
|
||||
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||
|
||||
const config = loadConfig();
|
||||
@@ -120,11 +120,16 @@ describe('Config - 配置管理', () => {
|
||||
expect(config.maxTokens).toBe(4096);
|
||||
});
|
||||
|
||||
it('从配置文件获取 baseUrl', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
it('从 ProviderConfig 获取 baseUrl', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
model: {
|
||||
provider: 'openai',
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({
|
||||
id: 'openai',
|
||||
baseUrl: 'https://custom.api.com/v1',
|
||||
}));
|
||||
});
|
||||
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||
|
||||
const config = loadConfig();
|
||||
@@ -132,67 +137,44 @@ describe('Config - 配置管理', () => {
|
||||
expect(config.baseUrl).toBe('https://custom.api.com/v1');
|
||||
});
|
||||
|
||||
it('从 ProviderConfig 获取 baseUrl', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({
|
||||
id: 'anthropic',
|
||||
baseUrl: 'https://provider-config.api.com/v1',
|
||||
it('获取模型的 contextWindow', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
model: {
|
||||
provider: 'anthropic',
|
||||
model: 'claude-sonnet-4-20250514',
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
|
||||
vi.mocked(providerRegistry.getModelInfo).mockReturnValue({
|
||||
id: 'claude-sonnet-4-20250514',
|
||||
name: 'Claude Sonnet 4',
|
||||
contextWindow: 200000,
|
||||
});
|
||||
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||
|
||||
const config = loadConfig();
|
||||
|
||||
expect(config.baseUrl).toBe('https://provider-config.api.com/v1');
|
||||
});
|
||||
});
|
||||
|
||||
describe('saveConfig - 保存配置', () => {
|
||||
it('创建目录并保存配置', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
|
||||
saveConfig({ provider: 'anthropic' });
|
||||
|
||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
{ recursive: true }
|
||||
);
|
||||
expect(fs.writeFileSync).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.stringContaining('anthropic')
|
||||
);
|
||||
});
|
||||
|
||||
it('合并现有配置', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
provider: 'anthropic',
|
||||
model: 'old-model',
|
||||
}));
|
||||
|
||||
saveConfig({ model: 'new-model' });
|
||||
|
||||
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
||||
const savedConfig = JSON.parse(writeCall[1] as string);
|
||||
|
||||
expect(savedConfig.provider).toBe('anthropic');
|
||||
expect(savedConfig.model).toBe('new-model');
|
||||
});
|
||||
|
||||
it('目录已存在时不重新创建', () => {
|
||||
vi.mocked(fs.existsSync)
|
||||
.mockReturnValueOnce(true) // 目录存在
|
||||
.mockReturnValueOnce(true); // 配置文件存在
|
||||
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
||||
|
||||
saveConfig({ provider: 'test' });
|
||||
|
||||
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
||||
expect(config.contextWindow).toBe(200000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadVisionConfig - Vision 配置', () => {
|
||||
it('返回 null 当没有配置 Vision API Key', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
it('返回 null 当没有配置 defaults.vision', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
|
||||
|
||||
const visionConfig = loadVisionConfig();
|
||||
|
||||
expect(visionConfig).toBeNull();
|
||||
});
|
||||
|
||||
it('返回 null 当 vision provider 没有 API Key', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
vision: {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'openai' });
|
||||
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||
|
||||
const visionConfig = loadVisionConfig();
|
||||
@@ -200,12 +182,17 @@ describe('Config - 配置管理', () => {
|
||||
expect(visionConfig).toBeNull();
|
||||
});
|
||||
|
||||
it('通过 ProviderRegistry 获取 Vision 配置', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
visionProvider: 'openai',
|
||||
visionModel: 'gpt-4-vision',
|
||||
}));
|
||||
it('从 defaults.vision 获取 Vision 配置', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
vision: {
|
||||
provider: 'openai',
|
||||
model: 'gpt-4o',
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({
|
||||
id: 'openai',
|
||||
baseUrl: 'https://api.openai.com/v1',
|
||||
});
|
||||
vi.mocked(resolveApiKey).mockReturnValue('vision-api-key');
|
||||
|
||||
const visionConfig = loadVisionConfig();
|
||||
@@ -213,33 +200,17 @@ describe('Config - 配置管理', () => {
|
||||
expect(visionConfig).not.toBeNull();
|
||||
expect(visionConfig?.provider).toBe('openai');
|
||||
expect(visionConfig?.apiKey).toBe('vision-api-key');
|
||||
expect(visionConfig?.model).toBe('gpt-4-vision');
|
||||
expect(visionConfig?.model).toBe('gpt-4o');
|
||||
expect(visionConfig?.baseUrl).toBe('https://api.openai.com/v1');
|
||||
});
|
||||
|
||||
it('默认使用 anthropic provider', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
vi.mocked(resolveApiKey).mockReturnValue('anthropic-key');
|
||||
|
||||
const visionConfig = loadVisionConfig();
|
||||
|
||||
expect(visionConfig).not.toBeNull();
|
||||
expect(visionConfig?.provider).toBe('anthropic');
|
||||
});
|
||||
|
||||
it('Vision baseUrl 配置', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||
visionBaseUrl: 'https://vision.api.com/v1',
|
||||
}));
|
||||
vi.mocked(resolveApiKey).mockReturnValue('vision-key');
|
||||
|
||||
const visionConfig = loadVisionConfig();
|
||||
|
||||
expect(visionConfig?.baseUrl).toBe('https://vision.api.com/v1');
|
||||
});
|
||||
|
||||
it('使用默认 Vision 模型', () => {
|
||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||
it('使用默认模型当 vision.model 未指定', () => {
|
||||
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
|
||||
vision: {
|
||||
provider: 'anthropic',
|
||||
},
|
||||
});
|
||||
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
|
||||
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||
|
||||
const visionConfig = loadVisionConfig();
|
||||
@@ -247,4 +218,15 @@ describe('Config - 配置管理', () => {
|
||||
expect(visionConfig?.model).toBe('claude-sonnet-4-20250514');
|
||||
});
|
||||
});
|
||||
|
||||
describe('ConfigurationError', () => {
|
||||
it('包含 provider 和 missingKey 信息', () => {
|
||||
const error = new ConfigurationError('Test error', 'openai', 'apiKey');
|
||||
|
||||
expect(error.name).toBe('ConfigurationError');
|
||||
expect(error.message).toBe('Test error');
|
||||
expect(error.provider).toBe('openai');
|
||||
expect(error.missingKey).toBe('apiKey');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user