Files
memind/scripts/dev-plaza.mjs
T
John 6ee6fd64dd Add MindSpace page live edit, chat skills, and H5 deploy tooling.
Introduce page edit sessions with draft preview and patch API, chat skill picker, user memory profile, h5ApiBase resolution, voice WAV transport, and scripts for 105/g2 deployment and Plaza local dev.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-15 22:09:38 -07:00

201 lines
5.9 KiB
JavaScript
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
/**
* Standalone Plaza dev: Portal API + Next.js + optional :80 reverse proxy.
*
* Usage:
* pnpm dev:plaza
* sudo pnpm dev:plaza-proxy # separate terminal, maps plaza.tkmind.cn:80 -> :3001
*/
import { spawn, execSync } from 'node:child_process';
import fs from 'node:fs';
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 portalPort = Number(process.env.H5_PORT ?? 8081);
const plazaPort = Number(process.env.PLAZA_PORT ?? 3001);
const publicPort = Number(process.env.PLAZA_PUBLIC_PORT ?? 443);
const plazaHost = process.env.PLAZA_LOCAL_HOST ?? 'plaza.tkmind.cn';
const plazaPublicScheme = String(process.env.PLAZA_PUBLIC_BASE ?? 'https://plaza.tkmind.cn').startsWith('http://')
? 'http'
: 'https';
const plazaPublicBase = (
process.env.PLAZA_PUBLIC_BASE ??
(publicPort === 80 || publicPort === 443
? `${plazaPublicScheme}://${plazaHost}`
: `${plazaPublicScheme}://${plazaHost}:${publicPort}`)
).replace(/\/$/, '');
const portalUrl = `http://127.0.0.1:${portalPort}`;
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 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;
}
}
function freePort(port) {
try {
execSync(`lsof -ti TCP:${port} -sTCP:LISTEN | xargs kill -9`, { stdio: 'ignore' });
} catch {
// already free
}
}
async function portOpen(port) {
try {
const res = await fetch(`http://127.0.0.1:${port}/auth/status`);
return res.ok;
} catch {
return false;
}
}
async function waitFor(check, label, retries = 80) {
for (let i = 0; i < retries; i += 1) {
try {
if (await check()) return;
} catch {
// still starting
}
await new Promise((resolve) => setTimeout(resolve, 500));
}
throw new Error(`${label} 启动超时`);
}
let portal;
let plaza;
let dnsServer;
let stopping = false;
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 || stopping) return;
if (code && code !== 0) {
console.error(`[${label}] exited with code ${code}`);
shutdown(code ?? 1);
}
});
return child;
}
function shutdown(code = 0) {
if (stopping) return;
stopping = true;
portal?.kill('SIGTERM');
plaza?.kill('SIGTERM');
dnsServer?.kill('SIGTERM');
setTimeout(() => process.exit(code), 300);
}
process.on('SIGINT', () => shutdown(0));
process.on('SIGTERM', () => shutdown(0));
if (!fs.existsSync(path.join(plazaDir, 'package.json'))) {
console.error(`未找到 Plaza Next.js${plazaDir}`);
process.exit(1);
}
function plazaResolverConfigured(host) {
try {
const body = fs.readFileSync(`/etc/resolver/${host}`, 'utf8');
return body.includes('tkmind-plaza-local');
} catch {
return false;
}
}
if (!plazaHostConfigured(plazaHost) || !plazaResolverConfigured(plazaHost)) {
console.warn('');
console.warn(`${plazaHost} 本地 DNS 未完整配置(Tailscale/Clash 会劫持解析)`);
console.warn(' 请执行:sudo pnpm setup:plaza-dns');
console.warn('');
}
const mindSpacePublicBase = (
process.env.H5_PUBLIC_BASE_URL ?? `http://localhost:${portalPort}`
).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',
};
console.log(`==> Plaza 独立开发模式`);
console.log(` 公开地址 ${plazaPublicBase}/plaza`);
console.log(` 内部端口 ${plazaPort}`);
freePort(plazaPort);
freePort(Number(process.env.PLAZA_LOCAL_DNS_PORT ?? 5533));
try {
console.log('==> 启动 Plaza 本地 DNS(覆盖 Tailscale fake-ip');
dnsServer = spawnChild('node', ['scripts/plaza-local-dns-server.mjs'], 'plaza-dns');
if (!(await portOpen(portalPort))) {
console.log(`==> 启动 Portal @ ${portalUrl}`);
portal = spawnChild('node', ['server.mjs'], 'portal');
await waitFor(() => portOpen(portalPort), 'Portal');
} else {
console.log(`==> Portal 已在运行: ${portalUrl}`);
}
console.log(`==> 启动 Plaza Next.js (内部 :${plazaPort})`);
plaza = spawnChild(
'npm',
['run', 'dev', '--', '-H', '0.0.0.0', '-p', String(plazaPort)],
'plaza',
plazaDir,
plazaEnv,
);
await waitFor(async () => {
const res = await fetch(`http://127.0.0.1:${plazaPort}/plaza`, {
headers: { Host: `${plazaHost}:${plazaPort}` },
});
return res.ok || res.status === 307 || res.status === 308;
}, 'Plaza Next.js');
console.log('');
console.log('Plaza 已就绪(内部):');
console.log(` http://127.0.0.1:${plazaPort}/plaza`);
console.log('');
console.log('公开访问(需 80 端口代理):');
console.log(` ${plazaPublicBase}/plaza`);
console.log('');
console.log('若浏览器还打不开 plaza.tkmind.cn,另开终端执行:');
console.log(' sudo pnpm dev:plaza-proxy');
} catch (err) {
console.error(err instanceof Error ? err.message : err);
shutdown(1);
}