9b4a25799f
Replace fixed ackText with a rule-based AckProvider that picks response templates by message type and intent (translate, summary, rewrite, poster, ppt, mindmap, code, search, schedule). Pure sync, zero I/O, auto-falls back to config.ackText on any error. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
263 lines
8.2 KiB
JavaScript
Executable File
263 lines
8.2 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import fs from 'node:fs/promises';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
const root = path.join(__dirname, '..');
|
|
const runtimeRoot = path.join(root, '.runtime', 'portal');
|
|
const distDir = path.join(root, 'dist');
|
|
const publicDir = path.join(root, 'public');
|
|
const nodeModulesDir = path.join(root, 'node_modules');
|
|
const schemaFile = path.join(root, 'schema.sql');
|
|
const esbuildBin = path.join(root, 'node_modules', '.pnpm', 'node_modules', '.bin', 'esbuild');
|
|
const skipBuild = process.argv.includes('--skip-build');
|
|
const skipNodeModules = process.argv.includes('--skip-node-modules');
|
|
|
|
const externalPackages = [
|
|
'@node-rs/argon2',
|
|
'@resvg/resvg-js',
|
|
'debug',
|
|
'express',
|
|
'http-proxy-middleware',
|
|
'jsonrepair',
|
|
'mysql2',
|
|
'mysql2/promise',
|
|
'qrcode',
|
|
'redis',
|
|
'undici',
|
|
];
|
|
|
|
function run(command, args, options = {}) {
|
|
return new Promise((resolve, reject) => {
|
|
const child = spawn(command, args, {
|
|
cwd: root,
|
|
stdio: 'inherit',
|
|
env: process.env,
|
|
...options,
|
|
});
|
|
child.on('exit', (code) => {
|
|
if (code === 0) resolve();
|
|
else reject(new Error(`${command} ${args.join(' ')} exited with code ${code ?? 1}`));
|
|
});
|
|
child.on('error', reject);
|
|
});
|
|
}
|
|
|
|
async function exists(targetPath) {
|
|
try {
|
|
await fs.access(targetPath);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
async function remove(targetPath) {
|
|
await fs.rm(targetPath, { recursive: true, force: true });
|
|
}
|
|
|
|
async function copyDir(source, target) {
|
|
await fs.mkdir(path.dirname(target), { recursive: true });
|
|
await fs.cp(source, target, { recursive: true, dereference: false });
|
|
}
|
|
|
|
async function writeFile(targetPath, content) {
|
|
await fs.mkdir(path.dirname(targetPath), { recursive: true });
|
|
await fs.writeFile(targetPath, content, 'utf8');
|
|
}
|
|
|
|
async function buildFrontend() {
|
|
if (skipBuild) return;
|
|
console.log('==> 构建 Portal 前端 dist');
|
|
await run('npm', ['run', 'build']);
|
|
}
|
|
|
|
async function bundleServer() {
|
|
if (!(await exists(esbuildBin))) {
|
|
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
|
}
|
|
console.log('==> 打包 Portal 后端为单文件 runtime');
|
|
const args = [
|
|
'server.mjs',
|
|
'--bundle',
|
|
'--platform=node',
|
|
'--format=esm',
|
|
'--target=node24',
|
|
'--outfile=.runtime/portal/server.mjs',
|
|
'--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
|
|
];
|
|
for (const pkg of externalPackages) {
|
|
args.push(`--external:${pkg}`);
|
|
}
|
|
await run(esbuildBin, args);
|
|
}
|
|
|
|
async function bundleSandboxMcp() {
|
|
if (!(await exists(esbuildBin))) {
|
|
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
|
}
|
|
console.log('==> 打包 sandbox MCP 为单文件 runtime');
|
|
const args = [
|
|
'mindspace-sandbox-mcp.mjs',
|
|
'--bundle',
|
|
'--platform=node',
|
|
'--format=esm',
|
|
'--target=node24',
|
|
'--outfile=.runtime/portal/mindspace-sandbox-mcp.mjs',
|
|
'--banner:js=import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);',
|
|
];
|
|
for (const pkg of externalPackages) {
|
|
args.push(`--external:${pkg}`);
|
|
}
|
|
await run(esbuildBin, args);
|
|
}
|
|
|
|
async function copyRuntimeAssets() {
|
|
console.log('==> 拷贝运行时静态资源');
|
|
await remove(runtimeRoot);
|
|
await fs.mkdir(runtimeRoot, { recursive: true });
|
|
|
|
if (!(await exists(distDir))) {
|
|
throw new Error(`缺少 dist 目录: ${distDir}`);
|
|
}
|
|
await copyDir(distDir, path.join(runtimeRoot, 'dist'));
|
|
|
|
if (await exists(publicDir)) {
|
|
await copyDir(publicDir, path.join(runtimeRoot, 'public'));
|
|
}
|
|
|
|
if (await exists(schemaFile)) {
|
|
await fs.copyFile(schemaFile, path.join(runtimeRoot, 'schema.sql'));
|
|
}
|
|
}
|
|
|
|
async function copyNodeModules() {
|
|
if (skipNodeModules) return;
|
|
if (!(await exists(nodeModulesDir))) {
|
|
throw new Error(`缺少 node_modules 目录: ${nodeModulesDir}`);
|
|
}
|
|
console.log('==> 拷贝生产运行依赖 node_modules');
|
|
await copyDir(nodeModulesDir, path.join(runtimeRoot, 'node_modules'));
|
|
await rewriteNodeModulesSymlinks(path.join(runtimeRoot, 'node_modules'));
|
|
}
|
|
|
|
async function rewriteNodeModulesSymlinks(runtimeNodeModulesDir) {
|
|
console.log('==> 重写 node_modules 顶层符号链接为 runtime 内相对路径');
|
|
const localNodeModulesRoot = `${nodeModulesDir}${path.sep}`;
|
|
|
|
async function visit(dir) {
|
|
const entries = await fs.readdir(dir);
|
|
for (const entryName of entries) {
|
|
const fullPath = path.join(dir, entryName);
|
|
const stats = await fs.lstat(fullPath);
|
|
if (stats.isSymbolicLink()) {
|
|
const linkTarget = await fs.readlink(fullPath);
|
|
const normalizedTarget = path.normalize(linkTarget);
|
|
if (!normalizedTarget.startsWith(localNodeModulesRoot)) continue;
|
|
const relativeInsideNodeModules = normalizedTarget.slice(localNodeModulesRoot.length);
|
|
const runtimeTarget = path.join(runtimeNodeModulesDir, relativeInsideNodeModules);
|
|
const replacement = path.relative(path.dirname(fullPath), runtimeTarget);
|
|
await fs.unlink(fullPath);
|
|
await fs.symlink(replacement, fullPath);
|
|
continue;
|
|
}
|
|
if (stats.isDirectory()) {
|
|
await visit(fullPath);
|
|
}
|
|
}
|
|
}
|
|
|
|
await visit(runtimeNodeModulesDir);
|
|
}
|
|
|
|
async function writeMetadata() {
|
|
const packageJson = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
|
|
const runtimePackageJson = {
|
|
name: `${packageJson.name}-portal-runtime`,
|
|
private: true,
|
|
type: 'module',
|
|
};
|
|
await writeFile(path.join(runtimeRoot, 'package.json'), `${JSON.stringify(runtimePackageJson, null, 2)}\n`);
|
|
await writeFile(
|
|
path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'),
|
|
[
|
|
'#!/usr/bin/env bash',
|
|
'set -euo pipefail',
|
|
'',
|
|
'ROOT="$(cd "$(dirname "$0")/.." && pwd)"',
|
|
'cd "$ROOT"',
|
|
'',
|
|
'if [[ -f "${ROOT}/.env" ]]; then',
|
|
' set -a',
|
|
' # shellcheck disable=SC1091',
|
|
' source "${ROOT}/.env"',
|
|
' set +a',
|
|
'fi',
|
|
'',
|
|
'export NODE_ENV=production',
|
|
'export H5_PORT="${H5_PORT:-8081}"',
|
|
'export H5_PUBLIC_BASE_URL="${H5_PUBLIC_BASE_URL:-https://m.tkmind.cn}"',
|
|
'export TKMIND_API_TARGET="${TKMIND_API_TARGET:-https://127.0.0.1:18006}"',
|
|
'export TKMIND_API_TARGET_1="${TKMIND_API_TARGET_1:-https://127.0.0.1:18007}"',
|
|
'',
|
|
'NODE_BIN="${NODE_BIN:-/opt/homebrew/opt/node@24/bin/node}"',
|
|
'if [[ ! -x "${NODE_BIN}" ]]; then',
|
|
' NODE_BIN="$(command -v node)"',
|
|
'fi',
|
|
'',
|
|
'exec "${NODE_BIN}" "${ROOT}/server.mjs"',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
await writeFile(
|
|
path.join(runtimeRoot, 'RUNBOOK.txt'),
|
|
[
|
|
'Portal runtime artifact',
|
|
'',
|
|
'This directory is meant to run on production without the source tree.',
|
|
'Required persisted items to inherit on the host:',
|
|
' .env',
|
|
' MindSpace/',
|
|
' data/',
|
|
' users/',
|
|
' .tailscale/',
|
|
' public/plaza-covers/',
|
|
' logs/',
|
|
'',
|
|
'Bundled alongside server.mjs (required for sandbox-fs MCP):',
|
|
' mindspace-sandbox-mcp.mjs (esbuild bundle; includes schedule-service deps)',
|
|
'',
|
|
'Key runtime differences must stay in .env, not in the artifact:',
|
|
' DATABASE_URL / MYSQL_*',
|
|
' H5_PUBLIC_BASE_URL',
|
|
' TKMIND_API_TARGET / TKMIND_API_TARGET_1',
|
|
' H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT',
|
|
'',
|
|
'Deployment and operations transport:',
|
|
' 105 fixed IP: 120.26.184.105',
|
|
' 103 / Studio fixed IP: 58.38.22.103',
|
|
' Do not switch back to 10.10.* LAN paths unless explicitly required.',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
async function main() {
|
|
await buildFrontend();
|
|
await copyRuntimeAssets();
|
|
await bundleServer();
|
|
await bundleSandboxMcp();
|
|
await copyNodeModules();
|
|
await writeMetadata();
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755);
|
|
console.log('');
|
|
console.log(`Portal runtime 已生成: ${runtimeRoot}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
});
|