Files
memind/scripts/dev.mjs
T
john 54de1dd8e3 Add local test domain setup (test.*.tkmind.cn).
DNS/TLS/proxy scripts and docs/local-dev.md replace localhost URLs for H5, memind_adm, Plaza, and Ops during local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-17 09:55:04 +08:00

308 lines
9.1 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)), '..');
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'));
const {
ADMIN_HOST,
allHostsConfigured,
DNS_PORT,
H5_HOST,
H5_PUBLIC_BASE,
OPS_HOST,
PLAZA_HOST,
PLAZA_PUBLIC_BASE,
publicUrl,
} = await import('./local-test-config.mjs');
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 adminPort = Number(process.env.ADMIN_PORT ?? 8082);
const portalUrl = `http://127.0.0.1:${portalPort}`;
const adminUrl = `http://127.0.0.1:${adminPort}`;
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 admin;
let plaza;
let localProxy;
let dnsServer;
let ops;
let vite;
let stopping = false;
function shutdown(code = 0) {
if (stopping) return;
stopping = true;
server?.kill('SIGTERM');
admin?.kill('SIGTERM');
plaza?.kill('SIGTERM');
localProxy?.kill('SIGTERM');
dnsServer?.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 = H5_PUBLIC_BASE;
const plazaEnv = {
PLAZA_API_PROXY: portalUrl,
PLAZA_API_BASE: portalUrl,
PLAZA_PUBLIC_BASE: PLAZA_PUBLIC_BASE,
NEXT_PUBLIC_API_BASE: PLAZA_PUBLIC_BASE,
NEXT_PUBLIC_SITE_BASE: PLAZA_PUBLIC_BASE,
NEXT_PUBLIC_MINDSPACE_BASE: mindSpacePublicBase,
NEXT_PUBLIC_PLAZA_BASE: '/plaza',
};
const viteEnv = {
VITE_PLAZA_BASE: PLAZA_PUBLIC_BASE,
VITE_MINDSPACE_BASE: mindSpacePublicBase,
};
const opsEnv = {
OPS_API_PROXY: portalUrl,
OPS_ADMIN_PROXY: adminUrl,
VITE_PLAZA_BASE: PLAZA_PUBLIC_BASE,
VITE_MINDSPACE_BASE: mindSpacePublicBase,
};
function ensurePlazaApp() {
if (!fs.existsSync(path.join(plazaDir, 'package.json'))) {
throw new Error(`未找到 Plaza Next.js${plazaDir}`);
}
}
function publicReachable(urlString) {
const url = new URL(urlString);
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 ensureLocalTestProxy() {
const h5Ready = await publicReachable(`${H5_PUBLIC_BASE}/`);
if (h5Ready) {
console.log(`==> 本地测试 HTTPS 入口已可用: ${H5_PUBLIC_BASE}`);
return;
}
console.log(`==> 尝试启动本地 HTTPS 代理 (${H5_PUBLIC_BASE})...`);
const proxyScript = path.join(root, 'scripts/local-test-proxy.mjs');
localProxy = spawn('node', [proxyScript], {
cwd: root,
env: process.env,
stdio: ['ignore', 'pipe', 'pipe'],
});
let proxyFailed = false;
localProxy.stdout?.on('data', (chunk) => process.stdout.write(`[local-proxy] ${chunk}`));
localProxy.stderr?.on('data', (chunk) => {
const text = String(chunk);
process.stderr.write(`[local-proxy] ${text}`);
if (/EACCES|EADDRINUSE|需要 root/.test(text)) proxyFailed = true;
});
localProxy.on('exit', (code) => {
if (code && code !== 0) proxyFailed = true;
});
for (let i = 0; i < 20; i += 1) {
if (proxyFailed) break;
if (await publicReachable(`${H5_PUBLIC_BASE}/`)) {
console.log(`==> 本地测试 HTTPS 入口已可用: ${H5_PUBLIC_BASE}`);
return;
}
await new Promise((resolve) => setTimeout(resolve, 250));
}
console.warn('');
console.warn('⚠ 本地 test 域名 HTTPS 入口暂不可用');
console.warn(' 请先:sudo pnpm setup:local-test');
console.warn(' 再另开终端:sudo pnpm dev:local-proxy');
console.warn(' 文档:docs/local-dev.md');
console.warn('');
}
console.log(`==> 清理端口 ${portalPort} / ${adminPort} / ${vitePort} / ${plazaPort} / ${opsPort} / ${DNS_PORT}...`);
freePort(portalPort);
freePort(adminPort);
freePort(vitePort);
freePort(plazaPort);
freePort(opsPort);
freePort(DNS_PORT);
await new Promise((resolve) => setTimeout(resolve, 500));
ensurePlazaApp();
if (!allHostsConfigured()) {
console.warn('');
console.warn('⚠ 本地 test 域名 DNS 未完整配置(Tailscale/Clash 可能劫持解析)');
console.warn(' 请执行:sudo pnpm setup:local-test');
console.warn(' 文档:docs/local-dev.md');
console.warn('');
}
console.log('==> 启动本地 test DNS...');
dnsServer = spawnChild('node', ['scripts/local-test-dns-server.mjs'], 'local-dns');
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('==> 启动 memind_adm (admin-server.mjs)...');
admin = spawnChild('node', ['admin-server.mjs'], 'admin');
await waitFor(adminUrl, async (url) => (await fetch(`${url}/healthz`)).ok, 'Admin');
console.log(`==> memind_adm 就绪 (内部 ${adminUrl}${publicUrl(ADMIN_HOST)})`);
console.log(`==> 启动 Plaza Next.js @ ${PLAZA_PUBLIC_BASE} (bind ${PLAZA_HOST})`);
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: `${PLAZA_HOST}:${plazaPort}` },
});
return res.ok || res.status === 307 || res.status === 308;
},
'Plaza',
80,
);
console.log(`==> Plaza 就绪: ${PLAZA_PUBLIC_BASE}/plaza`);
console.log(`==> 启动 Ops 后台 @ ${publicUrl(OPS_HOST)}/ops/`);
ops = spawnChild('npm', ['run', 'dev'], 'ops', opsDir, opsEnv);
await waitFor(
`http://127.0.0.1:${opsPort}`,
async (url) => (await fetch(`${url}/ops/`)).ok,
'Ops',
40,
);
console.log(`==> Ops 就绪: ${publicUrl(OPS_HOST)}/ops/`);
console.log(`==> 启动 Vite @ ${H5_PUBLIC_BASE}`);
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 就绪: ${H5_PUBLIC_BASE}`);
await ensureLocalTestProxy();
console.log('');
console.log('本地服务(请用 test 域名访问,见 docs/local-dev.md):');
console.log(` MindSpace UI ${H5_PUBLIC_BASE}/?preview=mindspace`);
console.log(` Plaza 广场 ${PLAZA_PUBLIC_BASE}/plaza`);
console.log(` Ops 审核后台 ${publicUrl(OPS_HOST)}/ops/`);
console.log(` memind_adm ${publicUrl(ADMIN_HOST)}`);
console.log('');
console.log('HTTPS 代理:sudo pnpm dev:local-proxy');
console.log('诊断:pnpm check:local-test');
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);
}