1b7d55848d
- 删除 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: 添加类型导入
401 lines
9.3 KiB
TypeScript
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
|
|
);
|
|
}
|
|
});
|