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:
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user