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
+232
View File
@@ -0,0 +1,232 @@
/**
* Hook 配置加载器
*
* 从项目配置文件加载 hook 配置
* 支持 .ai-assistant.json, .ai-assistant.jsonc, ai-assistant.config.json 等格式
*/
import * as fs from 'fs/promises';
import * as path from 'path';
import type { HookConfig, ShellCommandConfig, FileHookConfig } from './types.js';
// 支持的配置文件名
const CONFIG_FILE_NAMES = [
'.ai-assistant.json',
'.ai-assistant.jsonc',
'ai-assistant.config.json',
'.ai-assistantrc',
'.ai-assistantrc.json',
];
/**
* 完整的配置文件结构
*/
export interface ProjectConfig {
/** Hook 配置 */
hooks?: HookConfig;
/** 插件列表 */
plugins?: string[];
/** 其他配置... */
[key: string]: unknown;
}
/**
* 移除 JSON 中的注释(支持 JSONC 格式)
*/
function stripJsonComments(jsonString: string): string {
// 移除单行注释 // ...
let result = jsonString.replace(/\/\/.*$/gm, '');
// 移除多行注释 /* ... */
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
return result;
}
/**
* 解析 JSON 文件(支持 JSONC
*/
async function parseJsonFile(filePath: string): Promise<ProjectConfig | null> {
try {
const content = await fs.readFile(filePath, 'utf-8');
const cleanContent = stripJsonComments(content);
return JSON.parse(cleanContent);
} catch {
return null;
}
}
/**
* 在目录中查找配置文件
*/
async function findConfigFile(directory: string): Promise<string | null> {
for (const fileName of CONFIG_FILE_NAMES) {
const filePath = path.join(directory, fileName);
try {
await fs.access(filePath);
return filePath;
} catch {
// 文件不存在,继续查找
}
}
return null;
}
/**
* 验证 ShellCommandConfig
*/
function validateShellCommandConfig(config: unknown): config is ShellCommandConfig {
if (typeof config !== 'object' || config === null) return false;
const obj = config as Record<string, unknown>;
// command 必须是非空字符串数组
if (!Array.isArray(obj.command) || obj.command.length === 0) return false;
if (!obj.command.every((c) => typeof c === 'string')) return false;
// environment 如果存在,必须是对象
if (obj.environment !== undefined) {
if (typeof obj.environment !== 'object' || obj.environment === null) return false;
const env = obj.environment as Record<string, unknown>;
if (!Object.values(env).every((v) => typeof v === 'string')) return false;
}
// timeout 如果存在,必须是正数
if (obj.timeout !== undefined) {
if (typeof obj.timeout !== 'number' || obj.timeout <= 0) return false;
}
// cwd 如果存在,必须是字符串
if (obj.cwd !== undefined) {
if (typeof obj.cwd !== 'string') return false;
}
return true;
}
/**
* 验证 FileHookConfig
*/
function validateFileHookConfig(config: unknown): config is FileHookConfig {
if (typeof config !== 'object' || config === null) return false;
const obj = config as Record<string, unknown>;
for (const [pattern, commands] of Object.entries(obj)) {
// pattern 必须是非空字符串
if (typeof pattern !== 'string' || pattern.length === 0) return false;
// commands 必须是 ShellCommandConfig 数组
if (!Array.isArray(commands)) return false;
if (!commands.every(validateShellCommandConfig)) return false;
}
return true;
}
/**
* 验证 HookConfig
*/
function validateHookConfig(config: unknown): config is HookConfig {
if (typeof config !== 'object' || config === null) return false;
const obj = config as Record<string, unknown>;
// file_edited
if (obj.file_edited !== undefined) {
if (!validateFileHookConfig(obj.file_edited)) return false;
}
// file_created
if (obj.file_created !== undefined) {
if (!validateFileHookConfig(obj.file_created)) return false;
}
// file_deleted
if (obj.file_deleted !== undefined) {
if (!validateFileHookConfig(obj.file_deleted)) return false;
}
// session_completed
if (obj.session_completed !== undefined) {
if (!Array.isArray(obj.session_completed)) return false;
if (!obj.session_completed.every(validateShellCommandConfig)) return false;
}
return true;
}
/**
* 加载项目配置
*/
export async function loadProjectConfig(directory: string): Promise<ProjectConfig | null> {
const configPath = await findConfigFile(directory);
if (!configPath) return null;
const config = await parseJsonFile(configPath);
return config;
}
/**
* 加载 Hook 配置
*/
export async function loadHookConfig(directory: string): Promise<HookConfig | null> {
const projectConfig = await loadProjectConfig(directory);
if (!projectConfig?.hooks) return null;
// 验证配置
if (!validateHookConfig(projectConfig.hooks)) {
console.warn('Invalid hook configuration in project config file');
return null;
}
return projectConfig.hooks;
}
/**
* 加载插件列表
*/
export async function loadPluginList(directory: string): Promise<string[]> {
const projectConfig = await loadProjectConfig(directory);
if (!projectConfig?.plugins) return [];
// 验证插件列表
if (!Array.isArray(projectConfig.plugins)) return [];
if (!projectConfig.plugins.every((p) => typeof p === 'string')) return [];
return projectConfig.plugins;
}
/**
* 创建默认配置文件
*/
export async function createDefaultConfig(directory: string): Promise<void> {
const configPath = path.join(directory, '.ai-assistant.json');
const defaultConfig: ProjectConfig = {
hooks: {
file_edited: {
'*.ts': [
{
command: ['npx', 'tsc', '--noEmit'],
timeout: 30000,
},
],
'*.{js,jsx,ts,tsx}': [
{
command: ['npx', 'eslint', '--fix'],
timeout: 30000,
},
],
},
file_created: {},
file_deleted: {},
session_completed: [],
},
plugins: [],
};
await fs.writeFile(configPath, JSON.stringify(defaultConfig, null, 2));
}
/**
* 获取配置文件路径(如果存在)
*/
export async function getConfigFilePath(directory: string): Promise<string | null> {
return findConfigFile(directory);
}
+48
View File
@@ -0,0 +1,48 @@
/**
* Hook 系统模块
*
* 提供工具执行前后的 hook 功能,支持自定义命令执行
* 参考 open-code 的实现
*/
// Hook 管理器
export {
HookManager,
getHookManager,
initHookManager,
resetHookManager,
} from './manager.js';
// 配置加载
export {
loadProjectConfig,
loadHookConfig,
loadPluginList,
createDefaultConfig,
getConfigFilePath,
type ProjectConfig,
} from './config-loader.js';
// 类型导出
export type {
HookType,
HookConfig,
HookEvent,
HookEventListener,
ShellCommandConfig,
FileHookConfig,
Hooks,
Plugin,
PluginInput,
ToolExecuteBeforeInput,
ToolExecuteBeforeOutput,
ToolExecuteAfterInput,
ToolExecuteAfterOutput,
SessionStartInput,
SessionEndInput,
MessageBeforeInput,
MessageBeforeOutput,
MessageAfterInput,
FileChangeInput,
FileChangeOutput,
} from './types.js';
+495
View File
@@ -0,0 +1,495 @@
/**
* Hook 管理器
*
* 负责 hook 的注册、触发和管理
*/
import { spawn } from 'child_process';
import { minimatch } from 'minimatch';
import type {
Hooks,
HookType,
HookConfig,
HookEvent,
HookEventListener,
ShellCommandConfig,
FileHookConfig,
ToolExecuteBeforeInput,
ToolExecuteBeforeOutput,
ToolExecuteAfterInput,
ToolExecuteAfterOutput,
SessionStartInput,
SessionEndInput,
MessageBeforeInput,
MessageBeforeOutput,
MessageAfterInput,
FileChangeInput,
FileChangeOutput,
Plugin,
PluginInput,
} from './types.js';
/**
* Hook 管理器
*/
export class HookManager {
/** 已注册的 hooks */
private hooks: Hooks[] = [];
/** 配置型 hooks(从配置文件加载) */
private configHooks: HookConfig | null = null;
/** 事件监听器 */
private eventListeners: HookEventListener[] = [];
/** 当前工作目录 */
private workdir: string;
/** 会话 ID */
private sessionId: string;
constructor(workdir: string, sessionId?: string) {
this.workdir = workdir;
this.sessionId = sessionId || 'default';
}
/**
* 注册插件
*/
async registerPlugin(plugin: Plugin): Promise<void> {
const input: PluginInput = {
workdir: this.workdir,
sessionId: this.sessionId,
};
try {
const hooks = await plugin(input);
this.hooks.push(hooks);
} catch (error) {
console.error('Failed to register plugin:', error);
}
}
/**
* 注册 hooks 对象
*/
registerHooks(hooks: Hooks): void {
this.hooks.push(hooks);
}
/**
* 设置配置型 hooks
*/
setConfigHooks(config: HookConfig): void {
this.configHooks = config;
}
/**
* 添加事件监听器
*/
addEventListener(listener: HookEventListener): void {
this.eventListeners.push(listener);
}
/**
* 移除事件监听器
*/
removeEventListener(listener: HookEventListener): void {
const index = this.eventListeners.indexOf(listener);
if (index !== -1) {
this.eventListeners.splice(index, 1);
}
}
/**
* 发送事件
*/
private emitEvent(type: HookType, data: unknown): void {
const event: HookEvent = {
type,
timestamp: Date.now(),
data,
};
for (const listener of this.eventListeners) {
try {
listener(event);
} catch (error) {
console.error('Event listener error:', error);
}
}
}
/**
* 触发工具执行前 hook
*/
async triggerToolExecuteBefore(
input: ToolExecuteBeforeInput
): Promise<ToolExecuteBeforeOutput> {
const output: ToolExecuteBeforeOutput = {
args: { ...input.args },
};
for (const hook of this.hooks) {
if (hook['tool.execute.before']) {
try {
await hook['tool.execute.before'](input, output);
} catch (error) {
console.error('Hook tool.execute.before error:', error);
}
}
}
this.emitEvent('tool.execute.before', { input, output });
return output;
}
/**
* 触发工具执行后 hook
*/
async triggerToolExecuteAfter(
input: ToolExecuteAfterInput,
result: ToolExecuteAfterOutput['result']
): Promise<ToolExecuteAfterOutput> {
const output: ToolExecuteAfterOutput = {
result: { ...result },
};
for (const hook of this.hooks) {
if (hook['tool.execute.after']) {
try {
await hook['tool.execute.after'](input, output);
} catch (error) {
console.error('Hook tool.execute.after error:', error);
}
}
}
this.emitEvent('tool.execute.after', { input, output });
return output;
}
/**
* 触发会话开始 hook
*/
async triggerSessionStart(input: SessionStartInput): Promise<void> {
for (const hook of this.hooks) {
if (hook['session.start']) {
try {
await hook['session.start'](input);
} catch (error) {
console.error('Hook session.start error:', error);
}
}
}
this.emitEvent('session.start', input);
}
/**
* 触发会话结束 hook
*/
async triggerSessionEnd(input: SessionEndInput): Promise<void> {
for (const hook of this.hooks) {
if (hook['session.end']) {
try {
await hook['session.end'](input);
} catch (error) {
console.error('Hook session.end error:', error);
}
}
}
// 执行配置型 session_completed hooks
if (this.configHooks?.session_completed) {
await this.executeShellCommands(this.configHooks.session_completed);
}
this.emitEvent('session.end', input);
}
/**
* 触发消息前 hook
*/
async triggerMessageBefore(
input: MessageBeforeInput
): Promise<MessageBeforeOutput> {
const output: MessageBeforeOutput = {
content: input.content,
};
for (const hook of this.hooks) {
if (hook['message.before']) {
try {
await hook['message.before'](input, output);
} catch (error) {
console.error('Hook message.before error:', error);
}
}
}
this.emitEvent('message.before', { input, output });
return output;
}
/**
* 触发消息后 hook
*/
async triggerMessageAfter(input: MessageAfterInput): Promise<void> {
for (const hook of this.hooks) {
if (hook['message.after']) {
try {
await hook['message.after'](input);
} catch (error) {
console.error('Hook message.after error:', error);
}
}
}
this.emitEvent('message.after', input);
}
/**
* 触发文件编辑 hook
*/
async triggerFileEdited(input: FileChangeInput): Promise<FileChangeOutput> {
const output: FileChangeOutput = {};
// 执行插件 hooks
for (const hook of this.hooks) {
if (hook['file.edited']) {
try {
await hook['file.edited'](input, output);
} catch (error) {
console.error('Hook file.edited error:', error);
}
}
}
// 执行配置型 hooks
if (this.configHooks?.file_edited) {
const results = await this.executeFileHooks(
input.path,
this.configHooks.file_edited
);
output.commandResults = results;
}
this.emitEvent('file.edited', { input, output });
return output;
}
/**
* 触发文件创建 hook
*/
async triggerFileCreated(input: FileChangeInput): Promise<FileChangeOutput> {
const output: FileChangeOutput = {};
// 执行插件 hooks
for (const hook of this.hooks) {
if (hook['file.created']) {
try {
await hook['file.created'](input, output);
} catch (error) {
console.error('Hook file.created error:', error);
}
}
}
// 执行配置型 hooks
if (this.configHooks?.file_created) {
const results = await this.executeFileHooks(
input.path,
this.configHooks.file_created
);
output.commandResults = results;
}
this.emitEvent('file.created', { input, output });
return output;
}
/**
* 触发文件删除 hook
*/
async triggerFileDeleted(input: FileChangeInput): Promise<FileChangeOutput> {
const output: FileChangeOutput = {};
// 执行插件 hooks
for (const hook of this.hooks) {
if (hook['file.deleted']) {
try {
await hook['file.deleted'](input, output);
} catch (error) {
console.error('Hook file.deleted error:', error);
}
}
}
// 执行配置型 hooks
if (this.configHooks?.file_deleted) {
const results = await this.executeFileHooks(
input.path,
this.configHooks.file_deleted
);
output.commandResults = results;
}
this.emitEvent('file.deleted', { input, output });
return output;
}
/**
* 执行文件 hooks
* 根据文件路径匹配 glob 模式并执行对应命令
*/
private async executeFileHooks(
filePath: string,
config: FileHookConfig
): Promise<FileChangeOutput['commandResults']> {
const results: FileChangeOutput['commandResults'] = [];
for (const [pattern, commands] of Object.entries(config)) {
// 使用 minimatch 进行 glob 匹配
if (minimatch(filePath, pattern, { matchBase: true })) {
const commandResults = await this.executeShellCommands(commands, {
FILE_PATH: filePath,
});
results.push(...commandResults);
}
}
return results;
}
/**
* 执行 shell 命令列表
*/
private async executeShellCommands(
commands: ShellCommandConfig[],
extraEnv?: Record<string, string>
): Promise<Array<{ command: string[]; success: boolean; output?: string; error?: string }>> {
const results: Array<{
command: string[];
success: boolean;
output?: string;
error?: string;
}> = [];
for (const cmdConfig of commands) {
const result = await this.executeShellCommand(cmdConfig, extraEnv);
results.push(result);
}
return results;
}
/**
* 执行单个 shell 命令
*/
private executeShellCommand(
config: ShellCommandConfig,
extraEnv?: Record<string, string>
): Promise<{ command: string[]; success: boolean; output?: string; error?: string }> {
return new Promise((resolve) => {
const [cmd, ...args] = config.command;
const timeout = config.timeout || 30000;
const cwd = config.cwd || this.workdir;
const env = {
...process.env,
...config.environment,
...extraEnv,
};
let stdout = '';
let stderr = '';
const child = spawn(cmd, args, {
cwd,
env,
shell: true,
});
const timer = setTimeout(() => {
child.kill('SIGTERM');
resolve({
command: config.command,
success: false,
error: `Command timed out after ${timeout}ms`,
});
}, timeout);
child.stdout?.on('data', (data) => {
stdout += data.toString();
});
child.stderr?.on('data', (data) => {
stderr += data.toString();
});
child.on('close', (code) => {
clearTimeout(timer);
resolve({
command: config.command,
success: code === 0,
output: stdout.trim() || undefined,
error: stderr.trim() || undefined,
});
});
child.on('error', (error) => {
clearTimeout(timer);
resolve({
command: config.command,
success: false,
error: error.message,
});
});
});
}
/**
* 获取所有已注册的 hooks 数量
*/
getHookCount(): number {
return this.hooks.length;
}
/**
* 清空所有 hooks
*/
clear(): void {
this.hooks = [];
this.configHooks = null;
this.eventListeners = [];
}
}
// 全局 Hook 管理器实例
let globalHookManager: HookManager | null = null;
/**
* 获取全局 Hook 管理器
*/
export function getHookManager(): HookManager | null {
return globalHookManager;
}
/**
* 初始化全局 Hook 管理器
*/
export function initHookManager(workdir: string, sessionId?: string): HookManager {
globalHookManager = new HookManager(workdir, sessionId);
return globalHookManager;
}
/**
* 重置全局 Hook 管理器
*/
export function resetHookManager(): void {
if (globalHookManager) {
globalHookManager.clear();
globalHookManager = null;
}
}
+215
View File
@@ -0,0 +1,215 @@
/**
* Hook 系统类型定义
*
* 参考 open-code 的 hook 实现
*/
import type { Tool, ToolResult } from '../types/index.js';
/**
* Hook 类型枚举
*/
export type HookType =
| 'tool.execute.before' // 工具执行前
| 'tool.execute.after' // 工具执行后
| 'session.start' // 会话开始
| 'session.end' // 会话结束
| 'message.before' // 消息发送前
| 'message.after' // 消息接收后
| 'file.edited' // 文件被编辑后
| 'file.created' // 文件被创建后
| 'file.deleted'; // 文件被删除后
/**
* 工具执行前 Hook 的输入
*/
export interface ToolExecuteBeforeInput {
tool: string;
sessionId: string;
callId: string;
args: Record<string, unknown>;
}
/**
* 工具执行前 Hook 的输出(可修改)
*/
export interface ToolExecuteBeforeOutput {
args: Record<string, unknown>;
/** 设为 true 可阻止工具执行 */
skip?: boolean;
/** 跳过时返回的结果 */
skipResult?: ToolResult;
}
/**
* 工具执行后 Hook 的输入
*/
export interface ToolExecuteAfterInput {
tool: string;
sessionId: string;
callId: string;
args: Record<string, unknown>;
duration: number; // 执行时长(毫秒)
}
/**
* 工具执行后 Hook 的输出(可修改)
*/
export interface ToolExecuteAfterOutput {
result: ToolResult;
}
/**
* 会话开始 Hook 的输入
*/
export interface SessionStartInput {
sessionId: string;
workdir: string;
}
/**
* 会话结束 Hook 的输入
*/
export interface SessionEndInput {
sessionId: string;
messageCount: number;
duration: number; // 会话时长(毫秒)
}
/**
* 消息前 Hook 的输入
*/
export interface MessageBeforeInput {
sessionId: string;
content: string;
}
/**
* 消息前 Hook 的输出(可修改)
*/
export interface MessageBeforeOutput {
content: string;
/** 设为 true 可阻止消息发送 */
skip?: boolean;
}
/**
* 消息后 Hook 的输入
*/
export interface MessageAfterInput {
sessionId: string;
content: string;
toolCalls: number;
}
/**
* 文件变更 Hook 的输入
*/
export interface FileChangeInput {
path: string;
tool: string;
sessionId: string;
}
/**
* 文件变更 Hook 的输出
*/
export interface FileChangeOutput {
/** 执行的命令结果 */
commandResults?: Array<{
command: string[];
success: boolean;
output?: string;
error?: string;
}>;
}
/**
* Shell 命令配置
*/
export interface ShellCommandConfig {
/** 命令数组,第一个元素是命令,后面是参数 */
command: string[];
/** 环境变量 */
environment?: Record<string, string>;
/** 超时时间(毫秒),默认 30000 */
timeout?: number;
/** 工作目录,默认使用当前目录 */
cwd?: string;
}
/**
* 文件 Hook 配置
* 支持 glob 模式匹配文件
*/
export interface FileHookConfig {
/** glob 模式 -> 命令配置列表 */
[pattern: string]: ShellCommandConfig[];
}
/**
* Hook 配置
*/
export interface HookConfig {
/** 文件编辑后执行的 hook */
file_edited?: FileHookConfig;
/** 文件创建后执行的 hook */
file_created?: FileHookConfig;
/** 文件删除后执行的 hook */
file_deleted?: FileHookConfig;
/** 会话完成后执行的命令 */
session_completed?: ShellCommandConfig[];
}
/**
* Hook 函数类型
*/
export type HookFunction<Input, Output> = (
input: Input,
output: Output
) => Promise<void>;
/**
* Hook 定义接口
*/
export interface Hooks {
'tool.execute.before'?: HookFunction<ToolExecuteBeforeInput, ToolExecuteBeforeOutput>;
'tool.execute.after'?: HookFunction<ToolExecuteAfterInput, ToolExecuteAfterOutput>;
'session.start'?: (input: SessionStartInput) => Promise<void>;
'session.end'?: (input: SessionEndInput) => Promise<void>;
'message.before'?: HookFunction<MessageBeforeInput, MessageBeforeOutput>;
'message.after'?: (input: MessageAfterInput) => Promise<void>;
'file.edited'?: HookFunction<FileChangeInput, FileChangeOutput>;
'file.created'?: HookFunction<FileChangeInput, FileChangeOutput>;
'file.deleted'?: HookFunction<FileChangeInput, FileChangeOutput>;
}
/**
* 插件输入
*/
export interface PluginInput {
/** 当前工作目录 */
workdir: string;
/** 会话 ID */
sessionId?: string;
}
/**
* 插件定义
* 一个插件是一个函数,接收 PluginInput 返回 Hooks
*/
export type Plugin = (input: PluginInput) => Promise<Hooks>;
/**
* Hook 事件
*/
export interface HookEvent {
type: HookType;
timestamp: number;
data: unknown;
}
/**
* Hook 事件监听器
*/
export type HookEventListener = (event: HookEvent) => void;