feat(backend): 添加后端服务管理和活动监控功能

- 实现BackendManager类用于启动、停止和监控PanSou后端服务
- 添加ActivityMonitor类用于跟踪工具调用活动并支持空闲超时关闭
- 创建start-backend工具定义用于通过MCP启动后端服务
- 支持强制重启、健康检查和空闲超时自动关闭功能
This commit is contained in:
YAYOI27
2025-08-24 23:00:32 +08:00
parent 3b8412d73a
commit a9b41c6abe
3 changed files with 631 additions and 0 deletions

View File

@@ -0,0 +1,131 @@
import { Tool } from '@modelcontextprotocol/sdk/types.js';
import { BackendManager } from '../utils/backend-manager.js';
import { HttpClient } from '../utils/http-client.js';
import { Config } from '../utils/config.js';
/**
* 启动后端服务工具定义
*/
export const startBackendTool: Tool = {
name: 'start_backend',
description: '启动PanSou后端服务。如果后端服务未运行此工具将启动它并等待服务完全可用。',
inputSchema: {
type: 'object',
properties: {
force_restart: {
type: 'boolean',
description: '是否强制重启后端服务(即使已在运行)',
default: false
}
},
additionalProperties: false
}
};
/**
* 启动后端服务工具参数接口
*/
interface StartBackendArgs {
force_restart?: boolean;
}
/**
* 执行启动后端服务工具
*/
export async function executeStartBackendTool(
args: unknown,
httpClient?: HttpClient,
config?: Config
): Promise<string> {
try {
// 参数验证
const params = args as StartBackendArgs;
const forceRestart = params?.force_restart || false;
console.log('🚀 启动后端服务工具被调用');
// 如果没有提供依赖项,则创建默认实例
if (!config) {
const { loadConfig } = await import('../utils/config.js');
config = loadConfig();
}
if (!httpClient) {
const { HttpClient } = await import('../utils/http-client.js');
httpClient = new HttpClient(config);
}
// 创建后端管理器
const backendManager = new BackendManager(config, httpClient);
// 检查当前服务状态
httpClient.setSilentMode(true);
const isHealthy = await httpClient.testConnection();
httpClient.setSilentMode(false);
if (isHealthy && !forceRestart) {
return JSON.stringify({
success: true,
message: '后端服务已在运行',
status: 'already_running',
service_url: config.serverUrl
}, null, 2);
}
if (isHealthy && forceRestart) {
console.log('🔄 强制重启后端服务...');
}
console.log('🚀 正在启动后端服务...');
const started = await backendManager.startBackend();
if (!started) {
return JSON.stringify({
success: false,
message: '后端服务启动失败',
status: 'start_failed',
error: '无法启动后端服务,请检查配置和权限'
}, null, 2);
}
// 等待服务完全启动并进行健康检查
console.log('⏳ 等待服务完全启动...');
const maxRetries = 10;
let retries = 0;
while (retries < maxRetries) {
await new Promise(resolve => setTimeout(resolve, 1000)); // 等待1秒
const healthy = await httpClient.testConnection();
if (healthy) {
console.log('✅ 后端服务启动成功并通过健康检查');
return JSON.stringify({
success: true,
message: '后端服务启动成功',
status: 'started',
service_url: config.serverUrl,
startup_time: `${retries + 1}`
}, null, 2);
}
retries++;
console.log(`🔍 健康检查重试 ${retries}/${maxRetries}...`);
}
return JSON.stringify({
success: false,
message: '后端服务启动超时',
status: 'timeout',
error: '服务启动后未能通过健康检查,可能需要更多时间或存在配置问题'
}, null, 2);
} catch (error) {
console.error('启动后端服务时发生错误:', error);
return JSON.stringify({
success: false,
message: '启动后端服务时发生错误',
status: 'error',
error: error instanceof Error ? error.message : String(error)
}, null, 2);
}
}

View File

