feat: 重构为 Monorepo 架构并实现 HTTP Server

架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
This commit is contained in:
2025-12-12 10:42:20 +08:00
parent 59dbed926e
commit 5e32375f0e
301 changed files with 3281 additions and 43 deletions
@@ -0,0 +1,298 @@
import { describe, it, expect, beforeEach } from 'vitest';
import type { Tool } from '../../../src/types/index.js';
import type { AgentInfo, AgentToolConfig } from '../../../src/agent/types.js';
/**
* 模拟 Agent 类中的 filterToolsByAgentConfig 逻辑
* 由于 Agent 类有复杂的依赖,我们提取核心过滤逻辑进行测试
*/
function filterToolsByAgentConfig(
tools: Tool[],
toolConfig: AgentToolConfig | undefined
): Tool[] {
if (!toolConfig) return tools;
let filteredTools = tools;
// 如果设置了 enabled 列表,只保留这些工具
if (toolConfig.enabled && toolConfig.enabled.length > 0) {
const enabledSet = new Set(toolConfig.enabled);
filteredTools = filteredTools.filter((t) => enabledSet.has(t.name));
}
// 如果设置了 disabled 列表,排除这些工具
if (toolConfig.disabled && toolConfig.disabled.length > 0) {
const disabledSet = new Set(toolConfig.disabled);
filteredTools = filteredTools.filter((t) => !disabledSet.has(t.name));
}
// 如果禁止嵌套 Task,移除 task 工具
if (toolConfig.noTask) {
filteredTools = filteredTools.filter((t) => t.name !== 'task');
}
return filteredTools;
}
// 创建测试用的 mock 工具
function createMockTool(name: string): Tool {
return {
name,
description: `Mock tool: ${name}`,
parameters: { type: 'object', properties: {}, required: [] },
execute: async () => ({ success: true, output: 'mock' }),
};
}
describe('Agent 工具过滤 - filterToolsByAgentConfig', () => {
let allTools: Tool[];
beforeEach(() => {
// 创建一组测试工具
allTools = [
createMockTool('read_file'),
createMockTool('write_file'),
createMockTool('bash'),
createMockTool('task'),
createMockTool('tool_search'),
createMockTool('glob'),
createMockTool('grep'),
];
});
describe('无过滤配置', () => {
it('toolConfig 为 undefined 时返回所有工具', () => {
const result = filterToolsByAgentConfig(allTools, undefined);
expect(result).toHaveLength(allTools.length);
expect(result).toEqual(allTools);
});
it('toolConfig 为空对象时返回所有工具', () => {
const result = filterToolsByAgentConfig(allTools, {});
expect(result).toHaveLength(allTools.length);
});
});
describe('enabled 白名单过滤', () => {
it('只保留 enabled 列表中的工具', () => {
const config: AgentToolConfig = {
enabled: ['read_file', 'glob', 'grep'],
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(3);
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob', 'grep']);
});
it('enabled 为空数组时返回空列表', () => {
const config: AgentToolConfig = {
enabled: [],
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(allTools.length); // 空数组不触发过滤
});
it('enabled 中不存在的工具被忽略', () => {
const config: AgentToolConfig = {
enabled: ['read_file', 'non_existent_tool'],
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('read_file');
});
});
describe('disabled 黑名单过滤', () => {
it('排除 disabled 列表中的工具', () => {
const config: AgentToolConfig = {
disabled: ['bash', 'write_file'],
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(5);
expect(result.map((t) => t.name)).not.toContain('bash');
expect(result.map((t) => t.name)).not.toContain('write_file');
});
it('disabled 为空数组时返回所有工具', () => {
const config: AgentToolConfig = {
disabled: [],
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(allTools.length);
});
it('disabled 中不存在的工具被忽略', () => {
const config: AgentToolConfig = {
disabled: ['non_existent_tool'],
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(allTools.length);
});
});
describe('noTask 过滤', () => {
it('noTask=true 时移除 task 工具', () => {
const config: AgentToolConfig = {
noTask: true,
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(6);
expect(result.map((t) => t.name)).not.toContain('task');
});
it('noTask=false 时保留 task 工具', () => {
const config: AgentToolConfig = {
noTask: false,
};
const result = filterToolsByAgentConfig(allTools, config);
expect(result).toHaveLength(allTools.length);
expect(result.map((t) => t.name)).toContain('task');
});
it('noTask 未设置时保留 task 工具', () => {
const config: AgentToolConfig = {};
const result = filterToolsByAgentConfig(allTools, config);
expect(result.map((t) => t.name)).toContain('task');
});
});
describe('组合过滤', () => {
it('enabled + noTask 组合', () => {
const config: AgentToolConfig = {
enabled: ['read_file', 'task', 'glob'],
noTask: true,
};
const result = filterToolsByAgentConfig(allTools, config);
// enabled 先过滤为 [read_file, task, glob]
// noTask 再移除 task
expect(result).toHaveLength(2);
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob']);
});
it('disabled + noTask 组合', () => {
const config: AgentToolConfig = {
disabled: ['bash'],
noTask: true,
};
const result = filterToolsByAgentConfig(allTools, config);
// 原始 7 个工具,移除 bash 和 task
expect(result).toHaveLength(5);
expect(result.map((t) => t.name)).not.toContain('bash');
expect(result.map((t) => t.name)).not.toContain('task');
});
it('enabled + disabled 组合(enabled 优先)', () => {
const config: AgentToolConfig = {
enabled: ['read_file', 'bash', 'glob'],
disabled: ['bash'], // bash 在 enabled 中,也在 disabled 中
};
const result = filterToolsByAgentConfig(allTools, config);
// enabled 先过滤为 [read_file, bash, glob]
// disabled 再移除 bash
expect(result).toHaveLength(2);
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob']);
});
it('enabled + disabled + noTask 全部组合', () => {
const config: AgentToolConfig = {
enabled: ['read_file', 'write_file', 'task', 'glob'],
disabled: ['write_file'],
noTask: true,
};
const result = filterToolsByAgentConfig(allTools, config);
// enabled: [read_file, write_file, task, glob]
// disabled: 移除 write_file -> [read_file, task, glob]
// noTask: 移除 task -> [read_file, glob]
expect(result).toHaveLength(2);
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob']);
});
});
});
describe('AgentInfo 工具配置集成', () => {
it('explore agent 典型配置:只读工具', () => {
const exploreAgent: AgentInfo = {
name: 'explore',
description: '代码探索 Agent',
mode: 'subagent',
tools: {
enabled: ['read_file', 'glob', 'grep', 'tool_search'],
noTask: true,
},
};
const allTools = [
createMockTool('read_file'),
createMockTool('write_file'),
createMockTool('bash'),
createMockTool('task'),
createMockTool('glob'),
createMockTool('grep'),
createMockTool('tool_search'),
];
const result = filterToolsByAgentConfig(allTools, exploreAgent.tools);
expect(result).toHaveLength(4);
expect(result.map((t) => t.name).sort()).toEqual(['glob', 'grep', 'read_file', 'tool_search']);
});
it('code-reviewer agent 典型配置:禁用写操作', () => {
const reviewerAgent: AgentInfo = {
name: 'code-reviewer',
description: '代码审查 Agent',
mode: 'subagent',
tools: {
disabled: ['write_file', 'bash'],
noTask: true,
},
};
const allTools = [
createMockTool('read_file'),
createMockTool('write_file'),
createMockTool('bash'),
createMockTool('task'),
createMockTool('glob'),
createMockTool('grep'),
];
const result = filterToolsByAgentConfig(allTools, reviewerAgent.tools);
// 移除 write_file, bash, task
expect(result).toHaveLength(3);
expect(result.map((t) => t.name).sort()).toEqual(['glob', 'grep', 'read_file']);
});
it('build agent 典型配置:允许嵌套 Task', () => {
const buildAgent: AgentInfo = {
name: 'build',
description: '构建 Agent',
mode: 'primary',
tools: {
noTask: false, // 明确允许 task
},
};
const allTools = [
createMockTool('read_file'),
createMockTool('write_file'),
createMockTool('bash'),
createMockTool('task'),
];
const result = filterToolsByAgentConfig(allTools, buildAgent.tools);
expect(result).toHaveLength(4);
expect(result.map((t) => t.name)).toContain('task');
});
});
+411
View File
@@ -0,0 +1,411 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { Agent } from '../../../src/core/agent.js';
import { ToolRegistry } from '../../../src/tools/registry.js';
import { SessionManager } from '../../../src/session/index.js';
import type { AgentConfig, Tool } from '../../../src/types/index.js';
import type { AgentInfo } from '../../../src/agent/types.js';
// Mock providers
vi.mock('../../../src/core/providers.js', () => ({
getModelFactory: vi.fn(() => {
return (model: string) => ({
modelId: `mock:${model}`,
doGenerate: vi.fn(),
doStream: vi.fn(),
});
}),
}));
// Mock AI SDK
vi.mock('ai', () => ({
generateText: vi.fn().mockResolvedValue({
text: 'Mock response',
response: { messages: [] },
steps: [],
}),
streamText: vi.fn(() => ({
textStream: (async function* () {
yield 'Mock ';
yield 'streamed ';
yield 'response';
})(),
response: Promise.resolve({ messages: [] }),
})),
stepCountIs: vi.fn(() => () => false),
}));
// Mock agent registry and executor
vi.mock('../../../src/agent/index.js', () => ({
agentRegistry: {
get: vi.fn(),
},
AgentExecutor: vi.fn().mockImplementation(() => ({
execute: vi.fn().mockResolvedValue({
success: true,
text: 'Vision analysis result',
steps: 1,
sessionId: 'test',
}),
})),
}));
// Mock vision config
vi.mock('../../../src/utils/config.js', () => ({
loadVisionConfig: vi.fn(() => null),
}));
// Create mock tool
function createMockTool(name: string, category = 'core'): Tool & { metadata?: { deferLoading?: boolean } } {
return {
name,
description: `Mock ${name} tool`,
parameters: { type: 'object', properties: {}, required: [] },
execute: vi.fn().mockResolvedValue({ success: true, output: `${name} result` }),
metadata: { deferLoading: category !== 'core' },
};
}
// Default test config
const testConfig: AgentConfig = {
provider: 'anthropic',
apiKey: 'test-api-key',
model: 'claude-3-sonnet',
systemPrompt: 'You are a helpful assistant.',
maxTokens: 4096,
};
describe('Agent', () => {
let agent: Agent;
let registry: ToolRegistry;
beforeEach(() => {
vi.clearAllMocks();
registry = new ToolRegistry();
// Register some tools
registry.register(createMockTool('tool_search', 'core') as any);
registry.register(createMockTool('bash', 'core') as any);
registry.register(createMockTool('read_file', 'filesystem') as any);
registry.register(createMockTool('write_file', 'filesystem') as any);
agent = new Agent(testConfig);
agent.setRegistry(registry);
});
describe('constructor', () => {
it('初始化 Agent 配置', () => {
const config = agent.getConfig();
expect(config.provider).toBe('anthropic');
expect(config.model).toBe('claude-3-sonnet');
expect(config.systemPrompt).toBe('You are a helpful assistant.');
});
it('支持自定义压缩配置', () => {
const agentWithCompression = new Agent(testConfig, {
maxContextTokens: 50000,
compressionThreshold: 0.7,
});
expect(agentWithCompression.getCompressionManager()).toBeDefined();
});
});
describe('setRegistry', () => {
it('设置工具注册表', () => {
const newAgent = new Agent(testConfig);
expect(newAgent.getToolCount()).toEqual({ core: 0, discovered: 0, total: 0 });
newAgent.setRegistry(registry);
const counts = newAgent.getToolCount();
expect(counts.core).toBeGreaterThan(0);
});
});
describe('setSessionManager', () => {
it('设置会话管理器', async () => {
// Mock SessionManager
const mockSessionManager = {
getSession: vi.fn().mockReturnValue(null),
setMessages: vi.fn(),
setDiscoveredTools: vi.fn(),
newSession: vi.fn(),
} as unknown as SessionManager;
agent.setSessionManager(mockSessionManager);
expect(agent.getSessionManager()).toBe(mockSessionManager);
});
it('从会话恢复状态', async () => {
// Mock SessionManager with existing session
const mockSessionManager = {
getSession: vi.fn().mockReturnValue({
id: 'test-session',
messages: [{ role: 'user', content: 'Hello' }],
discoveredTools: ['read_file', 'write_file'],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}),
setMessages: vi.fn(),
setDiscoveredTools: vi.fn(),
newSession: vi.fn(),
} as unknown as SessionManager;
agent.setSessionManager(mockSessionManager);
const counts = agent.getToolCount();
expect(counts.discovered).toBe(2);
});
});
describe('getToolCount', () => {
it('返回工具数量统计', () => {
const counts = agent.getToolCount();
expect(counts).toHaveProperty('core');
expect(counts).toHaveProperty('discovered');
expect(counts).toHaveProperty('total');
expect(counts.total).toBe(counts.core + counts.discovered);
});
it('registry 未设置时返回 0', () => {
const newAgent = new Agent(testConfig);
const counts = newAgent.getToolCount();
expect(counts).toEqual({ core: 0, discovered: 0, total: 0 });
});
});
describe('getHistory', () => {
it('初始历史为空', () => {
expect(agent.getHistory()).toEqual([]);
});
});
describe('clearHistory', () => {
it('清空对话历史和发现的工具', async () => {
// Add some history by calling chat (mocked)
await agent.chat('Hello');
await agent.clearHistory();
expect(agent.getHistory()).toEqual([]);
expect(agent.getToolCount().discovered).toBe(0);
});
it('有会话管理器时创建新会话', async () => {
const mockSessionManager = {
getSession: vi.fn().mockReturnValue(null),
setMessages: vi.fn(),
setDiscoveredTools: vi.fn(),
newSession: vi.fn().mockResolvedValue(undefined),
} as unknown as SessionManager;
agent.setSessionManager(mockSessionManager);
await agent.clearHistory();
expect(mockSessionManager.newSession).toHaveBeenCalled();
});
});
describe('Agent Mode', () => {
const exploreAgent: AgentInfo = {
name: 'explore',
description: '代码探索 Agent',
mode: 'subagent',
prompt: 'You are an explore agent.',
tools: {
enabled: ['read_file', 'tool_search'],
noTask: true,
},
};
it('设置 Agent 模式', () => {
agent.setAgentMode(exploreAgent);
expect(agent.getAgentMode()).toBe(exploreAgent);
expect(agent.getAgentModeName()).toBe('explore');
});
it('切换 Agent 时更新 system prompt', () => {
agent.setAgentMode(exploreAgent);
expect(agent.getConfig().systemPrompt).toBe('You are an explore agent.');
});
it('切换回 default 时恢复原始 prompt', () => {
agent.setAgentMode(exploreAgent);
agent.setAgentMode(null);
expect(agent.getAgentModeName()).toBe('default');
expect(agent.getConfig().systemPrompt).toBe('You are a helpful assistant.');
});
it('Agent 没有自定义 prompt 时保持原始 prompt', () => {
const agentWithoutPrompt: AgentInfo = {
name: 'simple',
description: 'Simple agent',
mode: 'subagent',
};
agent.setAgentMode(agentWithoutPrompt);
expect(agent.getConfig().systemPrompt).toBe('You are a helpful assistant.');
});
});
describe('supportsVision', () => {
it('Anthropic Claude-3 支持 vision', () => {
const claudeAgent = new Agent({
...testConfig,
provider: 'anthropic',
model: 'claude-3-opus',
});
expect(claudeAgent.supportsVision()).toBe(true);
});
it('Anthropic Claude-4 支持 vision', () => {
const claudeAgent = new Agent({
...testConfig,
provider: 'anthropic',
model: 'claude-4-sonnet',
});
expect(claudeAgent.supportsVision()).toBe(true);
});
it('OpenAI GPT-4 支持 vision', () => {
const gptAgent = new Agent({
...testConfig,
provider: 'openai',
model: 'gpt-4-turbo',
});
expect(gptAgent.supportsVision()).toBe(true);
});
it('DeepSeek 不支持 vision', () => {
const deepseekAgent = new Agent({
...testConfig,
provider: 'deepseek',
model: 'deepseek-chat',
});
expect(deepseekAgent.supportsVision()).toBe(false);
});
it('旧版 Claude 不支持 vision', () => {
const oldClaudeAgent = new Agent({
...testConfig,
provider: 'anthropic',
model: 'claude-2',
});
expect(oldClaudeAgent.supportsVision()).toBe(false);
});
it('GPT-3.5 不支持 vision', () => {
const gpt35Agent = new Agent({
...testConfig,
provider: 'openai',
model: 'gpt-3.5-turbo',
});
expect(gpt35Agent.supportsVision()).toBe(false);
});
});
describe('getContextUsage', () => {
it('返回 token 使用情况', () => {
const usage = agent.getContextUsage();
expect(usage).toHaveProperty('input');
expect(usage).toHaveProperty('contextLimit');
expect(usage).toHaveProperty('available');
expect(usage).toHaveProperty('usagePercent');
});
});
describe('getContextUsageFormatted', () => {
it('返回格式化的使用情况字符串', () => {
const formatted = agent.getContextUsageFormatted();
expect(typeof formatted).toBe('string');
expect(formatted).toContain('k');
});
});
describe('getCompressionManager', () => {
it('返回压缩管理器实例', () => {
const manager = agent.getCompressionManager();
expect(manager).toBeDefined();
expect(manager).toHaveProperty('compress');
expect(manager).toHaveProperty('forceCompress');
});
});
describe('compactHistory', () => {
it('手动压缩对话历史', async () => {
const result = await agent.compactHistory();
expect(result).toHaveProperty('freedTokens');
expect(result).toHaveProperty('type');
});
});
describe('getConfig', () => {
it('返回配置副本', () => {
const config = agent.getConfig();
expect(config).toEqual(testConfig);
// 修改返回值不影响原始配置
config.model = 'modified';
expect(agent.getConfig().model).toBe('claude-3-sonnet');
});
});
});
describe('Agent - getAvailableTools error handling', () => {
it('registry 未设置时抛出错误', () => {
const agent = new Agent(testConfig);
// 使用 getToolCount 间接测试(因为 getAvailableTools 是 private
// 当 registry 为 null 时,getToolCount 返回 0
const counts = agent.getToolCount();
expect(counts.total).toBe(0);
});
});
describe('Agent - chat with images', () => {
let agent: Agent;
let registry: ToolRegistry;
beforeEach(() => {
vi.clearAllMocks();
registry = new ToolRegistry();
registry.register(createMockTool('tool_search', 'core') as any);
agent = new Agent(testConfig);
agent.setRegistry(registry);
});
it('支持 vision 的模型直接处理图片', async () => {
const visionAgent = new Agent({
...testConfig,
model: 'claude-3-opus',
});
visionAgent.setRegistry(registry);
const result = await visionAgent.chat({
text: 'What is in this image?',
images: [
{ data: 'base64data', mimeType: 'image/png' },
],
});
expect(result).toBeDefined();
});
it('不支持 vision 时返回错误消息(Vision 未配置)', async () => {
const deepseekAgent = new Agent({
...testConfig,
provider: 'deepseek',
model: 'deepseek-chat',
});
deepseekAgent.setRegistry(registry);
const result = await deepseekAgent.chat({
text: 'What is in this image?',
images: [
{ data: 'base64data', mimeType: 'image/png' },
],
});
expect(result).toContain('无法处理图片');
});
});
@@ -0,0 +1,264 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { getModelFactory, providers } from '../../../src/core/providers.js';
// Mock AI SDK providers
vi.mock('@ai-sdk/anthropic', () => ({
createAnthropic: vi.fn(() => {
const modelFn = (model: string) => ({ modelId: `anthropic:${model}` });
return modelFn;
}),
}));
vi.mock('@ai-sdk/deepseek', () => ({
createDeepSeek: vi.fn(() => {
const modelFn = (model: string) => ({ modelId: `deepseek:${model}` });
return modelFn;
}),
}));
vi.mock('@ai-sdk/openai', () => ({
createOpenAI: vi.fn(() => {
const modelFn = (model: string) => ({ modelId: `openai:${model}` });
return modelFn;
}),
}));
vi.mock('qwen-ai-provider-v5', () => ({
createQwen: vi.fn(() => {
const modelFn = (model: string) => ({ modelId: `qwen:${model}` });
return modelFn;
}),
}));
import { createAnthropic } from '@ai-sdk/anthropic';
import { createDeepSeek } from '@ai-sdk/deepseek';
import { createOpenAI } from '@ai-sdk/openai';
import { createQwen } from 'qwen-ai-provider-v5';
describe('providers', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('providers 注册表', () => {
it('包含所有支持的 provider 类型', () => {
expect(providers).toHaveProperty('anthropic');
expect(providers).toHaveProperty('deepseek');
expect(providers).toHaveProperty('openai');
});
it('每个 provider 是一个工厂函数', () => {
expect(typeof providers.anthropic).toBe('function');
expect(typeof providers.deepseek).toBe('function');
expect(typeof providers.openai).toBe('function');
});
});
describe('Anthropic provider', () => {
it('创建 Anthropic 客户端', () => {
const factory = providers.anthropic({
apiKey: 'test-api-key',
});
expect(createAnthropic).toHaveBeenCalledWith({
apiKey: 'test-api-key',
baseURL: undefined,
});
expect(typeof factory).toBe('function');
});
it('支持自定义 baseUrl', () => {
providers.anthropic({
apiKey: 'test-api-key',
baseUrl: 'https://custom.anthropic.com',
});
expect(createAnthropic).toHaveBeenCalledWith({
apiKey: 'test-api-key',
baseURL: 'https://custom.anthropic.com',
});
});
it('返回的工厂函数可以创建模型', () => {
const factory = providers.anthropic({
apiKey: 'test-api-key',
});
const model = factory('claude-3-opus');
expect(model).toEqual({ modelId: 'anthropic:claude-3-opus' });
});
});
describe('DeepSeek provider', () => {
it('创建 DeepSeek 客户端', () => {
const factory = providers.deepseek({
apiKey: 'test-deepseek-key',
});
expect(createDeepSeek).toHaveBeenCalledWith({
apiKey: 'test-deepseek-key',
baseURL: undefined,
});
expect(typeof factory).toBe('function');
});
it('支持自定义 baseUrl', () => {
providers.deepseek({
apiKey: 'test-deepseek-key',
baseUrl: 'https://custom.deepseek.com',
});
expect(createDeepSeek).toHaveBeenCalledWith({
apiKey: 'test-deepseek-key',
baseURL: 'https://custom.deepseek.com',
});
});
it('返回的工厂函数可以创建模型', () => {
const factory = providers.deepseek({
apiKey: 'test-deepseek-key',
});
const model = factory('deepseek-chat');
expect(model).toEqual({ modelId: 'deepseek:deepseek-chat' });
});
});
describe('OpenAI provider', () => {
it('创建 OpenAI 客户端(标准 URL', () => {
const factory = providers.openai({
apiKey: 'test-openai-key',
});
expect(createOpenAI).toHaveBeenCalledWith({
apiKey: 'test-openai-key',
baseURL: undefined,
});
expect(createQwen).not.toHaveBeenCalled();
expect(typeof factory).toBe('function');
});
it('支持自定义 baseUrl(非 DashScope', () => {
providers.openai({
apiKey: 'test-openai-key',
baseUrl: 'https://custom.openai.com/v1',
});
expect(createOpenAI).toHaveBeenCalledWith({
apiKey: 'test-openai-key',
baseURL: 'https://custom.openai.com/v1',
});
expect(createQwen).not.toHaveBeenCalled();
});
it('返回的工厂函数可以创建模型', () => {
const factory = providers.openai({
apiKey: 'test-openai-key',
});
const model = factory('gpt-4');
expect(model).toEqual({ modelId: 'openai:gpt-4' });
});
});
describe('DashScope (Qwen) 检测', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('检测 dashscope URL 并使用 Qwen provider', () => {
providers.openai({
apiKey: 'test-qwen-key',
baseUrl: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
});
expect(createQwen).toHaveBeenCalledWith({
apiKey: 'test-qwen-key',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
});
expect(createOpenAI).not.toHaveBeenCalled();
});
it('检测包含 dashscope 的任意 URL', () => {
providers.openai({
apiKey: 'test-qwen-key',
baseUrl: 'https://api.dashscope.example.com/v1',
});
expect(createQwen).toHaveBeenCalled();
expect(createOpenAI).not.toHaveBeenCalled();
});
it('Qwen 工厂函数可以创建模型', () => {
const factory = providers.openai({
apiKey: 'test-qwen-key',
baseUrl: 'https://dashscope.aliyuncs.com/v1',
});
const model = factory('qwen-turbo');
expect(model).toEqual({ modelId: 'qwen:qwen-turbo' });
});
});
});
describe('getModelFactory', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('获取 Anthropic 模型工厂', () => {
const factory = getModelFactory('anthropic', {
apiKey: 'test-key',
});
expect(typeof factory).toBe('function');
expect(createAnthropic).toHaveBeenCalled();
});
it('获取 DeepSeek 模型工厂', () => {
const factory = getModelFactory('deepseek', {
apiKey: 'test-key',
});
expect(typeof factory).toBe('function');
expect(createDeepSeek).toHaveBeenCalled();
});
it('获取 OpenAI 模型工厂', () => {
const factory = getModelFactory('openai', {
apiKey: 'test-key',
});
expect(typeof factory).toBe('function');
expect(createOpenAI).toHaveBeenCalled();
});
it('传递正确的选项给 provider', () => {
getModelFactory('anthropic', {
apiKey: 'my-api-key',
baseUrl: 'https://my-proxy.com',
});
expect(createAnthropic).toHaveBeenCalledWith({
apiKey: 'my-api-key',
baseURL: 'https://my-proxy.com',
});
});
it('不支持的 provider 抛出错误', () => {
expect(() => {
getModelFactory('unsupported' as any, {
apiKey: 'test-key',
});
}).toThrow('不支持的 provider: unsupported');
});
it('返回的工厂函数可以创建模型实例', () => {
const factory = getModelFactory('anthropic', {
apiKey: 'test-key',
});
const model = factory('claude-3-sonnet');
expect(model).toEqual({ modelId: 'anthropic:claude-3-sonnet' });
});
});