mirror of
https://github.com/fish2018/pansou.git
synced 2025-11-25 03:14:59 +08:00
feat(backend): 添加后端服务管理和活动监控功能
- 实现BackendManager类用于启动、停止和监控PanSou后端服务 - 添加ActivityMonitor类用于跟踪工具调用活动并支持空闲超时关闭 - 创建start-backend工具定义用于通过MCP启动后端服务 - 支持强制重启、健康检查和空闲超时自动关闭功能
This commit is contained in:
131
typescript/src/tools/start-backend.ts
Normal file
131
typescript/src/tools/start-backend.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
148
typescript/src/utils/activity-monitor.ts
Normal file
148
typescript/src/utils/activity-monitor.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
352
typescript/src/utils/backend-manager.ts
Normal file
352
typescript/src/utils/backend-manager.ts
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user