@@ -0,0 +1,148 @@
/**
* 活动监控器 - 跟踪MCP工具调用活动
*/
export class ActivityMonitor {
private lastActivityTime: number;
private idleTimeout: number;
private enableIdleShutdown: boolean;
private idleTimer: NodeJS.Timeout | null = null;
private onIdleCallback: (() => void) | null = null;
constructor(idleTimeout: number = 300000, enableIdleShutdown: boolean = true) {
this.lastActivityTime = Date.now();
this.idleTimeout = idleTimeout;
this.enableIdleShutdown = enableIdleShutdown;
}
/**
* 记录活动
*/
recordActivity(): void {
this.lastActivityTime = Date.now();
this.resetIdleTimer();
}
/**
* 获取最后活动时间
*/
getLastActivityTime(): number {
return this.lastActivityTime;
}
/**
* 获取空闲时间(毫秒)
*/
getIdleTime(): number {
return Date.now() - this.lastActivityTime;
}
/**
* 检查是否空闲超时
*/
isIdleTimeout(): boolean {
return this.getIdleTime() >= this.idleTimeout;
}
/**
* 设置空闲回调函数
*/
setOnIdleCallback(callback: () => void): void {
this.onIdleCallback = callback;
this.resetIdleTimer();
}
/**
* 重置空闲计时器
*/
private resetIdleTimer(): void {
if (!this.enableIdleShutdown || !this.onIdleCallback) {
return;
}
// 清除现有计时器
if (this.idleTimer) {
clearTimeout(this.idleTimer);
}
// 设置新的计时器
this.idleTimer = setTimeout(() => {
if (this.onIdleCallback) {
console.log(`[ActivityMonitor] 检测到空闲超时 (${this.idleTimeout}ms),触发空闲回调`);
this.onIdleCallback();
}
}, this.idleTimeout);
}
/**
* 停止监控
*/
stop(): void {
if (this.idleTimer) {
clearTimeout(this.idleTimer);
this.idleTimer = null;
}
this.onIdleCallback = null;
}
/**
* 更新配置
*/
updateConfig(idleTimeout: number, enableIdleShutdown: boolean): void {
this.idleTimeout = idleTimeout;
this.enableIdleShutdown = enableIdleShutdown;
this.resetIdleTimer();
}
/**
* 获取状态信息
*/
getStatus(): {
lastActivityTime: number;
idleTime: number;
idleTimeout: number;
enableIdleShutdown: boolean;
isIdleTimeout: boolean;
} {
return {
lastActivityTime: this.lastActivityTime,
idleTime: this.getIdleTime(),
idleTimeout: this.idleTimeout,
enableIdleShutdown: this.enableIdleShutdown,
isIdleTimeout: this.isIdleTimeout()
};
}
}
// 全局活动监控器实例
let globalActivityMonitor: ActivityMonitor | null = null;
/**
* 获取全局活动监控器实例
*/
export function getActivityMonitor(): ActivityMonitor {
if (!globalActivityMonitor) {
throw new Error('活动监控器未初始化,请先调用 initializeActivityMonitor');
}
return globalActivityMonitor;
}
/**
* 初始化全局活动监控器
*/
export function initializeActivityMonitor(idleTimeout: number, enableIdleShutdown: boolean): ActivityMonitor {
if (globalActivityMonitor) {
globalActivityMonitor.stop();
}
globalActivityMonitor = new ActivityMonitor(idleTimeout, enableIdleShutdown);
return globalActivityMonitor;
}
/**
* 停止全局活动监控器
*/
export function stopActivityMonitor(): void {
if (globalActivityMonitor) {
globalActivityMonitor.stop();
globalActivityMonitor = null;
}
}

View File

