Split back-office into standalone memind_adm service

Carve the platform super-admin API (/admin-api) and plaza operations console
API (/api/ops/v1) out of the public server.mjs into their own process, so the
user-facing fleet can no longer be taken down or scaled by back-office traffic.

Architecture (split-ready, single process for now):
- admin-routes.mjs: createAdminApi / createOpsApi route factories (DI, single
  source of truth; the route logic moved verbatim out of server.mjs).
- admin-bootstrap.mjs: lean service container (user-auth, LLM providers, minimal
  plaza graph) with no user-facing daemons.
- admin-server.mjs: standalone entry with a console registry. ADMIN_CONSOLES
  selects which consoles a process mounts (default both), so splitting into two
  processes later is a config change, not a code change.
- admin-guard.mjs (+ tests): per-console host / IP-CIDR allowlists, letting the
  super-admin surface be locked down harder than moderation.

server.mjs no longer serves or mounts either surface. user-auth.mjs gains
pagination on listUsers/listUsageRecords/listBillingLedger to back the admin
dashboards. dev.mjs launches memind_adm; the ops SPA proxies /api to it while
/auth stays on the portal. Sessions are read from the shared cookie
(H5_COOKIE_DOMAIN=.tkmind.cn); login stays on the main domain.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
john
2026-06-16 20:55:23 +08:00
parent befeedfd37
commit b5c600ad81
12 changed files with 1051 additions and 502 deletions
+14 -1
View File
@@ -13,7 +13,9 @@ 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 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://')
@@ -69,6 +71,7 @@ function spawnChild(command, args, label, cwd = root, extraEnv = {}) {
}
let server;
let admin;
let plaza;
let plazaProxy;
let ops;
@@ -79,6 +82,7 @@ function shutdown(code = 0) {
if (stopping) return;
stopping = true;
server?.kill('SIGTERM');
admin?.kill('SIGTERM');
plaza?.kill('SIGTERM');
plazaProxy?.kill('SIGTERM');
ops?.kill('SIGTERM');
@@ -121,7 +125,9 @@ const viteEnv = {
};
const opsEnv = {
// /auth (login/session) stays on the public portal; /api (ops console) hits memind_adm.
OPS_API_PROXY: portalUrl,
OPS_ADMIN_PROXY: adminUrl,
VITE_PLAZA_BASE: plazaPublicBase,
VITE_MINDSPACE_BASE: mindSpacePublicBase,
};
@@ -210,8 +216,9 @@ function plazaHostConfigured(host) {
}
}
console.log(`==> 清理端口 ${portalPort} / ${vitePort} / ${plazaPort} / ${opsPort}...`);
console.log(`==> 清理端口 ${portalPort} / ${adminPort} / ${vitePort} / ${plazaPort} / ${opsPort}...`);
freePort(portalPort);
freePort(adminPort);
freePort(vitePort);
freePort(plazaPort);
freePort(opsPort);
@@ -233,6 +240,11 @@ 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}`);
console.log(`==> 启动 Plaza Next.js @ ${plazaPublicBase} (bind ${plazaHost})`);
plaza = spawnChild(
'npm',
@@ -288,6 +300,7 @@ try {
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(` memind_adm ${adminUrl} (gadm.tkmind.cn)`);
console.log('');
console.log('首次使用 Ops:先登录 MindSpace,再执行 node scripts/grant-ops-role.mjs admin ops_admin');
} catch (err) {