Files
qiming/qimingclaw/crates/agent-electron-client/scripts/tools/check-startup-ports.js

179 lines
6.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env node
/**
* 启动前端口检查脚本(聚合逻辑的 CLI 入口)
*
* 端口默认值与解析规则与 src/shared/startupPorts.ts 保持一致,请同步修改。
* 从 ~/.qimingclaw/qimingclaw.dbWindows: %USERPROFILE%\.qimingclaw\)读取配置。
* 端口占用检查:优先统一用 bash 执行 scripts/tools/check-port.shWindows 需先 npm run prepare:git 集成 Git Bash
* 无 bash 或脚本时回退到 Node 内联 netstat/lsof。
* 依赖Node读取 DB 需系统 sqlite3Windows 上多数未预装,无则用默认端口)。
*/
const path = require('path');
const fs = require('fs');
const { execSync, spawnSync } = require('child_process');
const { getProjectRoot } = require('../utils/project-paths');
const isWin = process.platform === 'win32';
const home = process.env.HOME || process.env.USERPROFILE;
const dbPath = path.join(home, '.qimingclaw', 'qimingclaw.db');
const projectRoot = getProjectRoot();
const checkPortShPath = path.join(__dirname, 'check-port.sh');
const winBashPath = path.join(projectRoot, 'resources', 'git', 'bin', 'bash.exe');
// 与 shared/constants + shared/startupPorts 保持一致
const DEFAULTS = {
agent: 60001,
fileServer: 60000,
mcp: 18099,
lanproxyLocal: 60002,
vite: 60173,
};
const LABELS = {
agent: 'Agent(ComputerServer)',
fileServer: 'FileServer',
mcp: 'MCP Proxy',
lanproxyLocal: 'Lanproxy',
vite: 'Vite',
};
function getSetting(dbPath, key) {
try {
// key 仅来自内部常量仍做引号转义以防将来扩展Windows 路径含反斜杠,双引号包裹路径即可
const safeKey = String(key).replace(/'/g, "''");
const quotedPath = dbPath.replace(/\\/g, '/'); // sqlite3 在 Windows 上通常也接受正斜杠
const out = execSync(`sqlite3 "${quotedPath}" "SELECT value FROM settings WHERE key='${safeKey}';"`, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
...(isWin && { shell: true }), // Windows 下若 sqlite3 为 .cmd 或路径含空格,需 shell
}).trim();
if (!out) return null;
try {
return JSON.parse(out);
} catch {
return out;
}
} catch {
return null;
}
}
function resolvePortsFromSettings(getSettingFn) {
const step1 = getSettingFn('step1_config');
const agent = (step1 && step1.agentPort != null) ? step1.agentPort : DEFAULTS.agent;
const fileServer = (step1 && step1.fileServerPort != null) ? step1.fileServerPort : DEFAULTS.fileServer;
const mcpRaw = getSettingFn('mcp_proxy_port');
let mcp = DEFAULTS.mcp;
if (typeof mcpRaw === 'number' && Number.isInteger(mcpRaw)) mcp = mcpRaw;
else if (typeof mcpRaw === 'string') { const n = parseInt(mcpRaw, 10); if (!Number.isNaN(n)) mcp = n; }
return {
agent,
fileServer,
mcp,
lanproxyLocal: DEFAULTS.lanproxyLocal,
vite: DEFAULTS.vite,
};
}
function getPortsToCheck(ports, includeVite) {
const list = [
{ name: 'agent', label: LABELS.agent, port: ports.agent },
{ name: 'fileServer', label: LABELS.fileServer, port: ports.fileServer },
{ name: 'mcp', label: LABELS.mcp, port: ports.mcp },
{ name: 'lanproxyLocal', label: LABELS.lanproxyLocal, port: ports.lanproxyLocal },
];
if (includeVite) list.push({ name: 'vite', label: LABELS.vite, port: ports.vite });
return list;
}
/** 优先用 bash 执行 scripts/tools/check-port.sh与 main 进程一致),否则回退 Node 内联逻辑 */
function isPortInUse(port) {
const bashPath = isWin ? (fs.existsSync(winBashPath) ? winBashPath : null) : 'bash';
const scriptContent = fs.existsSync(checkPortShPath)
? fs.readFileSync(checkPortShPath, 'utf8')
: null;
if (bashPath && scriptContent) {
const result = spawnSync(bashPath, ['-c', scriptContent, '_', String(port)], {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
});
if (result.status === 0 && result.stdout) {
const text = (result.stdout && String(result.stdout)).trim();
const pid = text && /^\d+$/.test(text) ? parseInt(text, 10) : undefined;
return { inUse: true, pid };
}
return { inUse: false };
}
// 回退:无 bash 或脚本时用 Node 内联 netstat/lsof
try {
if (isWin) {
const out = execSync(`netstat -ano 2>nul | findstr ":${port} "`, {
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'pipe'],
shell: true,
});
const text = (out && String(out)).trim();
const inUse = text.length > 0;
let pid;
if (inUse) {
const firstLine = text.split(/\r?\n/)[0] || '';
const lastCol = firstLine.split(/\s+/).filter(Boolean).pop();
if (lastCol && /^\d+$/.test(lastCol)) pid = parseInt(lastCol, 10);
}
return { inUse, pid };
}
const out = execSync(`lsof -t -i :${port} 2>/dev/null`, { encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] });
const text = (out && String(out)).trim();
const inUse = text.length > 0;
const pid = inUse && /^\d+$/.test(text) ? parseInt(text.split('\n')[0], 10) : undefined;
return { inUse, pid };
} catch {
return { inUse: false };
}
}
function main() {
const includeVite = process.argv.includes('--vite');
const getSettingFn = (key) => getSetting(dbPath, key);
let ports;
let source = '';
if (!fs.existsSync(dbPath)) {
ports = { ...DEFAULTS };
source = '未找到 DB使用默认端口';
} else {
try {
ports = resolvePortsFromSettings(getSettingFn);
source = `已从 ${dbPath} 读取配置`;
} catch (err) {
ports = { ...DEFAULTS };
source = '读取 DB 失败(如未安装 sqlite3使用默认端口';
}
}
console.log('端口来源:', source);
const list = getPortsToCheck(ports, includeVite);
console.log('检查端口:', list.map(({ label, port }) => `${label}=${port}`).join(', '));
console.log('');
let hasInUse = false;
for (const { label, port } of list) {
const { inUse, pid } = isPortInUse(port);
if (inUse) {
hasInUse = true;
console.log(` [占用] ${label} (${port}) ${pid ? `PID=${pid}` : ''}`);
} else {
console.log(` [空闲] ${label} (${port})`);
}
}
process.exit(hasInUse ? 1 : 0);
}
main();