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
+197
View File
@@ -0,0 +1,197 @@
/**
* 内置 Commands
*
* 提供一些常用的预定义 Commands
*/
import type { Command } from '../types.js';
/**
* /init - 初始化项目配置
*/
export const initCommand: Command = {
name: 'init',
description: '分析代码库并创建 AGENTS.md 配置文件',
template: `Please analyze this codebase and create an AGENTS.md file containing:
1. **Build/lint/test commands** - Document how to build, lint, and test the project
2. **Code style guidelines** - Document the coding conventions used
3. **Project structure** - Overview of the directory structure
4. **Key dependencies** - Important libraries and frameworks used
Additional context: $ARGUMENTS
Start by exploring the project structure and package configuration files.`,
agent: 'explore',
subtask: false,
source: 'builtin',
};
/**
* /review - 代码审查
*/
export const reviewCommand: Command = {
name: 'review',
description: '审查代码变更',
template: `You are a code reviewer. Your job is to review code changes.
Input: $ARGUMENTS
## Determining What to Review
Based on the input, determine which type of review to perform:
1. **No arguments**: Review uncommitted changes using \`git diff\`
2. **Commit hash**: Review that specific commit using \`git show $ARGUMENTS\`
3. **Branch name**: Compare to specified branch using \`git diff $ARGUMENTS...HEAD\`
4. **PR URL**: Review the pull request (if gh CLI is available)
## What to Look For
- **Bugs**: Logic errors, edge cases, null/undefined handling, security issues
- **Structure**: Does it follow existing patterns? Is it maintainable?
- **Performance**: Only flag if obviously problematic
- **Tests**: Are there adequate tests for the changes?
## Output Format
Provide a structured review with:
1. Summary of changes
2. Issues found (categorized by severity)
3. Suggestions for improvement
4. Positive observations`,
agent: 'code-reviewer',
subtask: false,
source: 'builtin',
};
/**
* /test - 运行并修复测试
*/
export const testCommand: Command = {
name: 'test',
description: '运行测试并修复失败的测试',
template: `Run the test suite and analyze the results.
Focus on: $ARGUMENTS
## Instructions
1. First, run the test command for this project
2. If tests fail, analyze the failures
3. Identify the root cause of each failure
4. Fix the failing tests or the code they're testing
5. Re-run tests to verify fixes
If no specific focus is provided, run all tests.`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /fix - 修复问题
*/
export const fixCommand: Command = {
name: 'fix',
description: '修复指定的问题或错误',
template: `Please fix the following issue:
$ARGUMENTS
## Instructions
1. Understand the problem described
2. Locate the relevant code
3. Analyze the root cause
4. Implement a fix
5. Verify the fix works
6. Check for any side effects`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /explain - 解释代码
*/
export const explainCommand: Command = {
name: 'explain',
description: '解释代码或概念',
template: `Please explain the following:
$ARGUMENTS
Provide a clear, structured explanation that includes:
1. Overview - What it does at a high level
2. How it works - Step by step breakdown
3. Key concepts - Important patterns or techniques used
4. Examples - Practical usage examples if applicable`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /commit - 生成 commit 消息
*/
export const commitCommand: Command = {
name: 'commit',
description: '根据变更生成 Git commit 消息',
template: `Generate a Git commit message for the current changes.
Additional context: $ARGUMENTS
## Instructions
1. Run \`git diff --staged\` to see staged changes (or \`git diff\` if nothing staged)
2. Analyze the changes
3. Generate a commit message following Conventional Commits format:
- feat: New feature
- fix: Bug fix
- docs: Documentation
- style: Formatting
- refactor: Code restructuring
- test: Adding tests
- chore: Maintenance
4. Format:
- First line: type(scope): short description (50 chars max)
- Blank line
- Body: Detailed explanation if needed
5. Present the commit message for review`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* /help - 显示帮助
*/
export const helpCommand: Command = {
name: 'help',
description: '显示可用的命令和帮助信息',
template: `Show help information about available commands.
Topic: $ARGUMENTS
If a specific command is mentioned, provide detailed help for that command.
Otherwise, list all available commands with their descriptions.`,
agent: 'general',
subtask: false,
source: 'builtin',
};
/**
* 所有内置 Commands
*/
export const builtinCommands: Command[] = [
initCommand,
reviewCommand,
testCommand,
fixCommand,
explainCommand,
commitCommand,
helpCommand,
];
+284
View File
@@ -0,0 +1,284 @@
/**
* Command 执行器
*
* 负责解析和执行 Command
* - 参数替换($ARGUMENTS, $1, $2, ...
* - 文件引用(@filepath
* - Shell 命令执行(!`command`
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
import type { Command, CommandInput, CommandExecutionResult } from './types.js';
import { getCommandRegistry } from './registry.js';
const execAsync = promisify(exec);
/**
* Command 执行器
*/
export class CommandExecutor {
private workdir: string;
constructor(workdir: string = process.cwd()) {
this.workdir = workdir;
}
/**
* 解析用户输入的命令字符串
* 例如: "/review main..feature" → { command: "review", arguments: "main..feature", args: ["main..feature"] }
*/
parseInput(input: string): CommandInput | null {
// 移除开头的 /
const trimmed = input.trim();
if (!trimmed.startsWith('/')) {
return null;
}
const withoutSlash = trimmed.slice(1);
const spaceIndex = withoutSlash.indexOf(' ');
let commandName: string;
let argumentsStr: string;
if (spaceIndex === -1) {
commandName = withoutSlash;
argumentsStr = '';
} else {
commandName = withoutSlash.slice(0, spaceIndex);
argumentsStr = withoutSlash.slice(spaceIndex + 1).trim();
}
// 解析参数数组
const args = argumentsStr ? this.parseArgs(argumentsStr) : [];
return {
command: commandName,
arguments: argumentsStr,
args,
workdir: this.workdir,
};
}
/**
* 解析参数字符串为数组
* 支持引号包裹的参数
*/
private parseArgs(argsStr: string): string[] {
const args: string[] = [];
let current = '';
let inQuote = false;
let quoteChar = '';
for (const char of argsStr) {
if ((char === '"' || char === "'") && !inQuote) {
inQuote = true;
quoteChar = char;
} else if (char === quoteChar && inQuote) {
inQuote = false;
quoteChar = '';
} else if (char === ' ' && !inQuote) {
if (current) {
args.push(current);
current = '';
}
} else {
current += char;
}
}
if (current) {
args.push(current);
}
return args;
}
/**
* 执行 Command
*/
async execute(input: CommandInput): Promise<CommandExecutionResult> {
const registry = getCommandRegistry();
const command = registry.get(input.command);
if (!command) {
// 尝试搜索相似的 Command
const suggestions = registry.search(input.command, 3);
let errorMsg = `Command 不存在: /${input.command}`;
if (suggestions.length > 0) {
errorMsg += '\n\n你可能想要的 Command:\n';
for (const { command: cmd } of suggestions) {
errorMsg += `- /${cmd.name}`;
if (cmd.description) {
errorMsg += `: ${cmd.description}`;
}
errorMsg += '\n';
}
}
return {
success: false,
error: errorMsg,
};
}
try {
// 渲染模板
const prompt = await this.renderTemplate(command.template, input);
return {
success: true,
prompt,
agent: command.agent,
model: command.model,
subtask: command.subtask,
};
} catch (error) {
return {
success: false,
error: `Command 执行失败: ${error instanceof Error ? error.message : String(error)}`,
};
}
}
/**
* 渲染模板
*/
async renderTemplate(
template: string,
input: CommandInput
): Promise<string> {
let result = template;
// 1. 替换位置参数 $1, $2, ...
result = this.replacePositionalArgs(result, input.args);
// 2. 替换 $ARGUMENTS
result = result.replace(/\$ARGUMENTS/g, input.arguments);
// 3. 处理文件引用 @filepath
result = await this.resolveFileReferences(result, input.workdir);
// 4. 执行 Shell 命令 !`command`
result = await this.executeShellCommands(result, input.workdir);
return result;
}
/**
* 替换位置参数
*/
private replacePositionalArgs(template: string, args: string[]): string {
// 找出模板中使用的最大参数索引
const paramRegex = /\$(\d+)/g;
let maxIndex = 0;
let match;
while ((match = paramRegex.exec(template)) !== null) {
const index = parseInt(match[1], 10);
if (index > maxIndex) {
maxIndex = index;
}
}
// 替换参数
let result = template;
for (let i = 1; i <= maxIndex; i++) {
const value = i === maxIndex
? args.slice(i - 1).join(' ') // 最后一个参数获取剩余所有
: args[i - 1] || '';
result = result.replace(new RegExp(`\\$${i}`, 'g'), value);
}
return result;
}
/**
* 解析文件引用
* @filepath → 文件内容
*/
private async resolveFileReferences(
template: string,
workdir: string
): Promise<string> {
const fileRefRegex = /@([^\s]+)/g;
const matches = [...template.matchAll(fileRefRegex)];
if (matches.length === 0) {
return template;
}
let result = template;
for (const match of matches) {
const [fullMatch, filePath] = match;
const absolutePath = path.isAbsolute(filePath)
? filePath
: path.join(workdir, filePath);
try {
const content = await fs.readFile(absolutePath, 'utf-8');
// 替换为带有文件路径标记的内容
const replacement = `\`\`\`${path.extname(filePath).slice(1) || 'txt'}\n// ${filePath}\n${content}\n\`\`\``;
result = result.replace(fullMatch, replacement);
} catch (error) {
// 文件不存在,保留原样或提示
console.warn(`无法读取文件: ${absolutePath}`);
result = result.replace(fullMatch, `[文件不存在: ${filePath}]`);
}
}
return result;
}
/**
* 执行 Shell 命令
* !`command` → 命令输出
*/
private async executeShellCommands(
template: string,
workdir: string
): Promise<string> {
const shellRegex = /!\`([^`]+)\`/g;
const matches = [...template.matchAll(shellRegex)];
if (matches.length === 0) {
return template;
}
let result = template;
for (const match of matches) {
const [fullMatch, command] = match;
try {
const { stdout, stderr } = await execAsync(command, {
cwd: workdir,
timeout: 30000, // 30 秒超时
});
const output = (stdout + stderr).trim();
result = result.replace(fullMatch, output);
} catch (error) {
// 命令执行失败,替换为错误信息
const errorMsg = error instanceof Error ? error.message : String(error);
result = result.replace(fullMatch, `[命令执行失败: ${command}]\n${errorMsg}`);
}
}
return result;
}
}
/**
* 创建 Command 执行器
*/
export function createCommandExecutor(
workdir: string = process.cwd()
): CommandExecutor {
return new CommandExecutor(workdir);
}
+30
View File
@@ -0,0 +1,30 @@
/**
* Commands 模块
*
* 提供 Command 系统的所有功能导出
*/
// 类型
export type {
Command,
CommandInput,
CommandExecutionResult,
CommandSearchResult,
CommandFrontmatter,
} from './types.js';
// 加载器
export { CommandLoader, commandLoader } from './loader.js';
// 注册表
export {
CommandRegistry,
getCommandRegistry,
resetCommandRegistry,
} from './registry.js';
// 执行器
export { CommandExecutor, createCommandExecutor } from './executor.js';
// 内置 Commands
export { builtinCommands } from './builtin/index.js';
+172
View File
@@ -0,0 +1,172 @@
/**
* Command 加载器
*
* 负责从文件系统加载 Markdown 格式的 Command 定义。
* 支持从以下位置加载:
* 1. 内置 Commands(代码中定义)
* 2. 用户 Commands~/.config/ai-terminal/commands/
* 3. 项目 Commands./.ai-terminal/commands/
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import * as yaml from 'yaml';
import type { Command, CommandFrontmatter } from './types.js';
/**
* Command 加载器
*/
export class CommandLoader {
/**
* 从目录加载所有 Commands
*/
async loadFromDirectory(
dir: string,
source: 'user' | 'project'
): Promise<Command[]> {
const commands: Command[] = [];
try {
const exists = await fs
.access(dir)
.then(() => true)
.catch(() => false);
if (!exists) {
return commands;
}
await this.scanDirectory(dir, dir, source, commands);
} catch (error) {
console.warn(`读取 Commands 目录失败: ${dir}`, error);
}
return commands;
}
/**
* 递归扫描目录
*/
private async scanDirectory(
baseDir: string,
currentDir: string,
source: 'user' | 'project',
commands: Command[]
): Promise<void> {
const entries = await fs.readdir(currentDir, { withFileTypes: true });
for (const entry of entries) {
const fullPath = path.join(currentDir, entry.name);
if (entry.isFile() && entry.name.endsWith('.md')) {
try {
const command = await this.loadFromFile(fullPath, baseDir, source);
if (command) {
commands.push(command);
}
} catch (error) {
console.warn(`加载 Command 文件失败: ${fullPath}`, error);
}
} else if (entry.isDirectory() && !entry.name.startsWith('.')) {
// 递归加载子目录(支持嵌套路径如 deploy/staging
await this.scanDirectory(baseDir, fullPath, source, commands);
}
}
}
/**
* 从单个 Markdown 文件加载 Command
*/
async loadFromFile(
filePath: string,
baseDir: string,
source: 'user' | 'project'
): Promise<Command | null> {
const content = await fs.readFile(filePath, 'utf-8');
// 从文件路径推断命令名称
// baseDir/deploy/staging.md → deploy/staging
const relativePath = path.relative(baseDir, filePath);
const name = relativePath.slice(0, -3); // 移除 .md 后缀
return this.parseMarkdownCommand(content, name, source, filePath);
}
/**
* 解析 Markdown 格式的 Command
*
* 格式示例:
* ```markdown
* ---
* description: 代码审查
* agent: explore
* model: sonnet
* subtask: true
* ---
*
* You are a code reviewer...
*
* Input: $ARGUMENTS
* ```
*/
parseMarkdownCommand(
content: string,
name: string,
source: 'user' | 'project' | 'builtin',
sourcePath?: string
): Command | null {
// 解析 frontmatter
const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/);
let frontmatter: CommandFrontmatter = {};
let template: string;
if (frontmatterMatch) {
const [, frontmatterStr, bodyContent] = frontmatterMatch;
try {
frontmatter = yaml.parse(frontmatterStr) as CommandFrontmatter;
} catch (error) {
console.warn(`解析 Command frontmatter 失败: ${sourcePath}`, error);
}
template = bodyContent.trim();
} else {
// 没有 frontmatter,整个内容作为模板
template = content.trim();
}
if (!template) {
return null;
}
return {
name,
description: frontmatter.description,
template,
agent: frontmatter.agent,
model: frontmatter.model,
subtask: frontmatter.subtask,
source,
sourcePath,
};
}
/**
* 获取用户 Commands 目录
*/
getUserCommandsDir(): string {
const home = process.env.HOME || process.env.USERPROFILE || '';
return path.join(home, '.config', 'ai-terminal', 'commands');
}
/**
* 获取项目 Commands 目录
*/
getProjectCommandsDir(workdir: string = process.cwd()): string {
return path.join(workdir, '.ai-terminal', 'commands');
}
}
/**
* 全局 Command 加载器实例
*/
export const commandLoader = new CommandLoader();
+199
View File
@@ -0,0 +1,199 @@
/**
* Command 注册表
*
* 管理所有可用的 Commands,支持:
* - 注册/注销 Commands
* - 按名称查询
* - 搜索 Commands
*/
import type { Command, CommandSearchResult } from './types.js';
import { commandLoader } from './loader.js';
import { builtinCommands } from './builtin/index.js';
/**
* Command 注册表
*/
export class CommandRegistry {
private commands = new Map<string, Command>();
private initialized = false;
/**
* 初始化注册表
*/
async initialize(workdir: string = process.cwd()): Promise<void> {
if (this.initialized) {
return;
}
// 1. 注册内置 Commands
for (const command of builtinCommands) {
this.register(command);
}
// 2. 加载用户 Commands
const userDir = commandLoader.getUserCommandsDir();
const userCommands = await commandLoader.loadFromDirectory(userDir, 'user');
for (const command of userCommands) {
this.register(command);
}
// 3. 加载项目 Commands
const projectDir = commandLoader.getProjectCommandsDir(workdir);
const projectCommands = await commandLoader.loadFromDirectory(
projectDir,
'project'
);
for (const command of projectCommands) {
this.register(command);
}
this.initialized = true;
}
/**
* 注册 Command
*/
register(command: Command): void {
// 项目 Commands 优先级最高,可以覆盖同名的内置/用户 Commands
const existing = this.commands.get(command.name);
if (existing) {
// 优先级: project > user > builtin
const priority = { project: 3, user: 2, builtin: 1 };
if (priority[command.source] < priority[existing.source]) {
return; // 不覆盖更高优先级的 Command
}
}
this.commands.set(command.name, command);
}
/**
* 注销 Command
*/
unregister(name: string): boolean {
return this.commands.delete(name);
}
/**
* 获取 Command
*/
get(name: string): Command | undefined {
return this.commands.get(name);
}
/**
* 检查 Command 是否存在
*/
has(name: string): boolean {
return this.commands.has(name);
}
/**
* 获取所有 Commands
*/
getAll(): Command[] {
return Array.from(this.commands.values());
}
/**
* 搜索 Commands
*/
search(query: string, limit: number = 10): CommandSearchResult[] {
const queryLower = query.toLowerCase();
const results: CommandSearchResult[] = [];
for (const command of this.commands.values()) {
let score = 0;
// 精确名称匹配
if (command.name.toLowerCase() === queryLower) {
score = 100;
}
// 名称前缀匹配
else if (command.name.toLowerCase().startsWith(queryLower)) {
score = 80;
}
// 名称包含匹配
else if (command.name.toLowerCase().includes(queryLower)) {
score = 60;
}
// 描述匹配
else if (command.description?.toLowerCase().includes(queryLower)) {
score = 40;
}
if (score > 0) {
results.push({ command, score });
}
}
// 按分数降序排序
results.sort((a, b) => b.score - a.score);
return results.slice(0, limit);
}
/**
* 列出所有 Commands(用于帮助显示)
*/
list(): Array<{ name: string; description?: string; source: string }> {
return this.getAll()
.map((cmd) => ({
name: cmd.name,
description: cmd.description,
source: cmd.source,
}))
.sort((a, b) => a.name.localeCompare(b.name));
}
/**
* 重新加载 Commands
*/
async reload(workdir: string = process.cwd()): Promise<void> {
this.commands.clear();
this.initialized = false;
await this.initialize(workdir);
}
/**
* 获取统计信息
*/
getStats(): {
total: number;
bySource: Record<string, number>;
} {
const commands = this.getAll();
const bySource: Record<string, number> = {};
for (const command of commands) {
bySource[command.source] = (bySource[command.source] || 0) + 1;
}
return {
total: commands.length,
bySource,
};
}
}
/**
* 全局 Command 注册表实例
*/
let commandRegistryInstance: CommandRegistry | null = null;
/**
* 获取全局 Command 注册表
*/
export function getCommandRegistry(): CommandRegistry {
if (!commandRegistryInstance) {
commandRegistryInstance = new CommandRegistry();
}
return commandRegistryInstance;
}
/**
* 重置全局 Command 注册表(用于测试)
*/
export function resetCommandRegistry(): void {
commandRegistryInstance = null;
}
+80
View File
@@ -0,0 +1,80 @@
/**
* Command 系统类型定义
*
* Command 是用户可通过斜杠命令触发的可复用提示词模板。
* 与 Skill 不同,Command 面向用户,可控制完整执行流程。
*/
/**
* Command 定义
*/
export interface Command {
/** Command 名称(从文件路径推断,如 deploy/staging */
name: string;
/** Command 描述 */
description?: string;
/** 提示词模板 */
template: string;
/** 指定使用的 Agent */
agent?: string;
/** 指定使用的模型 (sonnet/opus/haiku) */
model?: string;
/** 是否作为子任务执行 */
subtask?: boolean;
/** 来源 */
source: 'builtin' | 'user' | 'project';
/** 来源路径 */
sourcePath?: string;
}
/**
* Command 执行输入
*/
export interface CommandInput {
/** Command 名称 */
command: string;
/** 原始参数字符串 */
arguments: string;
/** 解析后的参数数组 */
args: string[];
/** 当前工作目录 */
workdir: string;
}
/**
* Command 执行结果
*/
export interface CommandExecutionResult {
/** 是否成功 */
success: boolean;
/** 渲染后的提示 */
prompt?: string;
/** 指定的 Agent */
agent?: string;
/** 指定的模型 */
model?: string;
/** 是否作为子任务 */
subtask?: boolean;
/** 错误信息 */
error?: string;
}
/**
* Command 搜索结果
*/
export interface CommandSearchResult {
/** Command */
command: Command;
/** 匹配分数 */
score: number;
}
/**
* Command FrontmatterMarkdown 头部配置)
*/
export interface CommandFrontmatter {
description?: string;
agent?: string;
model?: string;
subtask?: boolean;
}