@@ -0,0 +1,352 @@
import { spawn, ChildProcess } from 'child_process';
import { promises as fs } from 'fs';
import path from 'path';
import { HttpClient } from './http-client.js';
import { Config } from './config.js';
import { ActivityMonitor } from './activity-monitor.js';
/**
* 后端服务管理器
* 负责自动启动、停止和监控PanSou Go后端服务
*/
export class BackendManager {
private process: ChildProcess | null = null;
private config: Config;
private httpClient: HttpClient;
private shutdownTimeout: NodeJS.Timeout | null = null;
private isShuttingDown = false;
private readonly SHUTDOWN_DELAY = 5000; // 5秒延迟关闭
private readonly STARTUP_TIMEOUT = 30000; // 30秒启动超时
private readonly HEALTH_CHECK_INTERVAL = 1000; // 1秒健康检查间隔
private activityMonitor: ActivityMonitor | null = null;
constructor(config: Config, httpClient: HttpClient) {
this.config = config;
this.httpClient = httpClient;
// 初始化活动监控器
if (this.config.enableIdleShutdown) {
this.activityMonitor = new ActivityMonitor(
this.config.idleTimeout,
this.config.enableIdleShutdown
);
// 设置空闲监控回调
this.activityMonitor.setOnIdleCallback(async () => {
console.error('⏰ 检测到空闲超时,自动关闭后端服务');
await this.stopBackend();
// 退出整个进程
process.exit(0);
});
console.error(`⏱️ 空闲监控已启用,超时时间: ${this.config.idleTimeout / 1000}`);
}
}
/**
* 检查后端服务是否正在运行
*/
async isBackendRunning(): Promise<boolean> {
try {
return await this.httpClient.testConnection();
} catch (error) {
return false;
}
}
/**
* 查找Go可执行文件路径
*/
private async findGoExecutable(): Promise<string | null> {
// 优先使用配置中的项目根目录
const configProjectRoot = this.config.projectRootPath;
const possiblePaths: string[] = [];
// 如果配置了项目根目录,直接在该目录下查找
if (configProjectRoot) {
possiblePaths.push(
path.join(configProjectRoot, 'pansou.exe'),
path.join(configProjectRoot, 'main.exe')
);
} else {
// 仅在没有配置项目根目录时才使用备用路径
possiblePaths.push(
// 当前工作目录
path.join(process.cwd(), 'pansou.exe'),
path.join(process.cwd(), 'main.exe'),
// 上级目录如果MCP在子目录中
path.join(process.cwd(), '..', 'pansou.exe'),
path.join(process.cwd(), '..', 'main.exe')
);
}
console.error('🔍 查找后端可执行文件...');
if (configProjectRoot) {
console.error(`📂 使用配置的项目根目录: ${configProjectRoot}`);
} else {
console.error(`📂 当前工作目录: ${process.cwd()}`);
}
for (const execPath of possiblePaths) {
try {
await fs.access(execPath);
console.error(`✅ 找到可执行文件: ${execPath}`);
return execPath;
} catch {
// 静默跳过未找到的路径
}
}
console.error('❌ 未找到可执行文件');
return null;
}
/**
* 启动后端服务
*/
async startBackend(): Promise<boolean> {
if (this.process) {
console.error('⚠️ 后端服务已在运行中');
return true;
}
// 首先检查是否已有服务在运行
this.httpClient.setSilentMode(true);
const isRunning = await this.isBackendRunning();
this.httpClient.setSilentMode(false);
if (isRunning) {
console.error('✅ 检测到后端服务已在运行');
return true;
}
// 查找Go可执行文件
const execPath = await this.findGoExecutable();
if (!execPath) {
console.error('❌ 未找到PanSou后端可执行文件');
console.error('请确保在项目根目录下存在以下文件之一:');
console.error(' - pansou.exe / pansou');
console.error(' - main.exe / main');
return false;
}
console.error(`🚀 启动后端服务: ${execPath}`);
try {
// 启动Go服务
this.process = spawn(execPath, [], {
cwd: path.dirname(execPath),
stdio: ['ignore', 'pipe', 'pipe'],
detached: false,
windowsHide: true
});
// 监听进程事件
this.process.on('error', (error) => {
console.error('❌ 后端服务启动失败:', error.message);
console.error('错误详情:', error);
this.process = null;
});
this.process.on('exit', (code, signal) => {
if (!this.isShuttingDown) {
console.error(`⚠️ 后端服务意外退出 (code: ${code}, signal: ${signal})`);
}
this.process = null;
});
// 添加进程启动确认
console.error(`📋 进程PID: ${this.process.pid}`);
console.error(`📂 工作目录: ${path.dirname(execPath)}`);
console.error(`⚙️ 启动参数: ${execPath}`);
// 给进程一点时间启动
await new Promise(resolve => setTimeout(resolve, 1000));
// 捕获输出(用于调试)
if (this.process.stdout) {
this.process.stdout.on('data', (data) => {
console.error('Backend stdout:', data.toString().trim());
});
}
if (this.process.stderr) {
this.process.stderr.on('data', (data) => {
console.error('Backend stderr:', data.toString().trim());
});
}
// 等待服务启动
const started = await this.waitForBackendReady();
if (started) {
console.error('✅ 后端服务启动成功');
// 空闲监控已在构造函数中设置
return true;
} else {
console.error('❌ 后端服务启动超时');
await this.stopBackend();
return false;
}
} catch (error) {
console.error('❌ 启动后端服务时发生错误:', error);
return false;
}
}
/**
* 等待后端服务就绪
*/
private async waitForBackendReady(): Promise<boolean> {
const startTime = Date.now();
// 在等待期间启用静默模式,避免输出网络错误
const originalSilentMode = this.httpClient.isSilentMode();
this.httpClient.setSilentMode(true);
try {
while (Date.now() - startTime < this.STARTUP_TIMEOUT) {
if (await this.isBackendRunning()) {
return true;
}
// 检查进程是否还在运行
if (!this.process || this.process.killed) {
return false;
}
// 等待一段时间后重试
await new Promise(resolve => setTimeout(resolve, this.HEALTH_CHECK_INTERVAL));
}
return false;
} finally {
// 恢复原始静默模式状态
this.httpClient.setSilentMode(originalSilentMode);
}
}
/**
* 停止后端服务
*/
async stopBackend(): Promise<void> {
if (!this.process) {
return;
}
console.error('🛑 正在停止后端服务...');
this.isShuttingDown = true;
try {
// 尝试优雅关闭
this.process.kill('SIGTERM');
// 等待进程退出
await new Promise<void>((resolve) => {
if (!this.process) {
resolve();
return;
}
const timeout = setTimeout(() => {
// 强制杀死进程
if (this.process && !this.process.killed) {
console.error('⚠️ 强制终止后端服务');
this.process.kill('SIGKILL');
}
resolve();
}, 5000);
this.process.on('exit', () => {
clearTimeout(timeout);
resolve();
});
});
console.error('✅ 后端服务已停止');
} catch (error) {
console.error('❌ 停止后端服务时发生错误:', error);
} finally {
this.process = null;
this.isShuttingDown = false;
}
}
/**
* 延迟停止后端服务
*/
scheduleShutdown(): void {
if (this.shutdownTimeout) {
clearTimeout(this.shutdownTimeout);
}
console.error(`⏰ 将在 ${this.SHUTDOWN_DELAY / 1000} 秒后关闭后端服务`);
this.shutdownTimeout = setTimeout(async () => {
await this.stopBackend();
this.shutdownTimeout = null;
}, this.SHUTDOWN_DELAY);
}
/**
* 取消计划的关闭
*/
cancelShutdown(): void {
if (this.shutdownTimeout) {
clearTimeout(this.shutdownTimeout);
this.shutdownTimeout = null;
console.error('⏸️ 取消后端服务关闭计划');
}
}
/**
* 获取后端服务状态
*/
getStatus(): {
processRunning: boolean;
serviceReachable: boolean;
pid?: number;
} {
return {
processRunning: this.process !== null && !this.process.killed,
serviceReachable: false, // 需要异步检查
pid: this.process?.pid
};
}
/**
* 记录活动(重置空闲计时器)
*/
recordActivity(): void {
if (this.activityMonitor) {
this.activityMonitor.recordActivity();
}
}
/**
* 获取活动监控状态
*/
getActivityStatus(): any {
return this.activityMonitor ? this.activityMonitor.getStatus() : null;
}
/**
* 清理资源
*/
async cleanup(): Promise<void> {
this.cancelShutdown();
if (this.activityMonitor) {
this.activityMonitor.stop();
this.activityMonitor = null;
}
await this.stopBackend();
}
}
/**
* 创建后端管理器实例
*/
export function createBackendManager(config: Config, httpClient: HttpClient): BackendManager {
return new BackendManager(config, httpClient);
}