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,205 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { skillTool, updateSkillDescription } from '../../../../src/tools/skill/skill.js';
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
import type { Skill } from '../../../../src/skills/types.js';
// Mock loader to prevent file system access
vi.mock('../../../../src/skills/loader.js', () => ({
skillLoader: {
loadFromDirectory: vi.fn().mockResolvedValue([]),
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
},
}));
// Mock builtin skills
vi.mock('../../../../src/skills/builtin/index.js', () => ({
builtinSkills: [
{
name: 'code-review',
description: '代码审查',
promptTemplate: '请审查以下代码:\n\n{{code}}\n\n重点:{{focus}}',
parameters: {
code: { type: 'string', required: true, description: '代码' },
focus: { type: 'string', required: false, default: '代码质量', description: '审查重点' },
},
category: 'development',
source: 'builtin',
enabled: true,
},
{
name: 'disabled-skill',
description: '禁用的 Skill',
promptTemplate: '不应该执行',
source: 'builtin',
enabled: false,
},
],
}));
describe('skillTool - Skill 工具', () => {
beforeEach(async () => {
vi.clearAllMocks();
resetSkillRegistry();
// 初始化注册表
const registry = getSkillRegistry();
await registry.initialize();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(skillTool.name).toBe('skill');
});
it('有正确的元数据', () => {
expect(skillTool.metadata.category).toBe('agent');
expect(skillTool.metadata.keywords).toContain('skill');
});
it('skill_name 参数是必须的', () => {
expect(skillTool.parameters.skill_name.required).toBe(true);
});
it('params 参数是可选的', () => {
expect(skillTool.parameters.params.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功执行 Skill 并渲染模板', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: {
code: 'function add(a, b) { return a + b; }',
focus: '性能',
},
});
expect(result.success).toBe(true);
expect(result.output).toContain('function add');
expect(result.output).toContain('性能');
expect(result.metadata?.skill).toBe('code-review');
});
it('使用默认参数值', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: {
code: 'const x = 1;',
},
});
expect(result.success).toBe(true);
expect(result.output).toContain('代码质量'); // 默认值
});
it('Skill 不存在时返回错误', async () => {
const result = await skillTool.execute({
skill_name: 'non-existent',
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('不存在');
});
it('Skill 不存在时提供建议', async () => {
const result = await skillTool.execute({
skill_name: 'code-rev', // 类似 code-review
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('code-review'); // 建议
});
it('缺少必需参数时返回错误', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: {}, // 缺少 code 参数
});
expect(result.success).toBe(false);
expect(result.error).toContain('缺少必需参数');
});
it('禁用的 Skill 返回错误', async () => {
const result = await skillTool.execute({
skill_name: 'disabled-skill',
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('禁用');
});
it('返回正确的 metadata', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
params: { code: 'test' },
});
expect(result.metadata?.skill).toBe('code-review');
expect(result.metadata?.category).toBe('development');
expect(result.metadata?.source).toBe('builtin');
});
});
describe('工具描述', () => {
it('更新后描述包含可用 Skill 列表', () => {
// beforeEach 中已初始化 registry,现在更新描述
updateSkillDescription();
expect(skillTool.description).toContain('code-review');
expect(skillTool.description).toContain('代码审查');
});
it('更新后描述包含分类信息', () => {
updateSkillDescription();
expect(skillTool.description).toContain('[development]');
});
it('更新后描述包含使用示例', () => {
updateSkillDescription();
expect(skillTool.description).toContain('使用示例');
expect(skillTool.description).toContain('skill_name');
});
it('禁用的 Skill 不在描述中', () => {
updateSkillDescription();
expect(skillTool.description).not.toContain('disabled-skill');
});
});
describe('updateSkillDescription', () => {
it('调用后描述被更新', async () => {
updateSkillDescription();
// 描述应该包含当前注册的 Skill
expect(skillTool.description).toContain('code-review');
expect(typeof skillTool.description).toBe('string');
});
});
describe('边界情况', () => {
it('params 为 undefined 时使用空对象', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
// 不提供 params
});
expect(result.success).toBe(false);
expect(result.error).toContain('缺少必需参数');
});
it('skill_name 为空字符串时返回错误', async () => {
const result = await skillTool.execute({
skill_name: '',
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('不存在');
});
});
});
@@ -0,0 +1,185 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { skillSearchTool } from '../../../../src/tools/skill/skill_search.js';
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
// Mock loader to prevent file system access
vi.mock('../../../../src/skills/loader.js', () => ({
skillLoader: {
loadFromDirectory: vi.fn().mockResolvedValue([]),
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
},
}));
// Mock builtin skills with various scenarios
vi.mock('../../../../src/skills/builtin/index.js', () => ({
builtinSkills: [
{
name: 'builtin-skill',
description: '内置技能',
promptTemplate: '内置',
keywords: ['builtin'],
category: 'development',
source: 'builtin',
enabled: true,
},
{
name: 'user-skill',
description: '用户定义的技能',
promptTemplate: '用户',
keywords: ['user', 'custom'],
category: 'development',
source: 'user', // 非 builtin 来源
enabled: true,
},
{
name: 'project-skill',
description: '项目技能',
promptTemplate: '项目',
keywords: ['project'],
source: 'project', // 非 builtin 来源,无分类
enabled: true,
// 无 category - 测试 uncategorized 分支
},
{
name: 'skill-with-params',
description: '带参数的技能',
promptTemplate: '带参数',
keywords: ['params'],
category: 'tools',
source: 'builtin',
enabled: true,
parameters: {
file: { type: 'string', description: '文件路径', required: true },
verbose: { type: 'boolean', description: '详细输出', required: false },
},
},
],
}));
describe('skillSearchTool - 扩展测试', () => {
beforeEach(async () => {
vi.clearAllMocks();
resetSkillRegistry();
const registry = getSkillRegistry();
await registry.initialize();
});
describe('list_all - 来源标记', () => {
it('非 builtin 来源的 Skill 显示来源标记', async () => {
const result = await skillSearchTool.execute({ list_all: true });
expect(result.success).toBe(true);
// user-skill 和 project-skill 应该显示来源标记
expect(result.output).toContain('[user]');
expect(result.output).toContain('[project]');
// builtin-skill 不应该显示 [builtin] 标记
expect(result.output).not.toContain('[builtin]');
});
});
describe('list_all - 未分类的 Skills', () => {
it('显示未分类的 Skills 在"其他"分组下', async () => {
const result = await skillSearchTool.execute({ list_all: true });
expect(result.success).toBe(true);
// project-skill 没有分类,应该出现在"其他"分组
expect(result.output).toContain('## 其他');
expect(result.output).toContain('project-skill');
});
});
describe('按分类筛选 - 参数显示', () => {
it('显示 Skill 的参数列表', async () => {
const result = await skillSearchTool.execute({ category: 'tools' });
expect(result.success).toBe(true);
expect(result.output).toContain('skill-with-params');
expect(result.output).toContain('参数:');
expect(result.output).toContain('file');
expect(result.output).toContain('verbose');
});
it('无参数的 Skill 不显示参数行', async () => {
const result = await skillSearchTool.execute({ category: 'development' });
expect(result.success).toBe(true);
expect(result.output).toContain('builtin-skill');
// 检查这个技能后面没有参数行
const lines = result.output.split('\n');
const builtinIndex = lines.findIndex((l) => l.includes('builtin-skill'));
if (builtinIndex !== -1 && builtinIndex + 1 < lines.length) {
// builtin-skill 没有参数,下一行不应该是"参数:"
const nextLine = lines[builtinIndex + 1];
expect(nextLine).not.toMatch(/^\s*参数:/);
}
});
});
describe('搜索结果 - 分类显示', () => {
it('搜索结果包含分类信息', async () => {
const result = await skillSearchTool.execute({ query: 'builtin' });
expect(result.success).toBe(true);
expect(result.output).toContain('分类:');
expect(result.output).toContain('development');
});
it('无分类的 Skill 搜索结果不显示分类', async () => {
const result = await skillSearchTool.execute({ query: 'project' });
expect(result.success).toBe(true);
expect(result.output).toContain('project-skill');
// project-skill 没有分类
const lines = result.output.split('\n');
const projectLine = lines.find((l) => l.includes('project-skill'));
expect(projectLine).toBeDefined();
// 该行不应包含"分类:"
expect(projectLine).not.toContain('分类:');
});
});
describe('空注册表', () => {
it('list_all 时注册表为空显示提示', async () => {
// 重置并使用空注册表
resetSkillRegistry();
// 需要重新 mock 为空
vi.doMock('../../../../src/skills/builtin/index.js', () => ({
builtinSkills: [],
}));
// 这个测试比较复杂,因为 mock 已经在模块加载时确定
// 跳过这个测试,因为需要更复杂的设置
});
});
describe('元数据', () => {
it('list_all 返回统计元数据', async () => {
const result = await skillSearchTool.execute({ list_all: true });
expect(result.metadata?.stats).toBeDefined();
expect(result.metadata?.stats.total).toBeGreaterThan(0);
});
it('按分类筛选返回分类和计数元数据', async () => {
const result = await skillSearchTool.execute({ category: 'development' });
expect(result.metadata?.category).toBe('development');
expect(result.metadata?.count).toBeGreaterThan(0);
});
it('搜索返回查询和结果数元数据', async () => {
const result = await skillSearchTool.execute({ query: 'skill' });
expect(result.metadata?.query).toBe('skill');
expect(result.metadata?.resultCount).toBeGreaterThan(0);
});
it('概览返回统计元数据', async () => {
const result = await skillSearchTool.execute({});
expect(result.metadata?.stats).toBeDefined();
});
});
});
@@ -0,0 +1,122 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { skillSearchTool } from '../../../../src/tools/skill/skill_search.js';
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
// Mock loader to prevent file system access
vi.mock('../../../../src/skills/loader.js', () => ({
skillLoader: {
loadFromDirectory: vi.fn().mockResolvedValue([]),
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
},
}));
// Mock builtin skills
vi.mock('../../../../src/skills/builtin/index.js', () => ({
builtinSkills: [
{
name: 'code-review',
description: '代码审查',
promptTemplate: '审查代码',
keywords: ['review', 'quality'],
category: 'development',
source: 'builtin',
enabled: true,
},
{
name: 'generate-tests',
description: '生成测试',
promptTemplate: '生成测试',
keywords: ['test', 'unit'],
category: 'testing',
source: 'builtin',
enabled: true,
},
{
name: 'explain-code',
description: '解释代码功能',
promptTemplate: '解释代码',
category: 'development',
source: 'builtin',
enabled: true,
},
],
}));
describe('skillSearchTool - Skill 搜索工具', () => {
beforeEach(async () => {
vi.clearAllMocks();
resetSkillRegistry();
const registry = getSkillRegistry();
await registry.initialize();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(skillSearchTool.name).toBe('skill_search');
});
it('有正确的元数据', () => {
expect(skillSearchTool.metadata.category).toBe('agent');
expect(skillSearchTool.metadata.keywords).toContain('search');
});
it('所有参数都是可选的', () => {
expect(skillSearchTool.parameters.query.required).toBe(false);
expect(skillSearchTool.parameters.category.required).toBe(false);
expect(skillSearchTool.parameters.list_all.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('list_all 列出所有 Skills', async () => {
const result = await skillSearchTool.execute({ list_all: true });
expect(result.success).toBe(true);
expect(result.output).toContain('code-review');
expect(result.output).toContain('generate-tests');
expect(result.output).toContain('explain-code');
expect(result.output).toContain('development');
expect(result.output).toContain('testing');
});
it('按关键词搜索', async () => {
const result = await skillSearchTool.execute({ query: 'review' });
expect(result.success).toBe(true);
expect(result.output).toContain('code-review');
expect(result.metadata?.query).toBe('review');
});
it('按分类筛选', async () => {
const result = await skillSearchTool.execute({ category: 'testing' });
expect(result.success).toBe(true);
expect(result.output).toContain('generate-tests');
expect(result.output).not.toContain('code-review');
});
it('分类不存在时显示可用分类', async () => {
const result = await skillSearchTool.execute({ category: 'non-existent' });
expect(result.success).toBe(true);
expect(result.output).toContain('没有 Skills');
expect(result.output).toContain('可用分类');
});
it('搜索无结果时提示', async () => {
const result = await skillSearchTool.execute({ query: 'xxxxxxxxx' });
expect(result.success).toBe(true);
expect(result.output).toContain('没有找到');
});
it('无参数时显示概览', async () => {
const result = await skillSearchTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('概览');
expect(result.output).toContain('使用方法');
});
});
});