Files
memind/scripts/dev.mjs
T
John 2e14873f2d Initial commit: Memind H5 portal with MindSpace, Plaza, and agent jobs.
Track application source and tests; exclude local env, user workspaces, and runtime data via .gitignore.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 15:04:43 -07:00

297 lines
9.0 KiB
JavaScript
Executable File
Raw 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.
#!/usr/bin/env node
import { spawn, execSync } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import https from 'node:https';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
const plazaDir = process.env.PLAZA_APP_DIR ?? path.join(root, '../tkmind_go/ui/plaza');
const opsDir = path.join(root, 'ops');
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 portalUrl = `http://127.0.0.1:${portalPort}`;
const plazaHost = process.env.PLAZA_LOCAL_HOST ?? 'plaza.tkmind.cn';
const plazaPublicPort = Number(process.env.PLAZA_PUBLIC_PORT ?? 443);
const plazaPublicScheme = String(process.env.PLAZA_PUBLIC_BASE ?? 'https://plaza.tkmind.cn').startsWith('http://')
? 'http'
: 'https';
const plazaPublicBase = (
process.env.PLAZA_PUBLIC_BASE ??
(plazaPublicPort === 80 || plazaPublicPort === 443
? `${plazaPublicScheme}://${plazaHost}`
: `${plazaPublicScheme}://${plazaHost}:${plazaPublicPort}`)
).replace(/\/$/, '');
const plazaUrl = plazaPublicBase;
const opsUrl = `http://127.0.0.1:${opsPort}`;
function loadEnvFile(filePath) {
if (!fs.existsSync(filePath)) return;
for (const line of fs.readFileSync(filePath, 'utf8').split('\n')) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith('#')) continue;
const eq = trimmed.indexOf('=');
if (eq < 0) continue;
const key = trimmed.slice(0, eq).trim();
const value = trimmed.slice(eq + 1).trim();
if (!process.env[key]) process.env[key] = value;
}
}
loadEnvFile(path.join(root, '../../.env.local'));
loadEnvFile(path.join(root, '.env'));
function freePort(port) {
try {
execSync(`lsof -ti TCP:${port} -sTCP:LISTEN | xargs kill -9`, { stdio: 'ignore' });
} catch {
// port already free
}
}
function spawnChild(command, args, label, cwd = root, extraEnv = {}) {
const child = spawn(command, args, {
cwd,
env: { ...process.env, ...extraEnv },
stdio: 'inherit',
});
child.on('exit', (code, signal) => {
if (signal) return;
if (code && code !== 0) {
console.error(`[${label}] exited with code ${code}`);
shutdown(code ?? 1);
}
});
return child;
}
let server;
let plaza;
let plazaProxy;
let ops;
let vite;
let stopping = false;
function shutdown(code = 0) {
if (stopping) return;
stopping = true;
server?.kill('SIGTERM');
plaza?.kill('SIGTERM');
plazaProxy?.kill('SIGTERM');
ops?.kill('SIGTERM');
vite?.kill('SIGTERM');
setTimeout(() => process.exit(code), 300);
}
process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));
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://localhost:${vitePort}`
).replace(/\/$/, '');
const plazaEnv = {
PLAZA_API_PROXY: portalUrl,
PLAZA_API_BASE: portalUrl,
PLAZA_PUBLIC_BASE: plazaPublicBase,
NEXT_PUBLIC_API_BASE: plazaPublicBase,
NEXT_PUBLIC_SITE_BASE: plazaPublicBase,
NEXT_PUBLIC_MINDSPACE_BASE: mindSpacePublicBase,
NEXT_PUBLIC_PLAZA_BASE: '/plaza',
};
const viteEnv = {
VITE_PLAZA_BASE: plazaPublicBase,
VITE_MINDSPACE_BASE: mindSpacePublicBase,
};
const opsEnv = {
OPS_API_PROXY: portalUrl,
VITE_PLAZA_BASE: plazaPublicBase,
VITE_MINDSPACE_BASE: mindSpacePublicBase,
};
function ensurePlazaApp() {
if (!fs.existsSync(path.join(plazaDir, 'package.json'))) {
throw new Error(`未找到 Plaza Next.js${plazaDir}`);
}
}
function plazaPublicReachable() {
const url = new URL(`${plazaPublicBase}/plaza`);
const isHttps = url.protocol === 'https:';
const port = url.port || (isHttps ? 443 : 80);
return new Promise((resolve) => {
const transport = isHttps ? https : http;
const req = transport.request(
{
hostname: url.hostname,
port,
path: `${url.pathname}${url.search}`,
method: 'GET',
rejectUnauthorized: false,
},
(res) => {
resolve(res.statusCode === 200 || res.statusCode === 307 || res.statusCode === 308);
},
);
req.on('error', () => resolve(false));
req.setTimeout(3000, () => {
req.destroy();
resolve(false);
});
req.end();
});
}
async function ensurePlazaPublicProxy() {
if (await plazaPublicReachable()) {
console.log(`==> Plaza 公开入口已可用: ${plazaPublicBase}/plaza`);
return;
}
console.log(`==> 尝试启动 Plaza 80 端口代理 (${plazaPublicBase})...`);
const proxyScript = path.join(root, 'scripts/plaza-local-proxy.mjs');
plazaProxy = spawn('node', [proxyScript], {
cwd: root,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let proxyFailed = false;
plazaProxy.stdout?.on('data', (chunk) => process.stdout.write(`[plaza-proxy] ${chunk}`));
plazaProxy.stderr?.on('data', (chunk) => {
const text = String(chunk);
process.stderr.write(`[plaza-proxy] ${text}`);
if (/EACCES|EADDRINUSE|需要 root/.test(text)) proxyFailed = true;
});
plazaProxy.on('exit', (code) => {
if (code && code !== 0) proxyFailed = true;
});
for (let i = 0; i < 20; i += 1) {
if (proxyFailed) break;
if (await plazaPublicReachable()) {
console.log(`==> Plaza 公开入口已可用: ${plazaPublicBase}/plaza`);
return;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
console.warn('');
console.warn(`⚠ 无法在本进程绑定 ${plazaPublicPort} 端口,${plazaPublicBase} 暂不可用`);
console.warn(' 请先:sudo pnpm setup:plaza-local');
console.warn(' 再另开终端:sudo pnpm dev:plaza-proxy');
console.warn('');
}
function plazaHostConfigured(host) {
try {
const hosts = fs.readFileSync('/etc/hosts', 'utf8');
return new RegExp(`^\\s*127\\.0\\.0\\.1\\s+${host.replace('.', '\\.')}(\\s|$)`, 'm').test(hosts);
} catch {
return false;
}
}
console.log(`==> 清理端口 ${portalPort} / ${vitePort} / ${plazaPort} / ${opsPort}...`);
freePort(portalPort);
freePort(vitePort);
freePort(plazaPort);
freePort(opsPort);
await new Promise((resolve) => setTimeout(resolve, 500));
ensurePlazaApp();
if (!plazaHostConfigured(plazaHost)) {
console.warn('');
console.warn(`${plazaHost} 尚未写入 /etc/hosts,浏览器无法打开 ${plazaPublicBase}`);
console.warn(' 请执行:sudo pnpm setup:plaza-dns');
console.warn('');
}
console.log('==> 启动 portal (server.mjs)...');
server = spawnChild('node', ['server.mjs'], 'portal');
try {
await waitFor(portalUrl, async (url) => (await fetch(`${url}/auth/status`)).ok, 'Portal');
console.log(`==> Portal 就绪: ${portalUrl}`);
console.log(`==> 启动 Plaza Next.js @ ${plazaPublicBase} (bind ${plazaHost})`);
plaza = spawnChild(
'npm',
['run', 'dev', '--', '-H', '0.0.0.0', '-p', String(plazaPort)],
'plaza',
plazaDir,
plazaEnv,
);
await waitFor(
`http://127.0.0.1:${plazaPort}`,
async (url) => {
const res = await fetch(`${url}/plaza`, {
headers: { Host: `${plazaHost}:${plazaPort}` },
});
return res.ok || res.status === 307 || res.status === 308;
},
'Plaza',
80,
);
console.log(`==> Plaza 内部就绪: http://127.0.0.1:${plazaPort}/plaza`);
await ensurePlazaPublicProxy();
console.log(`==> 启动 Ops 后台 @ ${opsUrl}/ops/`);
ops = spawnChild('npm', ['run', 'dev'], 'ops', opsDir, opsEnv);
await waitFor(
`http://localhost:${opsPort}`,
async (url) => (await fetch(`${url}/ops/`)).ok,
'Ops',
40,
);
console.log(`==> Ops 就绪: http://localhost:${opsPort}/ops/`);
console.log(`==> 启动 Vite @ http://localhost:${vitePort}`);
vite = spawnChild('npx', ['vite'], 'vite', root, viteEnv);
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',
);
console.log(`==> Vite 就绪: http://localhost:${vitePort}`);
console.log('');
console.log('本地服务:');
console.log(` MindSpace UI http://localhost:${vitePort}/?preview=mindspace`);
console.log(` Plaza 广场 ${plazaPublicBase}/plaza`);
console.log(` Plaza 内部 http://127.0.0.1:${plazaPort}/plaza`);
console.log(` (setup) sudo pnpm setup:plaza-local && sudo pnpm dev:plaza-proxy`);
console.log(` Ops 审核后台 http://localhost:${opsPort}/ops/`);
console.log(` API / Portal ${portalUrl}`);
console.log('');
console.log('首次使用 Ops:先登录 MindSpace,再执行 node scripts/grant-ops-role.mjs admin ops_admin');
} catch (err) {
console.error(err instanceof Error ? err.message : err);
shutdown(1);
}