/** * 桌面开发:APS_MODE=desktop → 数据落 ~/.aps,并拉起 API + Vite + Electron * 无论从仓库根还是 apps/* 调用,都以本脚本所在仓库为 cwd。 */ import { spawn, spawnSync } from 'node:child_process'; import crypto from 'node:crypto'; import net from 'node:net'; import os from 'node:os'; import path from 'node:path'; import process from 'node:process'; import { fileURLToPath } from 'node:url'; const isWin = process.platform === 'win32'; const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const kids = []; const home = process.env.APS_HOME || path.join(os.homedir(), '.aps'); const mockLicenseSecret = process.env.APS_MOCK_LICENSE_SECRET || crypto.randomBytes(32).toString('hex'); const mockLicenseTenant = process.env.APS_MOCK_LICENSE_TENANT_UUID || crypto.createHash('sha256').update(path.resolve(home)).digest('hex').slice(0, 32); const preferredApiPort = Number.parseInt(process.env.APS_PORT || '8000', 10); const preferredUiPort = Number.parseInt(process.env.APS_UI_PORT || '5173', 10); function canBindLoopback(port) { return new Promise((resolve) => { const probe = net.createServer(); probe.unref(); probe.once('error', () => resolve(false)); probe.listen({ host: '127.0.0.1', port, exclusive: true }, () => { probe.close(() => resolve(true)); }); }); } async function pickPort(label, preferred) { const maxPort = preferred + 9; for (let port = preferred; port <= maxPort; port += 1) { if (await canBindLoopback(port)) { if (port !== preferred) { console.log(`[dev] ${label} 端口 ${preferred} 已被占用,自动使用 ${port}`); } return port; } } throw new Error(`[dev] 未找到可用的 ${label} 端口(${preferred}-${maxPort} 均被占用)`); } const apiPort = await pickPort('API', preferredApiPort); const uiPort = await pickPort('UI', preferredUiPort); function run(label, command, args, env = {}) { const child = spawn(command, args, { cwd: repoRoot, env: { ...process.env, APS_MODE: 'desktop', APS_HOME: home, APS_AUTH_PROVIDER: process.env.APS_AUTH_PROVIDER || 'jms', APS_LICENSE_PROVIDER: process.env.APS_LICENSE_PROVIDER || 'mock', APS_MOCK_LICENSE_SECRET: mockLicenseSecret, APS_MOCK_LICENSE_TENANT_UUID: mockLicenseTenant, ...env, }, stdio: 'inherit', shell: isWin, }); child.on('exit', (code, signal) => { console.log(`[${label}] exit code=${code} signal=${signal}`); if (label === 'desktop' || (code !== null && code !== 0)) shutdown(code ?? 1); }); kids.push(child); return child; } function shutdown(code = 0) { for (const c of kids) { if (!Number.isInteger(c.pid) || c.pid <= 0) continue; if (isWin) { spawnSync('taskkill.exe', ['/pid', String(c.pid), '/T', '/F'], { stdio: 'ignore', windowsHide: true, }); continue; } try { c.kill(); } catch { /* ignore */ } } process.exit(code); } process.on('SIGINT', () => shutdown(0)); process.on('SIGTERM', () => shutdown(0)); const py = process.env.PYTHON || (isWin ? 'python' : 'python3'); run('api', py, ['-m', 'uvicorn', 'server.main:app', '--reload', '--port', String(apiPort)]); run('web', isWin ? 'npm.cmd' : 'npm', [ 'run', 'dev', '--prefix', 'apps/web', '--', '--host', '127.0.0.1', '--port', String(uiPort), '--strictPort', ], { VITE_API_TARGET: `http://127.0.0.1:${apiPort}`, }); // 等 Vite 起来再开 Electron setTimeout(() => { run('desktop', isWin ? 'npm.cmd' : 'npm', ['run', 'dev', '--prefix', 'apps/desktop'], { APS_UI_URL: `http://127.0.0.1:${uiPort}`, APS_API_URL: `http://127.0.0.1:${apiPort}`, }); }, 2500); console.log(`\nAPS 桌面开发(cwd=${repoRoot}):用户目录 ${home}\n`);