2026-07-28 02:12:46 +08:00
|
|
|
|
/**
|
|
|
|
|
|
* 桌面开发:APS_MODE=desktop → 数据落 ~/.aps,并拉起 API + Vite + Electron
|
|
|
|
|
|
* 无论从仓库根还是 apps/* 调用,都以本脚本所在仓库为 cwd。
|
|
|
|
|
|
*/
|
2026-08-26 00:25:46 +08:00
|
|
|
|
import { spawn, spawnSync } from 'node:child_process';
|
|
|
|
|
|
import net from 'node:net';
|
2026-07-28 02:12:46 +08:00
|
|
|
|
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');
|
2026-08-26 00:25:46 +08:00
|
|
|
|
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);
|
2026-07-28 02:12:46 +08:00
|
|
|
|
|
|
|
|
|
|
function run(label, command, args, env = {}) {
|
|
|
|
|
|
const child = spawn(command, args, {
|
|
|
|
|
|
cwd: repoRoot,
|
|
|
|
|
|
env: {
|
|
|
|
|
|
...process.env,
|
|
|
|
|
|
APS_MODE: 'desktop',
|
|
|
|
|
|
APS_HOME: home,
|
2026-07-29 23:22:40 +08:00
|
|
|
|
APS_AUTH_PROVIDER: process.env.APS_AUTH_PROVIDER || 'jms',
|
2026-07-28 02:12:46 +08:00
|
|
|
|
APS_LICENSE_PROVIDER: process.env.APS_LICENSE_PROVIDER || 'mock',
|
|
|
|
|
|
...env,
|
|
|
|
|
|
},
|
|
|
|
|
|
stdio: 'inherit',
|
|
|
|
|
|
shell: isWin,
|
|
|
|
|
|
});
|
|
|
|
|
|
child.on('exit', (code, signal) => {
|
2026-08-11 00:54:05 +08:00
|
|
|
|
console.log(`[${label}] exit code=${code} signal=${signal}`);
|
|
|
|
|
|
if (label === 'desktop' || (code !== null && code !== 0)) shutdown(code ?? 1);
|
2026-07-28 02:12:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
kids.push(child);
|
|
|
|
|
|
return child;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function shutdown(code = 0) {
|
|
|
|
|
|
for (const c of kids) {
|
2026-08-26 00:25:46 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-07-28 02:12:46 +08:00
|
|
|
|
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');
|
2026-08-11 00:54:05 +08:00
|
|
|
|
run('api', py, ['-m', 'uvicorn', 'server.main:app', '--reload', '--port', String(apiPort)]);
|
|
|
|
|
|
run('web', isWin ? 'npm.cmd' : 'npm', [
|
2026-08-26 00:25:46 +08:00
|
|
|
|
'run', 'dev', '--prefix', 'apps/web', '--', '--host', '127.0.0.1', '--port', String(uiPort), '--strictPort',
|
2026-08-11 00:54:05 +08:00
|
|
|
|
], {
|
|
|
|
|
|
VITE_API_TARGET: `http://127.0.0.1:${apiPort}`,
|
|
|
|
|
|
});
|
2026-07-28 02:12:46 +08:00
|
|
|
|
|
|
|
|
|
|
// 等 Vite 起来再开 Electron
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
run('desktop', isWin ? 'npm.cmd' : 'npm', ['run', 'dev', '--prefix', 'apps/desktop'], {
|
2026-08-11 00:54:05 +08:00
|
|
|
|
APS_UI_URL: `http://127.0.0.1:${uiPort}`,
|
|
|
|
|
|
APS_API_URL: `http://127.0.0.1:${apiPort}`,
|
2026-07-28 02:12:46 +08:00
|
|
|
|
});
|
|
|
|
|
|
}, 2500);
|
|
|
|
|
|
|
|
|
|
|
|
console.log(`\nAPS 桌面开发(cwd=${repoRoot}):用户目录 ${home}\n`);
|