refactor(config): 移除环境变量依赖,统一使用 Provider 配置系统

- 移除 .env.example 文件
- 简化 resolveApiKey 函数,只从配置文件读取 API Key
- 重构 loadConfig/loadVisionConfig/loadSummaryConfig 使用 ProviderRegistry
- 更新测试以 mock Provider 系统
This commit is contained in:
2025-12-14 21:29:36 +08:00
parent c9d0cbce5b
commit 9e011476c8
6 changed files with 143 additions and 451 deletions
@@ -11,6 +11,15 @@ vi.mock('fs', () => ({
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');
@@ -25,18 +34,6 @@ describe('Config - 配置管理扩展测试', () => {
beforeEach(() => {
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(() => {
@@ -48,7 +45,6 @@ describe('Config - 配置管理扩展测试', () => {
it('配置文件不存在返回空对象', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
// 需要重新导入以获取最新的 mock
vi.resetModules();
const { getConfig } = await import('../../../src/utils/config.js');
@@ -60,7 +56,6 @@ describe('Config - 配置管理扩展测试', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
provider: 'anthropic',
apiKey: 'test-key',
model: 'claude-sonnet-4-20250514',
}));
@@ -69,7 +64,7 @@ describe('Config - 配置管理扩展测试', () => {
const config = getConfig();
expect(config.provider).toBe('anthropic');
expect(config.apiKey).toBe('test-key');
expect(config.model).toBe('claude-sonnet-4-20250514');
});
it('配置文件解析错误返回空对象', async () => {
@@ -85,100 +80,61 @@ describe('Config - 配置管理扩展测试', () => {
});
describe('loadConfig - 加载配置', () => {
it('优先使用环境变量中的 API Key', async () => {
process.env.ANTHROPIC_API_KEY = 'env-api-key';
it('通过 ProviderRegistry 获取 API Key', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
provider: 'anthropic',
apiKey: 'file-api-key',
}));
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('env-api-key');
expect(config.apiKey).toBe('resolved-api-key');
});
it('使用配置文件中的 provider', async () => {
process.env.DEEPSEEK_API_KEY = 'deepseek-key';
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('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 () => {
process.env.ANTHROPIC_API_KEY = 'test-key';
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('环境变量 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 () => {
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();
});
@@ -189,66 +145,29 @@ describe('Config - 配置管理扩展测试', () => {
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('使用环境变量配置', 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 () => {
it('从配置文件获取 Vision 设置', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
visionProvider: 'openai',
visionModel: 'gpt-4o',
visionApiKey: 'config-vision-key',
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');
@@ -256,19 +175,18 @@ describe('Config - 配置管理扩展测试', () => {
expect(config!.apiKey).toBe('config-vision-key');
});
it('DeepSeek provider 回退到 deepseekApiKey', async () => {
process.env.DEEPSEEK_API_KEY = 'deepseek-key';
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
visionProvider: 'deepseek',
}));
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!.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.readFileSync).mockReturnValue(JSON.stringify({
provider: 'anthropic',
apiKey: 'existing-key',
}));
vi.resetModules();
@@ -322,17 +239,5 @@ describe('Config - 配置管理扩展测试', () => {
const savedConfig = JSON.parse(writeCall[1] as string);
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();
});
});
});