Files
ai-terminal-assistant/packages/server/src/routes/hooks.ts
T
kurihada 1b7d55848d refactor(server): 消除与 Core 的重复类型定义
- 删除 Server 中 60+ 个与 Core 重复的类型定义
- 将动态导入 (await import) 改为静态类型导入 (import type)
- 保留必要的运行时静态导入
- 修复测试文件中的 mock 初始化问题
- 净删除约 960 行重复代码

重构文件:
- routes/checkpoints.ts: 删除 155 行重复类型
- routes/agents.ts: 删除 93 行重复类型
- routes/commands.ts: 删除 83 行重复类型
- routes/mcp.ts: 修复类型窄化
- routes/hooks.ts: 已使用静态导入
- routes/providers.ts: 删除 63 行重复类型
- session/manager.ts: 删除 41 行重复类型
- routes/sessions.ts: 添加类型导入
- permission/handler.ts: 添加类型导入
2025-12-16 20:19:24 +08:00

401 lines
9.3 KiB
TypeScript

/**
* Hooks API Routes
*
* 提供 Hooks 配置管理的 REST API
*/
import { Hono } from 'hono';
import * as fs from 'fs/promises';
import * as path from 'path';
import { spawn } from 'child_process';
import { getConfig } from './config.js';
import type {
HookConfig,
ProjectConfig,
ShellCommandConfig,
FileHookConfig,
} from '@ai-assistant/core';
import {
loadProjectConfig,
loadHookConfig,
getConfigFilePath,
createDefaultConfig,
} from '@ai-assistant/core';
interface HookTestResult {
success: boolean;
stdout: string;
stderr: string;
exitCode: number;
duration: number;
}
export const hooksRouter = new Hono();
/**
* 移除 JSON 中的注释(支持 JSONC 格式)
*/
function stripJsonComments(jsonString: string): string {
let result = jsonString.replace(/\/\/.*$/gm, '');
result = result.replace(/\/\*[\s\S]*?\*\//g, '');
return result;
}
/**
* 读取配置文件
*/
async function readConfigFile(configPath: string): Promise<ProjectConfig | null> {
try {
const content = await fs.readFile(configPath, 'utf-8');
const cleanContent = stripJsonComments(content);
return JSON.parse(cleanContent);
} catch {
return null;
}
}
/**
* 写入配置文件
*/
async function writeConfigFile(configPath: string, config: ProjectConfig): Promise<void> {
await fs.writeFile(configPath, JSON.stringify(config, null, 2));
}
/**
* 获取或创建配置文件路径
*/
async function getOrCreateConfigPath(workdir: string): Promise<string> {
const existingPath = await getConfigFilePath(workdir);
if (existingPath) return existingPath;
return path.join(workdir, '.ai-assistant.json');
}
/**
* GET /hooks/config - 获取完整钩子配置
*/
hooksRouter.get('/config', async (c) => {
const config = getConfig();
const hookConfig = await loadHookConfig(config.workdir);
return c.json({
success: true,
data: hookConfig || {
file_edited: {},
file_created: {},
file_deleted: {},
session_completed: [],
},
});
});
/**
* PUT /hooks/config - 更新完整钩子配置
*/
hooksRouter.put('/config', async (c) => {
try {
const newHookConfig = await c.req.json<HookConfig>();
const config = getConfig();
const configPath = await getOrCreateConfigPath(config.workdir);
// 读取现有配置
let projectConfig = await readConfigFile(configPath);
if (!projectConfig) {
projectConfig = {};
}
// 更新 hooks 部分
projectConfig.hooks = newHookConfig;
// 写入配置文件
await writeConfigFile(configPath, projectConfig);
return c.json({
success: true,
data: newHookConfig,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update config',
},
500
);
}
});
/**
* GET /hooks/file-edited - 获取 file_edited 钩子
*/
hooksRouter.get('/file-edited', async (c) => {
const config = getConfig();
const hookConfig = await loadHookConfig(config.workdir);
return c.json({
success: true,
data: hookConfig?.file_edited || {},
});
});
/**
* PUT /hooks/file-edited - 更新 file_edited 钩子
*/
hooksRouter.put('/file-edited', async (c) => {
try {
const newFileEditedHooks = await c.req.json<FileHookConfig>();
const config = getConfig();
const configPath = await getOrCreateConfigPath(config.workdir);
let projectConfig = await readConfigFile(configPath);
if (!projectConfig) {
projectConfig = {};
}
if (!projectConfig.hooks) {
projectConfig.hooks = {};
}
projectConfig.hooks.file_edited = newFileEditedHooks;
await writeConfigFile(configPath, projectConfig);
return c.json({
success: true,
data: newFileEditedHooks,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update file_edited hooks',
},
500
);
}
});
/**
* GET /hooks/file-created - 获取 file_created 钩子
*/
hooksRouter.get('/file-created', async (c) => {
const config = getConfig();
const hookConfig = await loadHookConfig(config.workdir);
return c.json({
success: true,
data: hookConfig?.file_created || {},
});
});
/**
* PUT /hooks/file-created - 更新 file_created 钩子
*/
hooksRouter.put('/file-created', async (c) => {
try {
const newFileCreatedHooks = await c.req.json<FileHookConfig>();
const config = getConfig();
const configPath = await getOrCreateConfigPath(config.workdir);
let projectConfig = await readConfigFile(configPath);
if (!projectConfig) {
projectConfig = {};
}
if (!projectConfig.hooks) {
projectConfig.hooks = {};
}
projectConfig.hooks.file_created = newFileCreatedHooks;
await writeConfigFile(configPath, projectConfig);
return c.json({
success: true,
data: newFileCreatedHooks,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update file_created hooks',
},
500
);
}
});
/**
* GET /hooks/file-deleted - 获取 file_deleted 钩子
*/
hooksRouter.get('/file-deleted', async (c) => {
const config = getConfig();
const hookConfig = await loadHookConfig(config.workdir);
return c.json({
success: true,
data: hookConfig?.file_deleted || {},
});
});
/**
* PUT /hooks/file-deleted - 更新 file_deleted 钩子
*/
hooksRouter.put('/file-deleted', async (c) => {
try {
const newFileDeletedHooks = await c.req.json<FileHookConfig>();
const config = getConfig();
const configPath = await getOrCreateConfigPath(config.workdir);
let projectConfig = await readConfigFile(configPath);
if (!projectConfig) {
projectConfig = {};
}
if (!projectConfig.hooks) {
projectConfig.hooks = {};
}
projectConfig.hooks.file_deleted = newFileDeletedHooks;
await writeConfigFile(configPath, projectConfig);
return c.json({
success: true,
data: newFileDeletedHooks,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update file_deleted hooks',
},
500
);
}
});
/**
* GET /hooks/session-completed - 获取 session_completed 钩子
*/
hooksRouter.get('/session-completed', async (c) => {
const config = getConfig();
const hookConfig = await loadHookConfig(config.workdir);
return c.json({
success: true,
data: hookConfig?.session_completed || [],
});
});
/**
* PUT /hooks/session-completed - 更新 session_completed 钩子
*/
hooksRouter.put('/session-completed', async (c) => {
try {
const newSessionCompletedHooks = await c.req.json<ShellCommandConfig[]>();
const config = getConfig();
const configPath = await getOrCreateConfigPath(config.workdir);
let projectConfig = await readConfigFile(configPath);
if (!projectConfig) {
projectConfig = {};
}
if (!projectConfig.hooks) {
projectConfig.hooks = {};
}
projectConfig.hooks.session_completed = newSessionCompletedHooks;
await writeConfigFile(configPath, projectConfig);
return c.json({
success: true,
data: newSessionCompletedHooks,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to update session_completed hooks',
},
500
);
}
});
/**
* POST /hooks/test - 测试执行钩子命令
*/
hooksRouter.post('/test', async (c) => {
try {
const commandConfig = await c.req.json<ShellCommandConfig>();
const config = getConfig();
// 验证命令配置
if (!commandConfig.command || !Array.isArray(commandConfig.command) || commandConfig.command.length === 0) {
return c.json(
{
success: false,
error: 'Invalid command configuration: command must be a non-empty array',
},
400
);
}
const startTime = Date.now();
const timeout = commandConfig.timeout || 30000;
const cwd = commandConfig.cwd || config.workdir;
// 执行命令
const result = await new Promise<HookTestResult>((resolve) => {
const [cmd, ...args] = commandConfig.command;
const proc = spawn(cmd, args, {
cwd,
env: {
...process.env,
...commandConfig.environment,
},
shell: true,
timeout,
});
let stdout = '';
let stderr = '';
proc.stdout?.on('data', (data) => {
stdout += data.toString();
});
proc.stderr?.on('data', (data) => {
stderr += data.toString();
});
proc.on('close', (code) => {
resolve({
success: code === 0,
stdout,
stderr,
exitCode: code ?? -1,
duration: Date.now() - startTime,
});
});
proc.on('error', (error) => {
resolve({
success: false,
stdout,
stderr: error.message,
exitCode: -1,
duration: Date.now() - startTime,
});
});
});
return c.json({
success: true,
data: result,
});
} catch (error) {
return c.json(
{
success: false,
error: error instanceof Error ? error.message : 'Failed to test command',
},
500
);
}
});