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,272 @@
import { describe, it, expect } from 'vitest';
import {
builtinSkills,
codeReviewSkill,
explainCodeSkill,
generateDocsSkill,
generateTestsSkill,
refactorSuggestSkill,
fixBugSkill,
gitCommitSkill,
apiDesignSkill,
} from '../../../../src/skills/builtin/index.js';
describe('Builtin Skills - 内置技能', () => {
describe('builtinSkills 数组', () => {
it('包含所有内置 Skills', () => {
expect(builtinSkills).toHaveLength(8);
});
it('所有 Skills 都是启用状态', () => {
builtinSkills.forEach((skill) => {
expect(skill.enabled).toBe(true);
});
});
it('所有 Skills 都有必要字段', () => {
builtinSkills.forEach((skill) => {
expect(skill.name).toBeDefined();
expect(skill.displayName).toBeDefined();
expect(skill.description).toBeDefined();
expect(skill.promptTemplate).toBeDefined();
expect(skill.parameters).toBeDefined();
expect(skill.category).toBeDefined();
expect(skill.source).toBe('builtin');
});
});
it('所有 Skills 都有关键词', () => {
builtinSkills.forEach((skill) => {
expect(skill.keywords).toBeDefined();
expect(skill.keywords!.length).toBeGreaterThan(0);
});
});
it('Skills 名称唯一', () => {
const names = builtinSkills.map((s) => s.name);
const uniqueNames = new Set(names);
expect(uniqueNames.size).toBe(names.length);
});
});
describe('codeReviewSkill - 代码审查', () => {
it('有正确的名称', () => {
expect(codeReviewSkill.name).toBe('code-review');
expect(codeReviewSkill.displayName).toBe('代码审查');
});
it('属于 development 分类', () => {
expect(codeReviewSkill.category).toBe('development');
});
it('code 参数是必需的', () => {
expect(codeReviewSkill.parameters.code.required).toBe(true);
});
it('focus 参数是可选的', () => {
expect(codeReviewSkill.parameters.focus.required).toBe(false);
});
it('模板包含审查相关内容', () => {
expect(codeReviewSkill.promptTemplate).toContain('审查');
expect(codeReviewSkill.promptTemplate).toContain('{{code}}');
});
it('关键词包含相关词汇', () => {
expect(codeReviewSkill.keywords).toContain('review');
expect(codeReviewSkill.keywords).toContain('审查');
});
});
describe('explainCodeSkill - 代码解释', () => {
it('有正确的名称', () => {
expect(explainCodeSkill.name).toBe('explain-code');
expect(explainCodeSkill.displayName).toBe('代码解释');
});
it('属于 development 分类', () => {
expect(explainCodeSkill.category).toBe('development');
});
it('code 参数是必需的', () => {
expect(explainCodeSkill.parameters.code.required).toBe(true);
});
it('level 参数有枚举值', () => {
expect(explainCodeSkill.parameters.level.enum).toContain('beginner');
expect(explainCodeSkill.parameters.level.enum).toContain('intermediate');
expect(explainCodeSkill.parameters.level.enum).toContain('advanced');
});
it('level 参数有默认值', () => {
expect(explainCodeSkill.parameters.level.default).toBe('intermediate');
});
});
describe('generateDocsSkill - 文档生成', () => {
it('有正确的名称', () => {
expect(generateDocsSkill.name).toBe('generate-docs');
expect(generateDocsSkill.displayName).toBe('文档生成');
});
it('属于 documentation 分类', () => {
expect(generateDocsSkill.category).toBe('documentation');
});
it('type 参数有多种文档类型', () => {
expect(generateDocsSkill.parameters.type.enum).toContain('JSDoc');
expect(generateDocsSkill.parameters.type.enum).toContain('TSDoc');
expect(generateDocsSkill.parameters.type.enum).toContain('README');
});
it('type 参数有默认值', () => {
expect(generateDocsSkill.parameters.type.default).toBe('文档注释');
});
});
describe('generateTestsSkill - 测试生成', () => {
it('有正确的名称', () => {
expect(generateTestsSkill.name).toBe('generate-tests');
expect(generateTestsSkill.displayName).toBe('测试生成');
});
it('属于 testing 分类', () => {
expect(generateTestsSkill.category).toBe('testing');
});
it('framework 参数有多个框架选项', () => {
expect(generateTestsSkill.parameters.framework.enum).toContain('vitest');
expect(generateTestsSkill.parameters.framework.enum).toContain('jest');
expect(generateTestsSkill.parameters.framework.enum).toContain('mocha');
expect(generateTestsSkill.parameters.framework.enum).toContain('pytest');
});
it('framework 默认为 vitest', () => {
expect(generateTestsSkill.parameters.framework.default).toBe('vitest');
});
});
describe('refactorSuggestSkill - 重构建议', () => {
it('有正确的名称', () => {
expect(refactorSuggestSkill.name).toBe('refactor-suggest');
expect(refactorSuggestSkill.displayName).toBe('重构建议');
});
it('属于 development 分类', () => {
expect(refactorSuggestSkill.category).toBe('development');
});
it('goal 参数是可选的', () => {
expect(refactorSuggestSkill.parameters.goal.required).toBe(false);
});
it('模板包含重构相关内容', () => {
expect(refactorSuggestSkill.promptTemplate).toContain('重构');
expect(refactorSuggestSkill.promptTemplate).toContain('{{code}}');
});
});
describe('fixBugSkill - Bug 修复', () => {
it('有正确的名称', () => {
expect(fixBugSkill.name).toBe('fix-bug');
expect(fixBugSkill.displayName).toBe('Bug 修复');
});
it('属于 debugging 分类', () => {
expect(fixBugSkill.category).toBe('debugging');
});
it('有 error 和 context 可选参数', () => {
expect(fixBugSkill.parameters.error.required).toBe(false);
expect(fixBugSkill.parameters.context.required).toBe(false);
});
it('模板支持错误信息和上下文', () => {
expect(fixBugSkill.promptTemplate).toContain('{{#if error}}');
expect(fixBugSkill.promptTemplate).toContain('{{#if context}}');
});
});
describe('gitCommitSkill - Git Commit', () => {
it('有正确的名称', () => {
expect(gitCommitSkill.name).toBe('git-commit');
expect(gitCommitSkill.displayName).toBe('Git Commit');
});
it('属于 git 分类', () => {
expect(gitCommitSkill.category).toBe('git');
});
it('diff 参数是必需的', () => {
expect(gitCommitSkill.parameters.diff.required).toBe(true);
});
it('type 参数有 conventional commit 类型', () => {
expect(gitCommitSkill.parameters.type.enum).toContain('feat');
expect(gitCommitSkill.parameters.type.enum).toContain('fix');
expect(gitCommitSkill.parameters.type.enum).toContain('docs');
expect(gitCommitSkill.parameters.type.enum).toContain('refactor');
});
it('模板提到 Conventional Commits', () => {
expect(gitCommitSkill.promptTemplate).toContain('Conventional Commits');
});
});
describe('apiDesignSkill - API 设计', () => {
it('有正确的名称', () => {
expect(apiDesignSkill.name).toBe('api-design');
expect(apiDesignSkill.displayName).toBe('API 设计');
});
it('属于 architecture 分类', () => {
expect(apiDesignSkill.category).toBe('architecture');
});
it('requirement 参数是必需的', () => {
expect(apiDesignSkill.parameters.requirement.required).toBe(true);
});
it('constraints 参数是可选的', () => {
expect(apiDesignSkill.parameters.constraints.required).toBe(false);
});
it('模板提到 RESTful API', () => {
expect(apiDesignSkill.promptTemplate).toContain('RESTful API');
});
});
describe('参数类型验证', () => {
it('所有必需参数都是 string 类型', () => {
builtinSkills.forEach((skill) => {
Object.entries(skill.parameters).forEach(([, param]) => {
if (param.required) {
expect(param.type).toBe('string');
}
});
});
});
it('所有参数都有描述', () => {
builtinSkills.forEach((skill) => {
Object.entries(skill.parameters).forEach(([, param]) => {
expect(param.description).toBeDefined();
expect(param.description.length).toBeGreaterThan(0);
});
});
});
});
describe('分类覆盖', () => {
it('覆盖多个分类', () => {
const categories = new Set(builtinSkills.map((s) => s.category));
expect(categories.size).toBeGreaterThan(3);
expect(categories).toContain('development');
expect(categories).toContain('documentation');
expect(categories).toContain('testing');
expect(categories).toContain('debugging');
expect(categories).toContain('git');
expect(categories).toContain('architecture');
});
});
});
@@ -0,0 +1,193 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { SkillLoader } from '../../../src/skills/loader.js';
// Mock fs/promises
vi.mock('fs/promises', () => ({
access: vi.fn(),
readdir: vi.fn(),
readFile: vi.fn(),
}));
import * as fs from 'fs/promises';
describe('SkillLoader - Skill 加载器', () => {
let loader: SkillLoader;
beforeEach(() => {
vi.clearAllMocks();
loader = new SkillLoader();
});
describe('loadFromFile - 从文件加载', () => {
it('加载 YAML 格式的 Skill', async () => {
const yamlContent = `
skill:
name: test-skill
description: 测试 Skill
promptTemplate: "提示模板 {{param}}"
parameters:
param:
type: string
required: true
description: 参数描述
`;
vi.mocked(fs.readFile).mockResolvedValue(yamlContent);
const skill = await loader.loadFromFile('/test/skill.yaml', 'user');
expect(skill).not.toBeNull();
expect(skill?.name).toBe('test-skill');
expect(skill?.description).toBe('测试 Skill');
expect(skill?.source).toBe('user');
expect(skill?.parameters?.param.required).toBe(true);
});
it('加载 JSON 格式的 Skill', async () => {
const jsonContent = JSON.stringify({
skill: {
name: 'json-skill',
description: 'JSON Skill',
promptTemplate: '提示',
},
});
vi.mocked(fs.readFile).mockResolvedValue(jsonContent);
const skill = await loader.loadFromFile('/test/skill.json', 'project');
expect(skill).not.toBeNull();
expect(skill?.name).toBe('json-skill');
expect(skill?.source).toBe('project');
});
it('加载 Markdown 格式的 Skill(带 frontmatter', async () => {
const mdContent = `---
name: md-skill
description: Markdown Skill
parameters:
code:
type: string
required: true
description: 代码
---
# 代码审查
请审查以下代码:
{{code}}
`;
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
const skill = await loader.loadFromFile('/test/skill.md', 'user');
expect(skill).not.toBeNull();
expect(skill?.name).toBe('md-skill');
expect(skill?.description).toBe('Markdown Skill');
expect(skill?.promptTemplate).toContain('{{code}}');
expect(skill?.promptTemplate).toContain('代码审查');
});
it('加载 Markdown 格式的 Skill(无 frontmatter', async () => {
const mdContent = `# 简单提示
这是一个简单的提示模板。
{{input}}
`;
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
const skill = await loader.loadFromFile('/test/simple.md', 'user');
expect(skill).not.toBeNull();
expect(skill?.name).toBe('simple'); // 从文件名获取
expect(skill?.promptTemplate).toContain('简单提示');
});
it('缺少必需字段时返回 null', async () => {
const yamlContent = `
skill:
description: 没有 name 和 promptTemplate
`;
vi.mocked(fs.readFile).mockResolvedValue(yamlContent);
const skill = await loader.loadFromFile('/test/invalid.yaml', 'user');
expect(skill).toBeNull();
});
});
describe('loadFromDirectory - 从目录加载', () => {
it('目录不存在时返回空数组', async () => {
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
const skills = await loader.loadFromDirectory('/non-existent', 'user');
expect(skills).toEqual([]);
});
it('加载目录中的所有 Skill 文件', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'skill1.yaml', isFile: () => true, isDirectory: () => false },
{ name: 'skill2.json', isFile: () => true, isDirectory: () => false },
{ name: 'readme.txt', isFile: () => true, isDirectory: () => false }, // 不支持的格式
] as any);
vi.mocked(fs.readFile)
.mockResolvedValueOnce(`skill:\n name: skill1\n description: 1\n promptTemplate: t1`)
.mockResolvedValueOnce(JSON.stringify({ skill: { name: 'skill2', description: '2', promptTemplate: 't2' } }));
const skills = await loader.loadFromDirectory('/test', 'user');
expect(skills.length).toBe(2);
expect(skills.map((s) => s.name)).toContain('skill1');
expect(skills.map((s) => s.name)).toContain('skill2');
});
it('递归加载子目录', async () => {
vi.mocked(fs.access).mockResolvedValue(undefined);
vi.mocked(fs.readdir)
.mockResolvedValueOnce([
{ name: 'subdir', isFile: () => false, isDirectory: () => true },
{ name: 'root.yaml', isFile: () => true, isDirectory: () => false },
] as any)
.mockResolvedValueOnce([
{ name: 'sub.yaml', isFile: () => true, isDirectory: () => false },
] as any);
vi.mocked(fs.readFile)
.mockResolvedValueOnce(`skill:\n name: root\n description: root\n promptTemplate: root`)
.mockResolvedValueOnce(`skill:\n name: sub\n description: sub\n promptTemplate: sub`);
const skills = await loader.loadFromDirectory('/test', 'user');
expect(skills.length).toBe(2);
expect(skills.map((s) => s.name)).toContain('root');
expect(skills.map((s) => s.name)).toContain('sub');
});
});
describe('目录路径', () => {
it('getUserSkillsDir 返回用户 Skills 目录', () => {
const originalHome = process.env.HOME;
process.env.HOME = '/home/testuser';
const dir = loader.getUserSkillsDir();
expect(dir).toBe('/home/testuser/.config/ai-terminal/skills');
process.env.HOME = originalHome;
});
it('getProjectSkillsDir 返回项目 Skills 目录', () => {
const dir = loader.getProjectSkillsDir('/workspace');
expect(dir).toBe('/workspace/.ai-terminal/skills');
});
});
});
@@ -0,0 +1,407 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
SkillRegistry,
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: 'test-builtin',
description: '内置测试 Skill',
promptTemplate: '测试提示: {{param1}}',
parameters: {
param1: { type: 'string', required: true, description: '参数1' },
},
source: 'builtin',
enabled: true,
},
],
}));
describe('SkillRegistry - Skill 注册表', () => {
let registry: SkillRegistry;
beforeEach(() => {
resetSkillRegistry();
registry = new SkillRegistry({ autoLoad: false });
});
describe('register - 注册', () => {
it('成功注册 Skill', () => {
const skill: Skill = {
name: 'test-skill',
description: '测试 Skill',
promptTemplate: '测试提示',
source: 'user',
};
registry.register(skill);
expect(registry.has('test-skill')).toBe(true);
});
it('高优先级 Skill 可以覆盖低优先级', () => {
const builtinSkill: Skill = {
name: 'same-name',
description: '内置版本',
promptTemplate: '内置提示',
source: 'builtin',
};
const projectSkill: Skill = {
name: 'same-name',
description: '项目版本',
promptTemplate: '项目提示',
source: 'project',
};
registry.register(builtinSkill);
registry.register(projectSkill);
const result = registry.get('same-name');
expect(result?.source).toBe('project');
});
it('低优先级 Skill 不能覆盖高优先级', () => {
const projectSkill: Skill = {
name: 'same-name',
description: '项目版本',
promptTemplate: '项目提示',
source: 'project',
};
const builtinSkill: Skill = {
name: 'same-name',
description: '内置版本',
promptTemplate: '内置提示',
source: 'builtin',
};
registry.register(projectSkill);
registry.register(builtinSkill);
const result = registry.get('same-name');
expect(result?.source).toBe('project');
});
});
describe('get - 获取', () => {
it('获取存在的 Skill', () => {
const skill: Skill = {
name: 'test-skill',
description: '测试',
promptTemplate: '提示',
source: 'user',
};
registry.register(skill);
const result = registry.get('test-skill');
expect(result).toBeDefined();
expect(result?.name).toBe('test-skill');
});
it('获取不存在的 Skill 返回 undefined', () => {
const result = registry.get('non-existent');
expect(result).toBeUndefined();
});
});
describe('search - 搜索', () => {
beforeEach(() => {
const skills: Skill[] = [
{
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,
},
];
for (const skill of skills) {
registry.register(skill);
}
});
it('按名称精确匹配', () => {
const results = registry.search('code-review');
expect(results.length).toBeGreaterThan(0);
expect(results[0].skill.name).toBe('code-review');
expect(results[0].score).toBe(100);
});
it('按名称前缀匹配', () => {
const results = registry.search('code');
expect(results.length).toBe(2); // code-review, explain-code
});
it('按描述匹配', () => {
const results = registry.search('审查');
expect(results.length).toBeGreaterThan(0);
expect(results[0].skill.name).toBe('code-review');
});
it('按关键词匹配', () => {
const results = registry.search('quality');
expect(results.length).toBeGreaterThan(0);
expect(results[0].skill.name).toBe('code-review');
});
it('按分类匹配', () => {
const results = registry.search('testing');
expect(results.length).toBeGreaterThan(0);
expect(results[0].skill.name).toBe('generate-tests');
});
it('限制结果数量', () => {
const results = registry.search('code', 1);
expect(results.length).toBe(1);
});
});
describe('renderPrompt - 渲染模板', () => {
it('成功渲染模板', () => {
const skill: Skill = {
name: 'test',
description: '测试',
promptTemplate: '你好 {{name}},请{{action}}',
source: 'user',
};
const result = registry.renderPrompt(skill, {
name: '世界',
action: '测试',
});
expect(result.success).toBe(true);
expect(result.prompt).toBe('你好 世界,请测试');
});
it('缺少必需参数时返回错误', () => {
const skill: Skill = {
name: 'test',
description: '测试',
promptTemplate: '{{param}}',
parameters: {
param: { type: 'string', required: true, description: '必需参数' },
},
source: 'user',
};
const result = registry.renderPrompt(skill, {});
expect(result.success).toBe(false);
expect(result.error).toContain('缺少必需参数');
});
it('使用默认值', () => {
const skill: Skill = {
name: 'test',
description: '测试',
promptTemplate: '值: {{param}}',
parameters: {
param: {
type: 'string',
required: false,
default: '默认值',
description: '可选参数',
},
},
source: 'user',
};
const result = registry.renderPrompt(skill, {});
expect(result.success).toBe(true);
expect(result.prompt).toBe('值: 默认值');
});
it('保留未匹配的变量', () => {
const skill: Skill = {
name: 'test',
description: '测试',
promptTemplate: '{{known}} 和 {{unknown}}',
source: 'user',
};
const result = registry.renderPrompt(skill, { known: '已知' });
expect(result.success).toBe(true);
expect(result.prompt).toBe('已知 和 {{unknown}}');
});
});
describe('execute - 执行', () => {
it('成功执行 Skill', () => {
const skill: Skill = {
name: 'test',
description: '测试',
promptTemplate: '提示: {{param}}',
source: 'user',
enabled: true,
};
registry.register(skill);
const result = registry.execute('test', { param: '值' });
expect(result.success).toBe(true);
expect(result.prompt).toBe('提示: 值');
});
it('Skill 不存在时返回错误', () => {
const result = registry.execute('non-existent', {});
expect(result.success).toBe(false);
expect(result.error).toContain('不存在');
});
it('Skill 禁用时返回错误', () => {
const skill: Skill = {
name: 'disabled',
description: '禁用的 Skill',
promptTemplate: '提示',
source: 'user',
enabled: false,
};
registry.register(skill);
const result = registry.execute('disabled', {});
expect(result.success).toBe(false);
expect(result.error).toContain('禁用');
});
});
describe('getByCategory - 按分类获取', () => {
it('返回指定分类的 Skills', () => {
const skills: Skill[] = [
{
name: 'skill1',
description: '1',
promptTemplate: '1',
category: 'dev',
source: 'builtin',
enabled: true,
},
{
name: 'skill2',
description: '2',
promptTemplate: '2',
category: 'dev',
source: 'builtin',
enabled: true,
},
{
name: 'skill3',
description: '3',
promptTemplate: '3',
category: 'test',
source: 'builtin',
enabled: true,
},
];
for (const skill of skills) {
registry.register(skill);
}
const devSkills = registry.getByCategory('dev');
expect(devSkills.length).toBe(2);
});
});
describe('getStats - 统计信息', () => {
it('返回正确的统计信息', () => {
const skills: Skill[] = [
{
name: 'builtin1',
description: '1',
promptTemplate: '1',
category: 'dev',
source: 'builtin',
enabled: true,
},
{
name: 'user1',
description: '2',
promptTemplate: '2',
category: 'dev',
source: 'user',
enabled: true,
},
{
name: 'disabled1',
description: '3',
promptTemplate: '3',
source: 'builtin',
enabled: false,
},
];
for (const skill of skills) {
registry.register(skill);
}
const stats = registry.getStats();
expect(stats.total).toBe(3);
expect(stats.enabled).toBe(2);
expect(stats.bySource.builtin).toBe(2);
expect(stats.bySource.user).toBe(1);
expect(stats.byCategory.dev).toBe(2);
});
});
});
describe('getSkillRegistry - 全局注册表', () => {
beforeEach(() => {
resetSkillRegistry();
});
it('返回单例实例', () => {
const registry1 = getSkillRegistry();
const registry2 = getSkillRegistry();
expect(registry1).toBe(registry2);
});
it('resetSkillRegistry 重置实例', () => {
const registry1 = getSkillRegistry();
resetSkillRegistry();
const registry2 = getSkillRegistry();
expect(registry1).not.toBe(registry2);
});
});