#!/usr/bin/env node const path = require('path'); const { spawn } = require('child_process'); const { pathToFileURL } = require('url'); const { applyWorkspaceContext, workspaceIdentity } = require('../mcp/src/core/runtime-context'); const { buildStartupSummary } = require('../mcp/src/core/startup-summary'); function parseArgs(argv = process.argv.slice(2)) { const portIndex = argv.indexOf('--port'); const port = portIndex >= 0 ? Number(argv[portIndex + 1]) : Number(process.env.QIWEI_DASHBOARD_PORT || 4320); if (!Number.isInteger(port) || port < 1 || port > 65535) throw new Error('端口无效,请使用 --port 4320 这类有效端口'); return { port, openBrowser: !argv.includes('--no-open') }; } async function requestJson(url, timeoutMs = 15000) { const response = await fetch(url, { signal: AbortSignal.timeout(timeoutMs) }); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json(); } async function inspectRunningServer(baseUrl, expectedWorkspaceId) { try { const health = await requestJson(`${baseUrl}/api/health`, 2500); const actualWorkspaceId = health.data?.workspaceId || ''; if (actualWorkspaceId && actualWorkspaceId !== expectedWorkspaceId) { return { running: false, occupied: true, actualWorkspaceId }; } return { running: health.status === 'ok', occupied: false, health }; } catch { return { running: false, occupied: false }; } } function openUrl(url) { let command; let args; if (process.platform === 'win32') { command = 'cmd.exe'; args = ['/d', '/s', '/c', 'start', '', url]; } else if (process.platform === 'darwin') { command = 'open'; args = [url]; } else { command = 'xdg-open'; args = [url]; } const child = spawn(command, args, { detached: true, stdio: 'ignore', windowsHide: true }); child.unref(); } const delay = ms => new Promise(resolve => setTimeout(resolve, ms)); function runtimeIsRunning(state, isAlive) { return state?.status === 'running' && isAlive(state.pid); } async function ensureCallbackRuntime({ workspaceRoot, timeoutMs = 15000 } = {}) { const stateModuleUrl = pathToFileURL(path.join( __dirname, '..', 'runtime', 'callback-service', 'src', 'runtime-state.mjs', )).href; const { isProcessAlive, readRuntimeState } = await import(stateModuleUrl); let state = readRuntimeState(); if (runtimeIsRunning(state, isProcessAlive)) { return { ...state, reused: true }; } const deadline = Date.now() + timeoutMs; while (state.pid && isProcessAlive(state.pid) && state.status !== 'stopped' && Date.now() < deadline) { await delay(100); state = readRuntimeState(); if (runtimeIsRunning(state, isProcessAlive)) return { ...state, reused: true }; } if (state.pid && isProcessAlive(state.pid) && state.status !== 'stopped') { throw new Error(`Qiwei runtime pid=${state.pid} is still ${state.status || 'unknown'}.`); } const runtimeScript = path.join(__dirname, 'start-callback-runtime.mjs'); const child = spawn(process.execPath, [runtimeScript, 'start', '--no-dashboard'], { cwd: workspaceRoot, env: { ...process.env, QIWEI_WORKSPACE_ROOT: workspaceRoot }, detached: true, stdio: 'ignore', windowsHide: true, }); child.unref(); while (Date.now() < deadline) { await delay(100); state = readRuntimeState(); if (runtimeIsRunning(state, isProcessAlive)) { return { ...state, reused: false }; } if (state.pid === child.pid && state.status === 'stopped') { throw new Error(`Qiwei runtime stopped during startup: ${state.stopReason || 'unknown reason'}`); } } throw new Error(`Qiwei runtime did not become ready within ${timeoutMs}ms.`); } function printStartupSummary(baseUrl, summary, workspaceRoot, alreadyRunning) { process.stdout.write([ '', `企微工作台${alreadyRunning ? '已在运行' : '启动成功'}:${baseUrl}/#agent`, `项目目录:${workspaceRoot}`, `产品模式:${summary.product?.label || '个人版'} · ${summary.product?.collectionLabel || '本地主动监听'}`, '', '启动检查:', ...summary.checks.map(item => ` ${item.ready ? '[完成]' : '[待处理]'} ${item.label}${item.ready ? '' : ` — ${item.action}`}`), '', `下一步:${summary.nextAction}`, '', ].join('\n')); } async function main() { const options = parseArgs(); process.env.QIWEI_DASHBOARD_PORT = String(options.port); const context = applyWorkspaceContext({ packageRoot: path.resolve(__dirname, '..') }); const expectedWorkspaceId = workspaceIdentity(context.workspaceRoot); const baseUrl = `http://127.0.0.1:${options.port}`; const current = await inspectRunningServer(baseUrl, expectedWorkspaceId); if (current.occupied) { throw new Error(`端口 ${options.port} 正被另一个企微项目使用。请关闭旧工作台,或使用 npm run preview -- --port ${options.port + 1}`); } let server = null; if (!current.running) { const { startServer } = require('../mcp/src/dashboard/server'); ({ server } = await startServer(options.port)); } try { const runtime = await ensureCallbackRuntime({ workspaceRoot: context.workspaceRoot }); process.stdout.write(`Qiwei runtime ${runtime.reused ? 'reused' : 'started'} independently (pid=${runtime.pid}).\n`); } catch (error) { process.stderr.write(`Qiwei runtime startup is pending: ${error.message}\n`); } let status = {}; let agent = {}; try { [status, agent] = await Promise.all([ requestJson(`${baseUrl}/api/status`), requestJson(`${baseUrl}/api/agent/status`), ]); } catch (error) { process.stderr.write(`工作台已启动,但启动状态读取失败:${error.message}\n`); } const summary = buildStartupSummary(status, agent); printStartupSummary(baseUrl, summary, context.workspaceRoot, current.running); if (options.openBrowser) openUrl(`${baseUrl}/#agent`); if (current.running && !server) return; if (server) { let shuttingDown = false; const shutdown = signal => { if (shuttingDown) return; shuttingDown = true; process.stdout.write(`Closing dashboard after ${signal}; callback runtime remains active.\n`); server.close(() => process.exit(0)); setTimeout(() => process.exit(0), 3000).unref(); }; process.once('SIGINT', () => shutdown('SIGINT')); process.once('SIGTERM', () => shutdown('SIGTERM')); } } if (require.main === module) { main().catch(error => { process.stderr.write(`启动预览失败:${error.message}\n`); process.exit(1); }); } module.exports = { parseArgs, inspectRunningServer, runtimeIsRunning, ensureCallbackRuntime, };