refactor(config): 移除环境变量依赖,统一使用 Provider 配置系统
- 移除 .env.example 文件 - 简化 resolveApiKey 函数,只从配置文件读取 API Key - 重构 loadConfig/loadVisionConfig/loadSummaryConfig 使用 ProviderRegistry - 更新测试以 mock Provider 系统
This commit is contained in:
@@ -1,11 +0,0 @@
|
|||||||
# Anthropic API Key (必需)
|
|
||||||
ANTHROPIC_API_KEY=sk-ant-xxxxx
|
|
||||||
|
|
||||||
# 可选配置
|
|
||||||
AI_MODEL=claude-sonnet-4-20250514
|
|
||||||
AI_MAX_TOKENS=4096
|
|
||||||
|
|
||||||
# Vision 配置(用于图片理解,当主模型不支持 vision 时使用)
|
|
||||||
# 如果不配置,默认使用 Anthropic Claude
|
|
||||||
# VISION_PROVIDER=anthropic
|
|
||||||
# VISION_MODEL=claude-sonnet-4-20250514
|
|
||||||
@@ -73,22 +73,9 @@ export async function saveProvidersConfig(
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* 获取提供商的 API Key
|
* 获取提供商的 API Key
|
||||||
* 优先从环境变量获取,其次从配置获取
|
* 从配置文件获取
|
||||||
*/
|
*/
|
||||||
export function resolveApiKey(config?: ProviderConfig, envVar?: string): string | undefined {
|
export function resolveApiKey(config?: ProviderConfig): 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;
|
return config?.apiKey;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ export class ProviderRegistry {
|
|||||||
this.ensureInitialized();
|
this.ensureInitialized();
|
||||||
return Array.from(this.providers.entries()).map(([id, provider]) => {
|
return Array.from(this.providers.entries()).map(([id, provider]) => {
|
||||||
const config = this.configs.get(id);
|
const config = this.configs.get(id);
|
||||||
const apiKey = resolveApiKey(config, provider.info.apiKeyEnvVar);
|
const apiKey = resolveApiKey(config);
|
||||||
const customModels = config?.customModels ?? [];
|
const customModels = config?.customModels ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -165,7 +165,7 @@ export class ProviderRegistry {
|
|||||||
if (!provider) return undefined;
|
if (!provider) return undefined;
|
||||||
|
|
||||||
const config = this.configs.get(id);
|
const config = this.configs.get(id);
|
||||||
const apiKey = resolveApiKey(config, provider.info.apiKeyEnvVar);
|
const apiKey = resolveApiKey(config);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
id,
|
id,
|
||||||
@@ -355,7 +355,7 @@ export class ProviderRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = this.configs.get(providerId);
|
const config = this.configs.get(providerId);
|
||||||
const resolvedApiKey = apiKey ?? resolveApiKey(config, provider.info.apiKeyEnvVar);
|
const resolvedApiKey = apiKey ?? resolveApiKey(config);
|
||||||
|
|
||||||
if (!resolvedApiKey && !provider.info.baseUrl?.includes('localhost')) {
|
if (!resolvedApiKey && !provider.info.baseUrl?.includes('localhost')) {
|
||||||
return { success: false, error: 'API key not configured' };
|
return { success: false, error: 'API key not configured' };
|
||||||
@@ -390,7 +390,7 @@ export class ProviderRegistry {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const config = this.configs.get(providerId);
|
const config = this.configs.get(providerId);
|
||||||
const apiKey = options?.apiKey ?? resolveApiKey(config, provider.info.apiKeyEnvVar);
|
const apiKey = options?.apiKey ?? resolveApiKey(config);
|
||||||
const baseUrl = options?.baseUrl ?? config?.baseUrl ?? provider.info.baseUrl;
|
const baseUrl = options?.baseUrl ?? config?.baseUrl ?? provider.info.baseUrl;
|
||||||
|
|
||||||
if (!apiKey) {
|
if (!apiKey) {
|
||||||
|
|||||||
@@ -2,15 +2,13 @@ import * as fs from 'fs';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as os from 'os';
|
import * as os from 'os';
|
||||||
import type { AgentConfig, ProviderType } from '../types/index.js';
|
import type { AgentConfig, ProviderType } from '../types/index.js';
|
||||||
|
import { providerRegistry, resolveApiKey } from '../provider/index.js';
|
||||||
|
|
||||||
const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
|
const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant');
|
||||||
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json');
|
||||||
|
|
||||||
interface StoredConfig {
|
interface StoredConfig {
|
||||||
provider?: ProviderType;
|
provider?: ProviderType;
|
||||||
apiKey?: string;
|
|
||||||
deepseekApiKey?: string;
|
|
||||||
openaiApiKey?: string;
|
|
||||||
model?: string;
|
model?: string;
|
||||||
maxTokens?: number;
|
maxTokens?: number;
|
||||||
tavilyApiKey?: string;
|
tavilyApiKey?: string;
|
||||||
@@ -19,15 +17,11 @@ interface StoredConfig {
|
|||||||
// Vision 配置
|
// Vision 配置
|
||||||
visionProvider?: ProviderType;
|
visionProvider?: ProviderType;
|
||||||
visionModel?: string;
|
visionModel?: string;
|
||||||
/** Vision 专用的 API Key(可选,不设置则使用对应 provider 的 key) */
|
|
||||||
visionApiKey?: string;
|
|
||||||
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
|
/** Vision 专用的 Base URL(用于 OpenAI 兼容的 Vision 服务) */
|
||||||
visionBaseUrl?: string;
|
visionBaseUrl?: string;
|
||||||
// Summary 配置(用于对话压缩摘要生成)
|
// Summary 配置(用于对话压缩摘要生成)
|
||||||
summaryProvider?: ProviderType;
|
summaryProvider?: ProviderType;
|
||||||
summaryModel?: string;
|
summaryModel?: string;
|
||||||
/** Summary 专用的 API Key(可选,不设置则使用对应 provider 的 key) */
|
|
||||||
summaryApiKey?: string;
|
|
||||||
/** Summary 专用的 Base URL(用于 OpenAI 兼容的 Summary 服务) */
|
/** Summary 专用的 Base URL(用于 OpenAI 兼容的 Summary 服务) */
|
||||||
summaryBaseUrl?: string;
|
summaryBaseUrl?: string;
|
||||||
}
|
}
|
||||||
@@ -106,63 +100,33 @@ export function getConfig(): StoredConfig {
|
|||||||
|
|
||||||
// 加载配置
|
// 加载配置
|
||||||
export function loadConfig(): AgentConfig {
|
export function loadConfig(): AgentConfig {
|
||||||
// 从环境变量获取
|
|
||||||
const provider = (process.env.AI_PROVIDER as ProviderType) || 'anthropic';
|
|
||||||
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
||||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
|
||||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
||||||
const model = process.env.AI_MODEL;
|
|
||||||
const maxTokens = parseInt(process.env.AI_MAX_TOKENS || '4096', 10);
|
|
||||||
const baseUrl = process.env.AI_BASE_URL;
|
|
||||||
|
|
||||||
// 从配置文件读取
|
// 从配置文件读取
|
||||||
let storedConfig: StoredConfig = {};
|
const storedConfig = getConfig();
|
||||||
if (fs.existsSync(CONFIG_FILE)) {
|
|
||||||
try {
|
|
||||||
const content = fs.readFileSync(CONFIG_FILE, 'utf-8');
|
|
||||||
storedConfig = JSON.parse(content);
|
|
||||||
} catch {
|
|
||||||
// 忽略解析错误
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确定最终的 provider
|
// 确定最终的 provider
|
||||||
const finalProvider = storedConfig.provider || provider;
|
const finalProvider = storedConfig.provider || 'anthropic';
|
||||||
|
|
||||||
// 根据 provider 获取对应的 API Key
|
// 通过 ProviderRegistry 获取 API Key
|
||||||
let finalApiKey: string | undefined;
|
const providerConfig = providerRegistry.getConfig(finalProvider);
|
||||||
if (finalProvider === 'anthropic') {
|
const finalApiKey = resolveApiKey(providerConfig);
|
||||||
finalApiKey = anthropicApiKey || storedConfig.apiKey;
|
|
||||||
} else if (finalProvider === 'deepseek') {
|
|
||||||
finalApiKey = deepseekApiKey || storedConfig.deepseekApiKey;
|
|
||||||
} else if (finalProvider === 'openai') {
|
|
||||||
finalApiKey = openaiApiKey || storedConfig.openaiApiKey;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!finalApiKey) {
|
if (!finalApiKey) {
|
||||||
const envVarMap: Record<ProviderType, string> = {
|
console.error(`❌ 错误: 未配置 API Key`);
|
||||||
anthropic: 'ANTHROPIC_API_KEY',
|
console.error(`请在设置中配置 ${finalProvider} 的 API Key`);
|
||||||
deepseek: 'DEEPSEEK_API_KEY',
|
|
||||||
openai: 'OPENAI_API_KEY',
|
|
||||||
};
|
|
||||||
const envVar = envVarMap[finalProvider];
|
|
||||||
console.error(`❌ 错误: 未设置 ${envVar}`);
|
|
||||||
console.error(`请设置环境变量: export ${envVar}=your-api-key`);
|
|
||||||
console.error('或运行: ai-assist init 进行初始化配置');
|
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定模型
|
// 确定模型
|
||||||
const finalModel = model || storedConfig.model || DEFAULT_MODELS[finalProvider];
|
const finalModel = storedConfig.model || DEFAULT_MODELS[finalProvider];
|
||||||
|
|
||||||
// 确定 baseUrl(环境变量优先)
|
// 确定 baseUrl
|
||||||
const finalBaseUrl = baseUrl || storedConfig.baseUrl;
|
const finalBaseUrl = storedConfig.baseUrl || providerConfig?.baseUrl;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: finalProvider,
|
provider: finalProvider,
|
||||||
apiKey: finalApiKey,
|
apiKey: finalApiKey,
|
||||||
model: finalModel,
|
model: finalModel,
|
||||||
maxTokens: storedConfig.maxTokens || maxTokens,
|
maxTokens: storedConfig.maxTokens || 4096,
|
||||||
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
systemPrompt: DEFAULT_SYSTEM_PROMPT,
|
||||||
baseUrl: finalBaseUrl,
|
baseUrl: finalBaseUrl,
|
||||||
};
|
};
|
||||||
@@ -171,38 +135,18 @@ export function loadConfig(): AgentConfig {
|
|||||||
/**
|
/**
|
||||||
* 加载 Vision 配置
|
* 加载 Vision 配置
|
||||||
* Vision 用于图片理解,当主模型不支持 vision 时使用
|
* Vision 用于图片理解,当主模型不支持 vision 时使用
|
||||||
* 优先级:环境变量 > 配置文件 > 默认使用 Anthropic Claude
|
* 通过 ProviderRegistry 获取 API Key
|
||||||
*/
|
*/
|
||||||
export function loadVisionConfig(): VisionConfig | null {
|
export function loadVisionConfig(): VisionConfig | null {
|
||||||
// 从环境变量获取
|
|
||||||
const visionProvider = process.env.VISION_PROVIDER as ProviderType | undefined;
|
|
||||||
const visionModel = process.env.VISION_MODEL;
|
|
||||||
const visionApiKey = process.env.VISION_API_KEY;
|
|
||||||
const visionBaseUrl = process.env.VISION_BASE_URL;
|
|
||||||
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
||||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
|
||||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
||||||
|
|
||||||
// 从配置文件读取
|
// 从配置文件读取
|
||||||
const storedConfig = getConfig();
|
const storedConfig = getConfig();
|
||||||
|
|
||||||
// 确定 vision provider(默认使用 anthropic,因为 Claude 支持 vision)
|
// 确定 vision provider(默认使用 anthropic,因为 Claude 支持 vision)
|
||||||
const finalProvider = visionProvider || storedConfig.visionProvider || 'anthropic';
|
const finalProvider = storedConfig.visionProvider || 'anthropic';
|
||||||
|
|
||||||
// 获取 Vision 专用的 API Key(优先级:环境变量 > 配置文件专用 key > provider 对应的 key)
|
// 通过 ProviderRegistry 获取 API Key
|
||||||
let finalApiKey: string | undefined;
|
const providerConfig = providerRegistry.getConfig(finalProvider);
|
||||||
finalApiKey = visionApiKey || storedConfig.visionApiKey;
|
const finalApiKey = resolveApiKey(providerConfig);
|
||||||
|
|
||||||
// 如果没有专用 key,回退到对应 provider 的 key
|
|
||||||
if (!finalApiKey) {
|
|
||||||
if (finalProvider === 'anthropic') {
|
|
||||||
finalApiKey = anthropicApiKey || storedConfig.apiKey;
|
|
||||||
} else if (finalProvider === 'deepseek') {
|
|
||||||
finalApiKey = deepseekApiKey || storedConfig.deepseekApiKey;
|
|
||||||
} else if (finalProvider === 'openai') {
|
|
||||||
finalApiKey = openaiApiKey || storedConfig.openaiApiKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有 API Key,返回 null
|
// 如果没有 API Key,返回 null
|
||||||
if (!finalApiKey) {
|
if (!finalApiKey) {
|
||||||
@@ -210,10 +154,10 @@ export function loadVisionConfig(): VisionConfig | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确定模型
|
// 确定模型
|
||||||
const finalModel = visionModel || storedConfig.visionModel || DEFAULT_VISION_MODELS[finalProvider];
|
const finalModel = storedConfig.visionModel || DEFAULT_VISION_MODELS[finalProvider];
|
||||||
|
|
||||||
// 确定 baseUrl(Vision 专用)
|
// 确定 baseUrl
|
||||||
const finalBaseUrl = visionBaseUrl || storedConfig.visionBaseUrl;
|
const finalBaseUrl = storedConfig.visionBaseUrl || providerConfig?.baseUrl;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: finalProvider,
|
provider: finalProvider,
|
||||||
@@ -226,52 +170,26 @@ export function loadVisionConfig(): VisionConfig | null {
|
|||||||
/**
|
/**
|
||||||
* 加载 Summary 配置
|
* 加载 Summary 配置
|
||||||
* Summary 用于对话压缩时生成摘要,推荐使用成本较低的小模型
|
* Summary 用于对话压缩时生成摘要,推荐使用成本较低的小模型
|
||||||
* 优先级:环境变量 > 配置文件 > null(使用主模型)
|
* 通过 ProviderRegistry 获取 API Key
|
||||||
*/
|
*/
|
||||||
export function loadSummaryConfig(): SummaryConfig | null {
|
export function loadSummaryConfig(): SummaryConfig | null {
|
||||||
// 从环境变量获取
|
|
||||||
const summaryProvider = process.env.SUMMARY_PROVIDER as ProviderType | undefined;
|
|
||||||
const summaryModel = process.env.SUMMARY_MODEL;
|
|
||||||
const summaryApiKey = process.env.SUMMARY_API_KEY;
|
|
||||||
const summaryBaseUrl = process.env.SUMMARY_BASE_URL;
|
|
||||||
const anthropicApiKey = process.env.ANTHROPIC_API_KEY;
|
|
||||||
const deepseekApiKey = process.env.DEEPSEEK_API_KEY;
|
|
||||||
const openaiApiKey = process.env.OPENAI_API_KEY;
|
|
||||||
|
|
||||||
// 从配置文件读取
|
// 从配置文件读取
|
||||||
const storedConfig = getConfig();
|
const storedConfig = getConfig();
|
||||||
|
|
||||||
// 如果没有任何 summary 相关配置,返回 null(使用主模型)
|
// 如果没有任何 summary 相关配置,返回 null(使用主模型)
|
||||||
const hasSummaryConfig =
|
const hasSummaryConfig = storedConfig.summaryProvider || storedConfig.summaryModel;
|
||||||
summaryProvider ||
|
|
||||||
summaryModel ||
|
|
||||||
summaryApiKey ||
|
|
||||||
storedConfig.summaryProvider ||
|
|
||||||
storedConfig.summaryModel ||
|
|
||||||
storedConfig.summaryApiKey;
|
|
||||||
|
|
||||||
if (!hasSummaryConfig) {
|
if (!hasSummaryConfig) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定 summary provider(默认使用主配置的 provider)
|
// 确定 summary provider(默认使用主配置的 provider)
|
||||||
const mainProvider = (process.env.AI_PROVIDER as ProviderType) || storedConfig.provider || 'anthropic';
|
const mainProvider = storedConfig.provider || 'anthropic';
|
||||||
const finalProvider = summaryProvider || storedConfig.summaryProvider || mainProvider;
|
const finalProvider = storedConfig.summaryProvider || mainProvider;
|
||||||
|
|
||||||
// 获取 Summary 专用的 API Key(优先级:环境变量 > 配置文件专用 key > provider 对应的 key)
|
// 通过 ProviderRegistry 获取 API Key
|
||||||
let finalApiKey: string | undefined;
|
const providerConfig = providerRegistry.getConfig(finalProvider);
|
||||||
finalApiKey = summaryApiKey || storedConfig.summaryApiKey;
|
const finalApiKey = resolveApiKey(providerConfig);
|
||||||
|
|
||||||
// 如果没有专用 key,回退到对应 provider 的 key
|
|
||||||
if (!finalApiKey) {
|
|
||||||
if (finalProvider === 'anthropic') {
|
|
||||||
finalApiKey = anthropicApiKey || storedConfig.apiKey;
|
|
||||||
} else if (finalProvider === 'deepseek') {
|
|
||||||
finalApiKey = deepseekApiKey || storedConfig.deepseekApiKey;
|
|
||||||
} else if (finalProvider === 'openai') {
|
|
||||||
finalApiKey = openaiApiKey || storedConfig.openaiApiKey;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有 API Key,返回 null
|
// 如果没有 API Key,返回 null
|
||||||
if (!finalApiKey) {
|
if (!finalApiKey) {
|
||||||
@@ -279,10 +197,10 @@ export function loadSummaryConfig(): SummaryConfig | null {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 确定模型
|
// 确定模型
|
||||||
const finalModel = summaryModel || storedConfig.summaryModel || DEFAULT_SUMMARY_MODELS[finalProvider];
|
const finalModel = storedConfig.summaryModel || DEFAULT_SUMMARY_MODELS[finalProvider];
|
||||||
|
|
||||||
// 确定 baseUrl(Summary 专用)
|
// 确定 baseUrl
|
||||||
const finalBaseUrl = summaryBaseUrl || storedConfig.summaryBaseUrl;
|
const finalBaseUrl = storedConfig.summaryBaseUrl || providerConfig?.baseUrl;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
provider: finalProvider,
|
provider: finalProvider,
|
||||||
|
|||||||
@@ -11,6 +11,15 @@ vi.mock('fs', () => ({
|
|||||||
mkdirSync: 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
|
// Mock process.exit
|
||||||
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => {
|
||||||
throw new Error('process.exit called');
|
throw new Error('process.exit called');
|
||||||
@@ -25,18 +34,6 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// 清理环境变量
|
|
||||||
delete process.env.AI_PROVIDER;
|
|
||||||
delete process.env.ANTHROPIC_API_KEY;
|
|
||||||
delete process.env.DEEPSEEK_API_KEY;
|
|
||||||
delete process.env.OPENAI_API_KEY;
|
|
||||||
delete process.env.AI_MODEL;
|
|
||||||
delete process.env.AI_MAX_TOKENS;
|
|
||||||
delete process.env.AI_BASE_URL;
|
|
||||||
delete process.env.VISION_PROVIDER;
|
|
||||||
delete process.env.VISION_MODEL;
|
|
||||||
delete process.env.VISION_API_KEY;
|
|
||||||
delete process.env.VISION_BASE_URL;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -48,7 +45,6 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
it('配置文件不存在返回空对象', async () => {
|
it('配置文件不存在返回空对象', async () => {
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
|
||||||
// 需要重新导入以获取最新的 mock
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
const { getConfig } = await import('../../../src/utils/config.js');
|
const { getConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
@@ -60,7 +56,6 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
apiKey: 'test-key',
|
|
||||||
model: 'claude-sonnet-4-20250514',
|
model: 'claude-sonnet-4-20250514',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -69,7 +64,7 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
expect(config.provider).toBe('anthropic');
|
expect(config.provider).toBe('anthropic');
|
||||||
expect(config.apiKey).toBe('test-key');
|
expect(config.model).toBe('claude-sonnet-4-20250514');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('配置文件解析错误返回空对象', async () => {
|
it('配置文件解析错误返回空对象', async () => {
|
||||||
@@ -85,100 +80,61 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('loadConfig - 加载配置', () => {
|
describe('loadConfig - 加载配置', () => {
|
||||||
it('优先使用环境变量中的 API Key', async () => {
|
it('通过 ProviderRegistry 获取 API Key', async () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'env-api-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
apiKey: 'file-api-key',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('resolved-api-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
expect(config.apiKey).toBe('env-api-key');
|
expect(config.apiKey).toBe('resolved-api-key');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('使用配置文件中的 provider', async () => {
|
it('使用配置文件中的 provider', async () => {
|
||||||
process.env.DEEPSEEK_API_KEY = 'deepseek-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
provider: 'deepseek',
|
provider: 'deepseek',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('deepseek-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
expect(config.provider).toBe('deepseek');
|
expect(config.provider).toBe('deepseek');
|
||||||
expect(config.apiKey).toBe('deepseek-key');
|
expect(config.apiKey).toBe('deepseek-key');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('OpenAI provider 使用 OPENAI_API_KEY', async () => {
|
|
||||||
process.env.OPENAI_API_KEY = 'openai-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
provider: 'openai',
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
expect(config.provider).toBe('openai');
|
|
||||||
expect(config.apiKey).toBe('openai-key');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('环境变量 AI_MODEL 覆盖配置文件', async () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
process.env.AI_MODEL = 'claude-opus-4-20250514';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
provider: 'anthropic',
|
|
||||||
model: 'claude-sonnet-4-20250514',
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
expect(config.model).toBe('claude-opus-4-20250514');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('使用默认模型', async () => {
|
it('使用默认模型', async () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
expect(config.model).toBe('claude-sonnet-4-20250514');
|
expect(config.model).toBe('claude-sonnet-4-20250514');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('环境变量 AI_BASE_URL 覆盖配置文件', async () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
process.env.AI_BASE_URL = 'https://custom-api.example.com';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
provider: 'anthropic',
|
|
||||||
baseUrl: 'https://config-api.example.com',
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
expect(config.baseUrl).toBe('https://custom-api.example.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('无 API Key 时退出程序', async () => {
|
it('无 API Key 时退出程序', async () => {
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||||
const { loadConfig } = await import('../../../src/utils/config.js');
|
const { loadConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||||
|
|
||||||
expect(() => loadConfig()).toThrow('process.exit called');
|
expect(() => loadConfig()).toThrow('process.exit called');
|
||||||
expect(mockConsoleError).toHaveBeenCalled();
|
expect(mockConsoleError).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -189,66 +145,29 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||||
|
|
||||||
const config = loadVisionConfig();
|
const config = loadVisionConfig();
|
||||||
expect(config).toBeNull();
|
expect(config).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('使用环境变量配置', async () => {
|
it('从配置文件获取 Vision 设置', async () => {
|
||||||
process.env.VISION_PROVIDER = 'openai';
|
|
||||||
process.env.VISION_MODEL = 'gpt-4o';
|
|
||||||
process.env.VISION_API_KEY = 'vision-key';
|
|
||||||
process.env.VISION_BASE_URL = 'https://vision-api.example.com';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
|
||||||
|
|
||||||
const config = loadVisionConfig();
|
|
||||||
expect(config).not.toBeNull();
|
|
||||||
expect(config!.provider).toBe('openai');
|
|
||||||
expect(config!.model).toBe('gpt-4o');
|
|
||||||
expect(config!.apiKey).toBe('vision-key');
|
|
||||||
expect(config!.baseUrl).toBe('https://vision-api.example.com');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('默认使用 anthropic provider', async () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'anthropic-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
|
||||||
|
|
||||||
const config = loadVisionConfig();
|
|
||||||
expect(config).not.toBeNull();
|
|
||||||
expect(config!.provider).toBe('anthropic');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('Vision 专用 Key 优先于 provider Key', async () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'anthropic-key';
|
|
||||||
process.env.VISION_API_KEY = 'vision-specific-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
|
||||||
|
|
||||||
const config = loadVisionConfig();
|
|
||||||
expect(config!.apiKey).toBe('vision-specific-key');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('从配置文件加载 Vision 设置', async () => {
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
visionProvider: 'openai',
|
visionProvider: 'openai',
|
||||||
visionModel: 'gpt-4o',
|
visionModel: 'gpt-4o',
|
||||||
visionApiKey: 'config-vision-key',
|
|
||||||
visionBaseUrl: 'https://config-vision.example.com',
|
visionBaseUrl: 'https://config-vision.example.com',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('config-vision-key');
|
||||||
|
|
||||||
const config = loadVisionConfig();
|
const config = loadVisionConfig();
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(config!.provider).toBe('openai');
|
expect(config!.provider).toBe('openai');
|
||||||
@@ -256,19 +175,18 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
expect(config!.apiKey).toBe('config-vision-key');
|
expect(config!.apiKey).toBe('config-vision-key');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('DeepSeek provider 回退到 deepseekApiKey', async () => {
|
it('默认使用 anthropic provider', async () => {
|
||||||
process.env.DEEPSEEK_API_KEY = 'deepseek-key';
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
visionProvider: 'deepseek',
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
const { resolveApiKey } = await import('../../../src/provider/index.js');
|
||||||
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
const { loadVisionConfig } = await import('../../../src/utils/config.js');
|
||||||
|
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('anthropic-key');
|
||||||
|
|
||||||
const config = loadVisionConfig();
|
const config = loadVisionConfig();
|
||||||
expect(config).not.toBeNull();
|
expect(config).not.toBeNull();
|
||||||
expect(config!.apiKey).toBe('deepseek-key');
|
expect(config!.provider).toBe('anthropic');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -288,7 +206,6 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE);
|
vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
apiKey: 'existing-key',
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
@@ -322,17 +239,5 @@ describe('Config - 配置管理扩展测试', () => {
|
|||||||
const savedConfig = JSON.parse(writeCall[1] as string);
|
const savedConfig = JSON.parse(writeCall[1] as string);
|
||||||
expect(savedConfig.model).toBe('new-model');
|
expect(savedConfig.model).toBe('new-model');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('解析错误时使用空对象合并', async () => {
|
|
||||||
vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue('invalid json');
|
|
||||||
|
|
||||||
vi.resetModules();
|
|
||||||
const { saveConfig } = await import('../../../src/utils/config.js');
|
|
||||||
|
|
||||||
saveConfig({ provider: 'deepseek' });
|
|
||||||
|
|
||||||
expect(fs.writeFileSync).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||||
|
|
||||||
// Mock fs
|
// Mock fs
|
||||||
vi.mock('fs', () => ({
|
vi.mock('fs', () => ({
|
||||||
@@ -8,31 +8,22 @@ vi.mock('fs', () => ({
|
|||||||
mkdirSync: vi.fn(),
|
mkdirSync: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
// Mock provider registry
|
||||||
|
vi.mock('../../../src/provider/index.js', () => ({
|
||||||
|
providerRegistry: {
|
||||||
|
getInfo: vi.fn(),
|
||||||
|
getConfig: vi.fn(),
|
||||||
|
},
|
||||||
|
resolveApiKey: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
|
import { providerRegistry, resolveApiKey } from '../../../src/provider/index.js';
|
||||||
import { getConfig, loadConfig, saveConfig, loadVisionConfig } from '../../../src/utils/config.js';
|
import { getConfig, loadConfig, saveConfig, loadVisionConfig } from '../../../src/utils/config.js';
|
||||||
|
|
||||||
describe('Config - 配置管理', () => {
|
describe('Config - 配置管理', () => {
|
||||||
const originalEnv = { ...process.env };
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
// 清理环境变量
|
|
||||||
delete process.env.AI_PROVIDER;
|
|
||||||
delete process.env.ANTHROPIC_API_KEY;
|
|
||||||
delete process.env.DEEPSEEK_API_KEY;
|
|
||||||
delete process.env.OPENAI_API_KEY;
|
|
||||||
delete process.env.AI_MODEL;
|
|
||||||
delete process.env.AI_MAX_TOKENS;
|
|
||||||
delete process.env.AI_BASE_URL;
|
|
||||||
delete process.env.VISION_PROVIDER;
|
|
||||||
delete process.env.VISION_MODEL;
|
|
||||||
delete process.env.VISION_API_KEY;
|
|
||||||
delete process.env.VISION_BASE_URL;
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
// 恢复环境变量
|
|
||||||
process.env = { ...originalEnv };
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getConfig - 获取原始配置', () => {
|
describe('getConfig - 获取原始配置', () => {
|
||||||
@@ -40,14 +31,12 @@ describe('Config - 配置管理', () => {
|
|||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
apiKey: 'test-key',
|
|
||||||
model: 'claude-3-opus',
|
model: 'claude-3-opus',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const config = getConfig();
|
const config = getConfig();
|
||||||
|
|
||||||
expect(config.provider).toBe('anthropic');
|
expect(config.provider).toBe('anthropic');
|
||||||
expect(config.apiKey).toBe('test-key');
|
|
||||||
expect(config.model).toBe('claude-3-opus');
|
expect(config.model).toBe('claude-3-opus');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -70,36 +59,41 @@ describe('Config - 配置管理', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('loadConfig - 加载完整配置', () => {
|
describe('loadConfig - 加载完整配置', () => {
|
||||||
it('从环境变量获取 Anthropic 配置', () => {
|
it('通过 ProviderRegistry 获取 Anthropic 配置', () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'env-anthropic-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('resolved-anthropic-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
expect(config.provider).toBe('anthropic');
|
expect(config.provider).toBe('anthropic');
|
||||||
expect(config.apiKey).toBe('env-anthropic-key');
|
expect(config.apiKey).toBe('resolved-anthropic-key');
|
||||||
expect(config.model).toBe('claude-sonnet-4-20250514');
|
expect(config.model).toBe('claude-sonnet-4-20250514');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('从环境变量获取 DeepSeek 配置', () => {
|
it('从配置文件获取 DeepSeek provider', () => {
|
||||||
process.env.AI_PROVIDER = 'deepseek';
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
process.env.DEEPSEEK_API_KEY = 'env-deepseek-key';
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
provider: 'deepseek',
|
||||||
|
}));
|
||||||
|
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('resolved-deepseek-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
expect(config.provider).toBe('deepseek');
|
expect(config.provider).toBe('deepseek');
|
||||||
expect(config.apiKey).toBe('env-deepseek-key');
|
expect(config.apiKey).toBe('resolved-deepseek-key');
|
||||||
expect(config.model).toBe('deepseek-chat');
|
expect(config.model).toBe('deepseek-chat');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('配置文件优先级高于默认值', () => {
|
it('配置文件中的 model 和 maxTokens', () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'env-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
model: 'custom-model',
|
model: 'custom-model',
|
||||||
maxTokens: 8192,
|
maxTokens: 8192,
|
||||||
}));
|
}));
|
||||||
|
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@@ -107,25 +101,9 @@ describe('Config - 配置管理', () => {
|
|||||||
expect(config.maxTokens).toBe(8192);
|
expect(config.maxTokens).toBe(8192);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('配置文件中的 provider 优先', () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'anthropic-key';
|
|
||||||
process.env.DEEPSEEK_API_KEY = 'deepseek-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
provider: 'deepseek',
|
|
||||||
deepseekApiKey: 'stored-deepseek-key',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
expect(config.provider).toBe('deepseek');
|
|
||||||
// 使用环境变量中的 API Key(优先级更高)
|
|
||||||
expect(config.apiKey).toBe('deepseek-key');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('包含系统提示词', () => {
|
it('包含系统提示词', () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
@@ -134,22 +112,37 @@ describe('Config - 配置管理', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('默认 maxTokens 为 4096', () => {
|
it('默认 maxTokens 为 4096', () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
expect(config.maxTokens).toBe(4096);
|
expect(config.maxTokens).toBe(4096);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('环境变量设置的 maxTokens', () => {
|
it('从配置文件获取 baseUrl', () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
process.env.AI_MAX_TOKENS = '16384';
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
baseUrl: 'https://custom.api.com/v1',
|
||||||
|
}));
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||||
|
|
||||||
const config = loadConfig();
|
const config = loadConfig();
|
||||||
|
|
||||||
expect(config.maxTokens).toBe(16384);
|
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',
|
||||||
|
});
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||||
|
|
||||||
|
const config = loadConfig();
|
||||||
|
|
||||||
|
expect(config.baseUrl).toBe('https://provider-config.api.com/v1');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -157,7 +150,7 @@ describe('Config - 配置管理', () => {
|
|||||||
it('创建目录并保存配置', () => {
|
it('创建目录并保存配置', () => {
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
|
||||||
saveConfig({ provider: 'anthropic', apiKey: 'new-key' });
|
saveConfig({ provider: 'anthropic' });
|
||||||
|
|
||||||
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
expect(fs.mkdirSync).toHaveBeenCalledWith(
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
@@ -173,18 +166,16 @@ describe('Config - 配置管理', () => {
|
|||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
provider: 'anthropic',
|
provider: 'anthropic',
|
||||||
apiKey: 'old-key',
|
|
||||||
model: 'old-model',
|
model: 'old-model',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
saveConfig({ apiKey: 'new-key' });
|
saveConfig({ model: 'new-model' });
|
||||||
|
|
||||||
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
|
||||||
const savedConfig = JSON.parse(writeCall[1] as string);
|
const savedConfig = JSON.parse(writeCall[1] as string);
|
||||||
|
|
||||||
expect(savedConfig.provider).toBe('anthropic'); // 保留
|
expect(savedConfig.provider).toBe('anthropic');
|
||||||
expect(savedConfig.apiKey).toBe('new-key'); // 更新
|
expect(savedConfig.model).toBe('new-model');
|
||||||
expect(savedConfig.model).toBe('old-model'); // 保留
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('目录已存在时不重新创建', () => {
|
it('目录已存在时不重新创建', () => {
|
||||||
@@ -193,75 +184,29 @@ describe('Config - 配置管理', () => {
|
|||||||
.mockReturnValueOnce(true); // 配置文件存在
|
.mockReturnValueOnce(true); // 配置文件存在
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
vi.mocked(fs.readFileSync).mockReturnValue('{}');
|
||||||
|
|
||||||
saveConfig({ apiKey: 'test' });
|
saveConfig({ provider: 'test' });
|
||||||
|
|
||||||
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
expect(fs.mkdirSync).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('loadConfig - baseUrl 支持', () => {
|
|
||||||
it('从环境变量获取 baseUrl', () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
process.env.AI_BASE_URL = 'https://custom.api.com/v1';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
expect(config.baseUrl).toBe('https://custom.api.com/v1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('从配置文件获取 baseUrl', () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
baseUrl: 'https://stored.api.com/v1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
expect(config.baseUrl).toBe('https://stored.api.com/v1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('环境变量 baseUrl 优先于配置文件', () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
process.env.AI_BASE_URL = 'https://env.api.com/v1';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
baseUrl: 'https://stored.api.com/v1',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
expect(config.baseUrl).toBe('https://env.api.com/v1');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('OpenAI provider 支持 baseUrl', () => {
|
|
||||||
process.env.AI_PROVIDER = 'openai';
|
|
||||||
process.env.OPENAI_API_KEY = 'test-openai-key';
|
|
||||||
process.env.AI_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
||||||
|
|
||||||
const config = loadConfig();
|
|
||||||
|
|
||||||
expect(config.provider).toBe('openai');
|
|
||||||
expect(config.baseUrl).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1');
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('loadVisionConfig - Vision 配置', () => {
|
describe('loadVisionConfig - Vision 配置', () => {
|
||||||
it('返回 null 当没有配置 Vision API Key', () => {
|
it('返回 null 当没有配置 Vision API Key', () => {
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue(undefined);
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
const visionConfig = loadVisionConfig();
|
||||||
|
|
||||||
expect(visionConfig).toBeNull();
|
expect(visionConfig).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('从环境变量获取 Vision 配置', () => {
|
it('通过 ProviderRegistry 获取 Vision 配置', () => {
|
||||||
process.env.VISION_PROVIDER = 'openai';
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
process.env.VISION_API_KEY = 'vision-api-key';
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
process.env.VISION_MODEL = 'gpt-4-vision';
|
visionProvider: 'openai',
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
visionModel: 'gpt-4-vision',
|
||||||
|
}));
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('vision-api-key');
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
const visionConfig = loadVisionConfig();
|
||||||
|
|
||||||
@@ -271,87 +216,35 @@ describe('Config - 配置管理', () => {
|
|||||||
expect(visionConfig?.model).toBe('gpt-4-vision');
|
expect(visionConfig?.model).toBe('gpt-4-vision');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('从配置文件获取 Vision 配置', () => {
|
it('默认使用 anthropic provider', () => {
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
vi.mocked(resolveApiKey).mockReturnValue('anthropic-key');
|
||||||
visionProvider: 'anthropic',
|
|
||||||
visionApiKey: 'stored-vision-key',
|
|
||||||
visionModel: 'claude-3-opus',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
const visionConfig = loadVisionConfig();
|
||||||
|
|
||||||
expect(visionConfig).not.toBeNull();
|
expect(visionConfig).not.toBeNull();
|
||||||
expect(visionConfig?.provider).toBe('anthropic');
|
expect(visionConfig?.provider).toBe('anthropic');
|
||||||
expect(visionConfig?.apiKey).toBe('stored-vision-key');
|
|
||||||
expect(visionConfig?.model).toBe('claude-3-opus');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('回退到对应 provider 的 API Key', () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'anthropic-main-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
|
||||||
|
|
||||||
expect(visionConfig).not.toBeNull();
|
|
||||||
expect(visionConfig?.provider).toBe('anthropic'); // 默认
|
|
||||||
expect(visionConfig?.apiKey).toBe('anthropic-main-key'); // 回退
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Vision baseUrl 配置', () => {
|
it('Vision baseUrl 配置', () => {
|
||||||
process.env.VISION_PROVIDER = 'openai';
|
vi.mocked(fs.existsSync).mockReturnValue(true);
|
||||||
process.env.VISION_API_KEY = 'vision-key';
|
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
||||||
process.env.VISION_BASE_URL = 'https://vision.api.com/v1';
|
visionBaseUrl: 'https://vision.api.com/v1',
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
}));
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('vision-key');
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
const visionConfig = loadVisionConfig();
|
||||||
|
|
||||||
expect(visionConfig?.baseUrl).toBe('https://vision.api.com/v1');
|
expect(visionConfig?.baseUrl).toBe('https://vision.api.com/v1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('从配置文件回退到 deepseek API key', () => {
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
visionProvider: 'deepseek',
|
|
||||||
deepseekApiKey: 'deepseek-stored-key',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
|
||||||
|
|
||||||
expect(visionConfig?.provider).toBe('deepseek');
|
|
||||||
expect(visionConfig?.apiKey).toBe('deepseek-stored-key');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('从配置文件回退到 openai API key', () => {
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(true);
|
|
||||||
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
|
|
||||||
visionProvider: 'openai',
|
|
||||||
openaiApiKey: 'openai-stored-key',
|
|
||||||
}));
|
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
|
||||||
|
|
||||||
expect(visionConfig?.provider).toBe('openai');
|
|
||||||
expect(visionConfig?.apiKey).toBe('openai-stored-key');
|
|
||||||
});
|
|
||||||
|
|
||||||
it('使用默认 Vision 模型', () => {
|
it('使用默认 Vision 模型', () => {
|
||||||
process.env.ANTHROPIC_API_KEY = 'test-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
vi.mocked(fs.existsSync).mockReturnValue(false);
|
||||||
|
vi.mocked(resolveApiKey).mockReturnValue('test-key');
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
const visionConfig = loadVisionConfig();
|
||||||
|
|
||||||
expect(visionConfig?.model).toBe('claude-sonnet-4-20250514');
|
expect(visionConfig?.model).toBe('claude-sonnet-4-20250514');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('Vision 专用 key 优先于 provider key', () => {
|
|
||||||
process.env.ANTHROPIC_API_KEY = 'anthropic-main-key';
|
|
||||||
process.env.VISION_API_KEY = 'vision-specific-key';
|
|
||||||
vi.mocked(fs.existsSync).mockReturnValue(false);
|
|
||||||
|
|
||||||
const visionConfig = loadVisionConfig();
|
|
||||||
|
|
||||||
expect(visionConfig?.apiKey).toBe('vision-specific-key');
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user