aps-agent/scripts/dev-desktop.mjs

110 lines
3.7 KiB
JavaScript
Raw Permalink 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.

/**
* 桌面开发: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`);