fde6503bdf
Memind CI / Test, build, and release guards (push) Has been cancelled
Enable optional SEO/GEO injection and discovery routes for confirmed public pages while keeping private pages noindex. Add premium page template skills, portal catalog API, template shop UI, and Baidu push gated by memind_adm config. Co-authored-by: Cursor <cursoragent@cursor.com>
220 lines
6.1 KiB
JavaScript
220 lines
6.1 KiB
JavaScript
#!/usr/bin/env node
|
||
/**
|
||
* LaunchAgent / 后台常驻版 dev 栈:portal + admin + ops + vite。
|
||
* 单个子进程异常退出时会自动重启;启动阶段失败则整体退出,由 launchd KeepAlive 重试。
|
||
*/
|
||
import { spawn, execSync } from 'node:child_process';
|
||
import fs from 'node:fs';
|
||
import os from 'node:os';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
import {
|
||
applyMemindRuntimeProfile,
|
||
describeMemindRuntimeProfile,
|
||
loadMemindEnvFiles,
|
||
} from './memind-runtime-profile.mjs';
|
||
|
||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const opsDir = path.join(root, 'ops');
|
||
const nodeBin = process.execPath;
|
||
|
||
const logFile =
|
||
process.env.MEMIND_DEV_LOG ?? path.join(os.homedir(), 'Library/Logs/memind-dev.log');
|
||
|
||
function log(message) {
|
||
const line = `[${new Date().toISOString()}] ${message}\n`;
|
||
try {
|
||
fs.appendFileSync(logFile, line);
|
||
} catch {
|
||
process.stdout.write(line);
|
||
}
|
||
}
|
||
|
||
// Load .env before reading ports — otherwise ADMIN_PORT defaults to 8082 and
|
||
// split-service MindSpace (also on 8082) gets killed by freePort().
|
||
loadMemindEnvFiles(root);
|
||
applyMemindRuntimeProfile({ rootDir: root });
|
||
log(`Runtime profile: ${describeMemindRuntimeProfile()}`);
|
||
|
||
const portalPort = Number(process.env.H5_PORT ?? 8081);
|
||
const vitePort = Number(process.env.VITE_PORT ?? 5173);
|
||
const plazaPort = Number(process.env.PLAZA_PORT ?? 3001);
|
||
const opsPort = Number(process.env.OPS_PORT ?? 3002);
|
||
const adminPort = Number(process.env.ADMIN_PORT ?? 8082);
|
||
const portalUrl = `http://127.0.0.1:${portalPort}`;
|
||
const adminUrl = `http://127.0.0.1:${adminPort}`;
|
||
const plazaPublicBase = (
|
||
process.env.PLAZA_PUBLIC_BASE ?? `http://127.0.0.1:${plazaPort}`
|
||
).replace(/\/$/, '');
|
||
|
||
function freePort(port) {
|
||
try {
|
||
execSync(`lsof -ti TCP:${port} -sTCP:LISTEN | xargs kill -9`, { stdio: 'ignore' });
|
||
} catch {
|
||
// port already free
|
||
}
|
||
}
|
||
|
||
let fullyStarted = false;
|
||
let stopping = false;
|
||
const children = { server: null, admin: null, ops: null, vite: null };
|
||
|
||
function shutdown(code = 0) {
|
||
if (stopping) return;
|
||
stopping = true;
|
||
for (const child of Object.values(children)) {
|
||
child?.kill('SIGTERM');
|
||
}
|
||
setTimeout(() => process.exit(code), 300);
|
||
}
|
||
|
||
process.on('SIGINT', () => shutdown(0));
|
||
process.on('SIGTERM', () => shutdown(0));
|
||
|
||
function attachExitHandler(label, child, respawn) {
|
||
child.on('exit', (code, signal) => {
|
||
if (stopping || signal === 'SIGTERM') return;
|
||
if (!code || code === 0) return;
|
||
if (!fullyStarted) {
|
||
log(`[${label}] 启动阶段退出 code=${code}`);
|
||
shutdown(code);
|
||
return;
|
||
}
|
||
log(`[${label}] 异常退出 code=${code},3 秒后重启...`);
|
||
setTimeout(() => {
|
||
if (stopping) return;
|
||
respawn();
|
||
}, 3000);
|
||
});
|
||
}
|
||
|
||
function spawnPortal() {
|
||
const child = spawn(nodeBin, [path.join(root, 'server.mjs')], {
|
||
cwd: root,
|
||
env: process.env,
|
||
stdio: 'inherit',
|
||
});
|
||
children.server = child;
|
||
attachExitHandler('portal', child, spawnPortal);
|
||
return child;
|
||
}
|
||
|
||
function spawnAdmin() {
|
||
const child = spawn(nodeBin, [path.join(root, 'admin-server.mjs')], {
|
||
cwd: root,
|
||
env: process.env,
|
||
stdio: 'inherit',
|
||
});
|
||
children.admin = child;
|
||
attachExitHandler('admin', child, spawnAdmin);
|
||
return child;
|
||
}
|
||
|
||
function spawnOps() {
|
||
const child = spawn('npm', ['run', 'dev'], {
|
||
cwd: opsDir,
|
||
env: { ...process.env, ...opsEnv },
|
||
stdio: 'inherit',
|
||
});
|
||
children.ops = child;
|
||
attachExitHandler('ops', child, spawnOps);
|
||
return child;
|
||
}
|
||
|
||
function spawnVite() {
|
||
const child = spawn('npx', ['vite'], {
|
||
cwd: root,
|
||
env: { ...process.env, ...viteEnv },
|
||
stdio: 'inherit',
|
||
});
|
||
children.vite = child;
|
||
attachExitHandler('vite', child, spawnVite);
|
||
return child;
|
||
}
|
||
|
||
async function waitFor(url, check, label, retries = 60) {
|
||
for (let i = 0; i < retries; i += 1) {
|
||
try {
|
||
if (await check(url)) return;
|
||
} catch {
|
||
// still starting
|
||
}
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
}
|
||
throw new Error(`${label} 未在 ${url} 启动`);
|
||
}
|
||
|
||
const mindSpacePublicBase = (
|
||
process.env.H5_PUBLIC_BASE_URL ?? `http://127.0.0.1:${vitePort}`
|
||
).replace(/\/$/, '');
|
||
|
||
const viteEnv = {
|
||
VITE_PLAZA_BASE: plazaPublicBase,
|
||
VITE_MINDSPACE_BASE: mindSpacePublicBase,
|
||
};
|
||
|
||
const opsEnv = {
|
||
OPS_API_PROXY: portalUrl,
|
||
OPS_ADMIN_PROXY: adminUrl,
|
||
VITE_PLAZA_BASE: plazaPublicBase,
|
||
VITE_MINDSPACE_BASE: mindSpacePublicBase,
|
||
};
|
||
|
||
log(`清理端口 ${portalPort} / ${adminPort} / ${vitePort} / ${opsPort}...`);
|
||
freePort(portalPort);
|
||
freePort(adminPort);
|
||
freePort(vitePort);
|
||
freePort(opsPort);
|
||
await new Promise((resolve) => setTimeout(resolve, 500));
|
||
|
||
log('启动 portal (server.mjs)...');
|
||
spawnPortal();
|
||
|
||
try {
|
||
await waitFor(portalUrl, async (url) => (await fetch(`${url}/auth/status`)).ok, 'Portal');
|
||
log(`Portal 就绪: ${portalUrl}`);
|
||
|
||
log('启动 memind_adm (admin-server.mjs)...');
|
||
spawnAdmin();
|
||
await waitFor(adminUrl, async (url) => (await fetch(`${url}/healthz`)).ok, 'Admin');
|
||
log(`memind_adm 就绪: ${adminUrl}`);
|
||
|
||
log(`启动 Ops 后台 @ http://127.0.0.1:${opsPort}/ops/`);
|
||
spawnOps();
|
||
await waitFor(
|
||
`http://127.0.0.1:${opsPort}`,
|
||
async (url) => (await fetch(`${url}/ops/`)).ok,
|
||
'Ops',
|
||
40,
|
||
);
|
||
log(`Ops 就绪: http://127.0.0.1:${opsPort}/ops/`);
|
||
|
||
log(`启动 Vite @ http://127.0.0.1:${vitePort}`);
|
||
spawnVite();
|
||
await waitFor(
|
||
`http://127.0.0.1:${vitePort}`,
|
||
async (url) => {
|
||
const [rootRes, mainRes] = await Promise.all([
|
||
fetch(`${url}/`),
|
||
fetch(`${url}/src/main.tsx`),
|
||
]);
|
||
return rootRes.ok && mainRes.ok;
|
||
},
|
||
'Vite',
|
||
);
|
||
log(`Vite 就绪: http://127.0.0.1:${vitePort}`);
|
||
|
||
fullyStarted = true;
|
||
log('');
|
||
log('本地服务(LaunchAgent 常驻):');
|
||
log(` MindSpace UI http://127.0.0.1:${vitePort}/?preview=mindspace`);
|
||
log(` Ops 审核后台 http://127.0.0.1:${opsPort}/ops/`);
|
||
log(` API / Portal ${portalUrl}`);
|
||
log(` memind_adm ${adminUrl}`);
|
||
log('');
|
||
log('Plaza 未包含在此 Agent 内;需要时请单独运行 pnpm dev:plaza');
|
||
} catch (err) {
|
||
log(err instanceof Error ? err.message : String(err));
|
||
shutdown(1);
|
||
}
|