229 lines
9.3 KiB
JavaScript
229 lines
9.3 KiB
JavaScript
|
|
import { exec, spawn } from "child_process";
|
|||
|
|
import { promisify } from "util";
|
|||
|
|
import { existsSync, readFileSync } from "fs";
|
|||
|
|
import { join } from "path";
|
|||
|
|
const execAsync = promisify(exec);
|
|||
|
|
// 进程管理器 - 跟踪所有启动的开发服务器
|
|||
|
|
const managedProcesses = new Map();
|
|||
|
|
export const devServerTool = {
|
|||
|
|
name: "dev_server",
|
|||
|
|
description: "开发服务器管理。启动/停止/重启开发服务器,查看运行状态和实时日志。支持自动检测项目的 dev 命令。",
|
|||
|
|
inputSchema: {
|
|||
|
|
type: "object",
|
|||
|
|
properties: {
|
|||
|
|
project_path: {
|
|||
|
|
type: "string",
|
|||
|
|
description: "项目根目录(绝对路径)",
|
|||
|
|
},
|
|||
|
|
action: {
|
|||
|
|
type: "string",
|
|||
|
|
description: "操作类型",
|
|||
|
|
enum: ["start", "stop", "restart", "status", "logs", "list"],
|
|||
|
|
},
|
|||
|
|
command: {
|
|||
|
|
type: "string",
|
|||
|
|
description: "自定义启动命令(可选,默认自动检测 npm run dev 等)",
|
|||
|
|
},
|
|||
|
|
name: {
|
|||
|
|
type: "string",
|
|||
|
|
description: "服务器名称/标识(可选,默认使用目录名)",
|
|||
|
|
},
|
|||
|
|
tail: {
|
|||
|
|
type: "number",
|
|||
|
|
description: "显示最近几行日志(默认 30)",
|
|||
|
|
},
|
|||
|
|
},
|
|||
|
|
required: ["action"],
|
|||
|
|
},
|
|||
|
|
};
|
|||
|
|
function getServerName(projectPath, name) {
|
|||
|
|
if (name)
|
|||
|
|
return name;
|
|||
|
|
if (projectPath)
|
|||
|
|
return projectPath.split(/[/\\]/).pop() || "server";
|
|||
|
|
return "default";
|
|||
|
|
}
|
|||
|
|
function detectDevCommand(projectPath) {
|
|||
|
|
const pkgPath = join(projectPath, "package.json");
|
|||
|
|
if (existsSync(pkgPath)) {
|
|||
|
|
try {
|
|||
|
|
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|||
|
|
const scripts = pkg.scripts || {};
|
|||
|
|
// 按优先级检测
|
|||
|
|
for (const name of ["dev", "start:dev", "serve", "start"]) {
|
|||
|
|
if (scripts[name])
|
|||
|
|
return `npm run ${name}`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
}
|
|||
|
|
if (existsSync(join(projectPath, "manage.py"))) {
|
|||
|
|
return "python manage.py runserver";
|
|||
|
|
}
|
|||
|
|
if (existsSync(join(projectPath, "main.py"))) {
|
|||
|
|
return "python main.py";
|
|||
|
|
}
|
|||
|
|
if (existsSync(join(projectPath, "app.py"))) {
|
|||
|
|
return "python app.py";
|
|||
|
|
}
|
|||
|
|
return null;
|
|||
|
|
}
|
|||
|
|
export async function executeDevServer(args) {
|
|||
|
|
const { project_path, action, command, name, tail = 30 } = args;
|
|||
|
|
switch (action) {
|
|||
|
|
case "list": {
|
|||
|
|
if (managedProcesses.size === 0) {
|
|||
|
|
return "# 开发服务器\n\n_没有正在运行的服务器_";
|
|||
|
|
}
|
|||
|
|
const output = ["# 运行中的开发服务器", ""];
|
|||
|
|
for (const [key, info] of managedProcesses) {
|
|||
|
|
const running = !info.process.killed && info.process.exitCode === null;
|
|||
|
|
const uptime = Math.round((Date.now() - info.startTime) / 1000);
|
|||
|
|
output.push(`## ${running ? "🟢" : "🔴"} ${key}`, `- 命令: \`${info.command}\``, `- 目录: ${info.cwd}`, `- PID: ${info.process.pid}`, `- 运行时间: ${uptime}s`, `- 状态: ${running ? "运行中" : "已停止"}`, ``);
|
|||
|
|
}
|
|||
|
|
return output.join("\n");
|
|||
|
|
}
|
|||
|
|
case "start": {
|
|||
|
|
if (!project_path)
|
|||
|
|
return "❌ start 需要 project_path 参数";
|
|||
|
|
const serverName = getServerName(project_path, name);
|
|||
|
|
const existing = managedProcesses.get(serverName);
|
|||
|
|
if (existing && !existing.process.killed && existing.process.exitCode === null) {
|
|||
|
|
return `⚠️ 服务器 "${serverName}" 已在运行中 (PID: ${existing.process.pid})\n\n使用 action=restart 重启,或 action=stop 先停止`;
|
|||
|
|
}
|
|||
|
|
const startCmd = command || detectDevCommand(project_path);
|
|||
|
|
if (!startCmd) {
|
|||
|
|
return `❌ 未检测到启动命令,请手动指定 command 参数\n\n常见命令:\n- npm run dev\n- npm start\n- python app.py`;
|
|||
|
|
}
|
|||
|
|
// 解析命令
|
|||
|
|
const isWin = process.platform === "win32";
|
|||
|
|
const child = spawn(isWin ? "cmd" : "sh", [isWin ? "/c" : "-c", startCmd], {
|
|||
|
|
cwd: project_path,
|
|||
|
|
stdio: ["ignore", "pipe", "pipe"],
|
|||
|
|
detached: false,
|
|||
|
|
});
|
|||
|
|
const logs = [];
|
|||
|
|
const maxLogs = 200;
|
|||
|
|
const addLog = (data, stream) => {
|
|||
|
|
const lines = data.toString().split("\n").filter(Boolean);
|
|||
|
|
for (const line of lines) {
|
|||
|
|
logs.push(`[${stream}] ${line}`);
|
|||
|
|
if (logs.length > maxLogs)
|
|||
|
|
logs.shift();
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
child.stdout?.on("data", (data) => addLog(data, "out"));
|
|||
|
|
child.stderr?.on("data", (data) => addLog(data, "err"));
|
|||
|
|
managedProcesses.set(serverName, {
|
|||
|
|
process: child,
|
|||
|
|
command: startCmd,
|
|||
|
|
cwd: project_path,
|
|||
|
|
startTime: Date.now(),
|
|||
|
|
logs,
|
|||
|
|
});
|
|||
|
|
// 等待一会检查是否立即崩溃
|
|||
|
|
await new Promise((r) => setTimeout(r, 2000));
|
|||
|
|
const crashed = child.exitCode !== null;
|
|||
|
|
if (crashed) {
|
|||
|
|
const output = logs.join("\n");
|
|||
|
|
managedProcesses.delete(serverName);
|
|||
|
|
return `# ❌ 服务器启动失败\n\n命令: \`${startCmd}\`\n退出码: ${child.exitCode}\n\n\`\`\`\n${output.slice(0, 3000)}\n\`\`\``;
|
|||
|
|
}
|
|||
|
|
return [
|
|||
|
|
`# ✅ 服务器已启动`,
|
|||
|
|
``,
|
|||
|
|
`- 名称: ${serverName}`,
|
|||
|
|
`- 命令: \`${startCmd}\``,
|
|||
|
|
`- PID: ${child.pid}`,
|
|||
|
|
`- 目录: ${project_path}`,
|
|||
|
|
``,
|
|||
|
|
`最近日志:`,
|
|||
|
|
"```",
|
|||
|
|
logs.slice(-10).join("\n") || "(等待输出...)",
|
|||
|
|
"```",
|
|||
|
|
``,
|
|||
|
|
`💡 使用 \`dev_server action=logs\` 查看实时日志`,
|
|||
|
|
].join("\n");
|
|||
|
|
}
|
|||
|
|
case "stop": {
|
|||
|
|
const serverName = getServerName(project_path, name);
|
|||
|
|
const info = managedProcesses.get(serverName);
|
|||
|
|
if (!info) {
|
|||
|
|
return `❌ 未找到服务器 "${serverName}"\n\n使用 action=list 查看所有运行中的服务器`;
|
|||
|
|
}
|
|||
|
|
const pid = info.process.pid;
|
|||
|
|
try {
|
|||
|
|
// Windows 需要 taskkill 杀进程树
|
|||
|
|
if (process.platform === "win32" && pid) {
|
|||
|
|
await execAsync(`taskkill /PID ${pid} /T /F`).catch(() => { });
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
info.process.kill("SIGTERM");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
managedProcesses.delete(serverName);
|
|||
|
|
return `# ✅ 服务器已停止\n\n- 名称: ${serverName}\n- PID: ${pid}`;
|
|||
|
|
}
|
|||
|
|
case "restart": {
|
|||
|
|
const serverName = getServerName(project_path, name);
|
|||
|
|
const info = managedProcesses.get(serverName);
|
|||
|
|
if (info) {
|
|||
|
|
try {
|
|||
|
|
if (process.platform === "win32" && info.process.pid) {
|
|||
|
|
await execAsync(`taskkill /PID ${info.process.pid} /T /F`).catch(() => { });
|
|||
|
|
}
|
|||
|
|
else {
|
|||
|
|
info.process.kill("SIGTERM");
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
catch { }
|
|||
|
|
managedProcesses.delete(serverName);
|
|||
|
|
await new Promise((r) => setTimeout(r, 1000));
|
|||
|
|
}
|
|||
|
|
// 重新启动
|
|||
|
|
return executeDevServer({
|
|||
|
|
project_path: info?.cwd || project_path,
|
|||
|
|
action: "start",
|
|||
|
|
command: command || info?.command,
|
|||
|
|
name: serverName,
|
|||
|
|
});
|
|||
|
|
}
|
|||
|
|
case "status": {
|
|||
|
|
const serverName = getServerName(project_path, name);
|
|||
|
|
const info = managedProcesses.get(serverName);
|
|||
|
|
if (!info) {
|
|||
|
|
return `❌ 未找到服务器 "${serverName}"`;
|
|||
|
|
}
|
|||
|
|
const running = !info.process.killed && info.process.exitCode === null;
|
|||
|
|
const uptime = Math.round((Date.now() - info.startTime) / 1000);
|
|||
|
|
return [
|
|||
|
|
`# ${running ? "🟢" : "🔴"} ${serverName}`,
|
|||
|
|
``,
|
|||
|
|
`- 状态: ${running ? "运行中" : `已停止 (退出码: ${info.process.exitCode})`}`,
|
|||
|
|
`- 命令: \`${info.command}\``,
|
|||
|
|
`- PID: ${info.process.pid}`,
|
|||
|
|
`- 运行时间: ${uptime}s`,
|
|||
|
|
`- 目录: ${info.cwd}`,
|
|||
|
|
].join("\n");
|
|||
|
|
}
|
|||
|
|
case "logs": {
|
|||
|
|
const serverName = getServerName(project_path, name);
|
|||
|
|
const info = managedProcesses.get(serverName);
|
|||
|
|
if (!info) {
|
|||
|
|
return `❌ 未找到服务器 "${serverName}"`;
|
|||
|
|
}
|
|||
|
|
const recentLogs = info.logs.slice(-tail);
|
|||
|
|
return [
|
|||
|
|
`# 📋 ${serverName} 日志(最近 ${tail} 行)`,
|
|||
|
|
``,
|
|||
|
|
"```",
|
|||
|
|
recentLogs.join("\n") || "(无日志)",
|
|||
|
|
"```",
|
|||
|
|
].join("\n");
|
|||
|
|
}
|
|||
|
|
default:
|
|||
|
|
return `❌ 未知操作: ${action}`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
//# sourceMappingURL=devServer.js.map
|