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
|