668 lines
26 KiB
JavaScript
Executable File
668 lines
26 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
|
import fs from 'node:fs/promises';
|
|
import os from 'node:os';
|
|
import path from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { spawn } from 'node:child_process';
|
|
|
|
import { removeForbiddenPortalRuntimePaths } from '../release-gate/artifact.mjs';
|
|
|
|
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 runtimeNodeTarget = process.env.PORTAL_RUNTIME_NODE_TARGET || 'node24';
|
|
const runtimeInstallMode = process.env.PORTAL_RUNTIME_INSTALL_MODE || 'bundle-node-modules';
|
|
const linuxArm64ResvgPackage = '@resvg/resvg-js-linux-arm64-gnu';
|
|
const linuxArm64Argon2Package = '@node-rs/argon2-linux-arm64-gnu';
|
|
const linuxArm64NativePackages = [
|
|
{
|
|
hostPackage: '@resvg/resvg-js',
|
|
targetPackage: linuxArm64ResvgPackage,
|
|
},
|
|
{
|
|
hostPackage: '@node-rs/argon2',
|
|
targetPackage: linuxArm64Argon2Package,
|
|
},
|
|
];
|
|
|
|
const externalPackages = [
|
|
'@img/sharp-darwin-arm64',
|
|
'@img/sharp-libvips-darwin-arm64',
|
|
'@node-rs/argon2',
|
|
'@resvg/resvg-js',
|
|
'chromium-bidi',
|
|
'chromium-bidi/*',
|
|
'debug',
|
|
'express',
|
|
'fsevents',
|
|
'http-proxy-middleware',
|
|
'jsonrepair',
|
|
'mysql2',
|
|
'mysql2/promise',
|
|
'playwright',
|
|
'playwright-core',
|
|
'qrcode',
|
|
'redis',
|
|
'sharp',
|
|
'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) {
|
|
if (!(await exists(targetPath))) return;
|
|
try {
|
|
await fs.rm(targetPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 });
|
|
} catch (error) {
|
|
if (error?.code !== 'ENOTEMPTY' && error?.code !== 'EBUSY') throw error;
|
|
await run('rm', ['-rf', targetPath]);
|
|
}
|
|
}
|
|
|
|
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');
|
|
const buildEnv = {
|
|
...process.env,
|
|
VITE_PLAZA_BASE:
|
|
process.env.VITE_PLAZA_BASE ??
|
|
process.env.PLAZA_PUBLIC_BASE ??
|
|
'https://plaza.tkmind.cn',
|
|
VITE_MINDSPACE_BASE:
|
|
process.env.VITE_MINDSPACE_BASE ??
|
|
process.env.H5_PUBLIC_BASE_URL ??
|
|
'https://m.tkmind.cn',
|
|
};
|
|
await run('npm', ['run', 'build'], { env: buildEnv });
|
|
}
|
|
|
|
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=${runtimeNodeTarget}`,
|
|
'--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 bundleCanaryProxy() {
|
|
if (!(await exists(esbuildBin))) {
|
|
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
|
}
|
|
console.log('==> 打包用户级灰度路由器');
|
|
const args = [
|
|
'memind-canary-proxy.mjs',
|
|
'--bundle',
|
|
'--platform=node',
|
|
'--format=esm',
|
|
`--target=${runtimeNodeTarget}`,
|
|
'--outfile=.runtime/portal/memind-canary-proxy.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 bundleDeepseekCompatProxy() {
|
|
if (!(await exists(esbuildBin))) {
|
|
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
|
}
|
|
console.log('==> 打包 DeepSeek 工具轮次兼容代理');
|
|
const args = [
|
|
'deepseek-no-think-proxy.mjs',
|
|
'--bundle',
|
|
'--platform=node',
|
|
'--format=esm',
|
|
`--target=${runtimeNodeTarget}`,
|
|
'--outfile=.runtime/portal/deepseek-no-think-proxy.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=${runtimeNodeTarget}`,
|
|
'--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 bundleMindSearchMcp() {
|
|
if (!(await exists(esbuildBin))) {
|
|
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
|
}
|
|
console.log('==> 打包 MindSearch MCP 为单文件 runtime');
|
|
const args = [
|
|
'tkmind-search-mcp.mjs',
|
|
'--bundle',
|
|
'--platform=node',
|
|
'--format=esm',
|
|
`--target=${runtimeNodeTarget}`,
|
|
'--outfile=.runtime/portal/tkmind-search-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 bundleExcelMcp() {
|
|
if (!(await exists(esbuildBin))) {
|
|
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
|
}
|
|
console.log('==> 打包 Excel Analyst MCP 为单文件 runtime');
|
|
const args = [
|
|
'tkmind-excel-mcp.mjs',
|
|
'--bundle',
|
|
'--platform=node',
|
|
'--format=esm',
|
|
`--target=${runtimeNodeTarget}`,
|
|
'--outfile=.runtime/portal/tkmind-excel-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 bundleAgentRunWorker() {
|
|
if (!(await exists(esbuildBin))) {
|
|
throw new Error(`未找到 esbuild: ${esbuildBin}`);
|
|
}
|
|
console.log('==> 打包 agent run worker 为单文件 runtime');
|
|
const args = [
|
|
'scripts/agent-run-worker.mjs',
|
|
'--bundle',
|
|
'--platform=node',
|
|
'--format=esm',
|
|
`--target=${runtimeNodeTarget}`,
|
|
'--outfile=.runtime/portal/scripts/agent-run-worker.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'));
|
|
}
|
|
|
|
await removeForbiddenPortalRuntimePaths(runtimeRoot);
|
|
|
|
if (await exists(schemaFile)) {
|
|
await fs.copyFile(schemaFile, path.join(runtimeRoot, 'schema.sql'));
|
|
}
|
|
|
|
const skillsDir = path.join(root, 'skills');
|
|
if (await exists(skillsDir)) {
|
|
console.log('==> 拷贝平台 skills 目录');
|
|
await copyDir(skillsDir, path.join(runtimeRoot, 'skills'));
|
|
}
|
|
}
|
|
|
|
async function copyNodeModules() {
|
|
if (skipNodeModules) return;
|
|
if (!(await exists(nodeModulesDir))) {
|
|
throw new Error(`缺少 node_modules 目录: ${nodeModulesDir}`);
|
|
}
|
|
const resolvedNodeModulesDir = await fs.realpath(nodeModulesDir);
|
|
console.log('==> 拷贝生产运行依赖 node_modules');
|
|
await copyDir(resolvedNodeModulesDir, path.join(runtimeRoot, 'node_modules'));
|
|
await rewriteNodeModulesSymlinks(path.join(runtimeRoot, 'node_modules'), resolvedNodeModulesDir);
|
|
await installLinuxArm64NativePackages();
|
|
}
|
|
|
|
async function readLinuxArm64NativePackageVersion({ hostPackage, targetPackage }) {
|
|
const hostPackageJson = JSON.parse(
|
|
await fs.readFile(path.join(nodeModulesDir, hostPackage, 'package.json'), 'utf8'),
|
|
);
|
|
const version = hostPackageJson.optionalDependencies?.[targetPackage];
|
|
if (!version) {
|
|
throw new Error(`无法从 ${hostPackage} 确定 ${targetPackage} 的版本`);
|
|
}
|
|
return version;
|
|
}
|
|
|
|
// The runtime artifact is assembled on macOS but runs in a Linux ARM64 Colima VM.
|
|
// pnpm copies the host optional binaries only, so explicitly materialize every
|
|
// Linux ARM64 native package that an externalized runtime dependency loads.
|
|
async function installLinuxArm64NativePackage(nativePackage) {
|
|
const { targetPackage } = nativePackage;
|
|
const version = await readLinuxArm64NativePackageVersion(nativePackage);
|
|
const stagingDir = await fs.mkdtemp(path.join(os.tmpdir(), 'memind-native-linux-arm64-'));
|
|
const targetDir = path.join(runtimeRoot, 'node_modules', ...targetPackage.split('/'));
|
|
try {
|
|
console.log(`==> 补齐 Linux ARM64 原生依赖 ${targetPackage} (${version})`);
|
|
await run('npm', ['pack', `${targetPackage}@${version}`, '--pack-destination', stagingDir]);
|
|
const archive = (await fs.readdir(stagingDir)).find((name) => name.endsWith('.tgz'));
|
|
if (!archive) throw new Error(`未生成 ${targetPackage} 安装包`);
|
|
await remove(targetDir);
|
|
await fs.mkdir(targetDir, { recursive: true });
|
|
await run('tar', ['-xzf', path.join(stagingDir, archive), '--strip-components=1', '-C', targetDir]);
|
|
const files = await fs.readdir(targetDir);
|
|
if (!files.some((name) => name.endsWith('.node'))) {
|
|
throw new Error(`${targetPackage} 缺少原生 .node 文件`);
|
|
}
|
|
} finally {
|
|
await remove(stagingDir);
|
|
}
|
|
}
|
|
|
|
async function installLinuxArm64NativePackages() {
|
|
for (const nativePackage of linuxArm64NativePackages) {
|
|
await installLinuxArm64NativePackage(nativePackage);
|
|
}
|
|
}
|
|
|
|
async function rewriteNodeModulesSymlinks(runtimeNodeModulesDir, sourceNodeModulesDir) {
|
|
console.log('==> 重写 node_modules 顶层符号链接为 runtime 内相对路径');
|
|
const localNodeModulesRoot = `${sourceNodeModulesDir}${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 copyMindspacePublicLinkTools() {
|
|
console.log('==> 拷贝 MindSpace 公开页链接检查工具');
|
|
await fs.copyFile(
|
|
path.join(root, 'mindspace-public-links.mjs'),
|
|
path.join(runtimeRoot, 'mindspace-public-links.mjs'),
|
|
);
|
|
await fs.mkdir(path.join(runtimeRoot, 'scripts'), { recursive: true });
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'check-mindspace-public-links.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'check-mindspace-public-links.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'repair-mindspace-public-downloads.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'repair-mindspace-public-downloads.mjs'),
|
|
);
|
|
}
|
|
|
|
async function writeMetadata() {
|
|
const packageJson = JSON.parse(await fs.readFile(path.join(root, 'package.json'), 'utf8'));
|
|
const runtimeDependencies = Object.fromEntries(
|
|
externalPackages
|
|
.filter((pkg) => packageJson.dependencies?.[pkg])
|
|
.map((pkg) => [pkg, packageJson.dependencies[pkg]]),
|
|
);
|
|
const runtimeOptionalDependencies = Object.fromEntries(
|
|
await Promise.all(
|
|
linuxArm64NativePackages.map(async (nativePackage) => [
|
|
nativePackage.targetPackage,
|
|
await readLinuxArm64NativePackageVersion(nativePackage),
|
|
]),
|
|
),
|
|
);
|
|
const runtimePackageJson = {
|
|
name: `${packageJson.name}-portal-runtime`,
|
|
private: true,
|
|
type: 'module',
|
|
engines: {
|
|
node: '>=22',
|
|
},
|
|
dependencies: runtimeDependencies,
|
|
optionalDependencies: runtimeOptionalDependencies,
|
|
};
|
|
await writeFile(path.join(runtimeRoot, 'package.json'), `${JSON.stringify(runtimePackageJson, null, 2)}\n`);
|
|
const tunnelScript = path.join(root, 'scripts', 'memind-portal-tunnel.sh');
|
|
const runtimeScriptsDir = path.join(runtimeRoot, 'scripts');
|
|
await fs.mkdir(runtimeScriptsDir, { recursive: true });
|
|
if (await exists(tunnelScript)) {
|
|
await fs.copyFile(tunnelScript, path.join(runtimeScriptsDir, 'memind-portal-tunnel.sh'));
|
|
await fs.chmod(path.join(runtimeScriptsDir, 'memind-portal-tunnel.sh'), 0o755);
|
|
} else {
|
|
throw new Error(`缺少隧道脚本: ${tunnelScript}`);
|
|
}
|
|
|
|
const prodStartScript = path.join(root, 'scripts', 'run-memind-portal-prod.sh');
|
|
if (await exists(prodStartScript)) {
|
|
await fs.copyFile(prodStartScript, path.join(runtimeScriptsDir, 'run-memind-portal-prod.sh'));
|
|
await fs.chmod(path.join(runtimeScriptsDir, 'run-memind-portal-prod.sh'), 0o755);
|
|
} else {
|
|
throw new Error(`缺少 Portal 启动脚本: ${prodStartScript}`);
|
|
}
|
|
for (const scriptName of [
|
|
'run-memind-portal-candidate.sh',
|
|
'run-memind-canary-proxy-prod.sh',
|
|
'run-deepseek-compat-proxy-candidate.sh',
|
|
]) {
|
|
const source = path.join(root, 'scripts', scriptName);
|
|
if (!(await exists(source))) {
|
|
throw new Error(`缺少灰度运行脚本: ${source}`);
|
|
}
|
|
await fs.copyFile(source, path.join(runtimeScriptsDir, scriptName));
|
|
await fs.chmod(path.join(runtimeScriptsDir, scriptName), 0o755);
|
|
}
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'goosed-canary.compose.yml'),
|
|
path.join(runtimeScriptsDir, 'goosed-canary.compose.yml'),
|
|
);
|
|
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'load-env.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'load-env.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'wechat-mp-menu.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'wechat-mp-menu.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'check-stream-runtime.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'check-stream-runtime.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'runtime-worker-drain.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'runtime-worker-metrics.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'runtime-worker-metrics.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'runtime-worker-heartbeat.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'runtime-worker-heartbeat.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'runtime-slo-report.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'runtime-slo-report.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'embed-memory-v2-local-hash.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'embed-memory-v2-local-hash.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'install-runtime-metrics-agent.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'install-runtime-metrics-agent.sh'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'install-runtime-heartbeat-agent.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'install-runtime-heartbeat-agent.sh'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'install-runtime-slo-report-agent.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'install-runtime-slo-report-agent.sh'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'install-runtime-slo-soak-agent.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'install-runtime-slo-soak-agent.sh'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'install-agent-run-worker-agent.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'install-agent-run-worker-agent.sh'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'check-tool-runtime.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'check-agent-code-run-entry.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'check-agent-code-run-entry.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'check-agent-run-worker.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'check-agent-run-worker.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'agent-run-guard.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'agent-run-guard.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'agent-run-guard-lib.mjs'),
|
|
path.join(runtimeRoot, 'scripts', 'agent-run-guard-lib.mjs'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'install-agent-run-guard-agent.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'install-agent-run-guard-agent.sh'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'resume-agent-run-prod.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'resume-agent-run-prod.sh'),
|
|
);
|
|
await fs.copyFile(
|
|
path.join(root, 'scripts', 'ensure-goose-session-postgres.sh'),
|
|
path.join(runtimeRoot, 'scripts', 'ensure-goose-session-postgres.sh'),
|
|
);
|
|
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)',
|
|
' tkmind-search-mcp.mjs (esbuild bundle; required when MindSearch is enabled)',
|
|
' tkmind-excel-mcp.mjs (esbuild bundle; required when Excel Analyst is enabled)',
|
|
' wechat-mp.bundle.mjs (esbuild bundle; hot-swappable WeChat MP module)',
|
|
' memind-canary-proxy.mjs (immutable-identity stable/candidate router)',
|
|
' deepseek-no-think-proxy.mjs (DeepSeek V4 tool-round compatibility proxy)',
|
|
'',
|
|
'Post-deploy validation (scripts/check-mindspace-public-links.mjs):',
|
|
' Scans MindSpace/*/public/*.html for missing relative href/src/cover targets.',
|
|
'',
|
|
`Runtime install mode: ${runtimeInstallMode}`,
|
|
`Bundle target: ${runtimeNodeTarget}`,
|
|
'',
|
|
'Platform agent skills (synced into user workspaces):',
|
|
' skills/',
|
|
'Key runtime differences must stay in .env, not in the artifact:',
|
|
' DATABASE_URL / MYSQL_*',
|
|
' H5_PUBLIC_BASE_URL',
|
|
' TKMIND_API_TARGETS / TKMIND_API_TARGET / TKMIND_API_TARGET_1',
|
|
' H5_USERS_ROOT / MINDSPACE_STORAGE_ROOT / MEMIND_SHARED_PUBLISH_ROOT',
|
|
'',
|
|
'Frontend compile-time public URLs (baked into dist at build):',
|
|
' VITE_PLAZA_BASE defaults to https://plaza.tkmind.cn in build-portal-runtime.mjs',
|
|
' VITE_MINDSPACE_BASE defaults to H5_PUBLIC_BASE_URL or https://m.tkmind.cn',
|
|
' Do not build production dist with VITE_PLAZA_BASE=http://127.0.0.1:* in .env',
|
|
'',
|
|
'Deployment and operations transport:',
|
|
' H5 public domain: m.tkmind.cn',
|
|
' Current public path: m.tkmind.cn -> 105 nginx -> 103 Portal :8081',
|
|
' No separate public test domain remains; use explicit local/test overrides only when needed.',
|
|
' Production H5 and WeChat links should use m.tkmind.cn unless a test rollout explicitly overrides H5_PUBLIC_BASE_URL.',
|
|
' Canary layout: stable :8081, candidate :18081, identity router :18080, candidate goosed :18015.',
|
|
'',
|
|
'Streaming runtime operations:',
|
|
' node scripts/check-stream-runtime.mjs',
|
|
' node scripts/runtime-worker-drain.mjs status',
|
|
' node scripts/runtime-worker-drain.mjs reconcile',
|
|
' node scripts/runtime-worker-drain.mjs reconcile --apply',
|
|
' node scripts/runtime-worker-metrics.mjs status',
|
|
' node scripts/runtime-worker-metrics.mjs sample',
|
|
' bash scripts/install-runtime-metrics-agent.sh',
|
|
' node scripts/runtime-worker-heartbeat.mjs once',
|
|
' node scripts/runtime-worker-heartbeat.mjs serve',
|
|
' bash scripts/install-runtime-heartbeat-agent.sh',
|
|
' node scripts/runtime-slo-report.mjs',
|
|
' node scripts/runtime-slo-report.mjs --write-report',
|
|
' node scripts/runtime-slo-report.mjs --write-report --prune --retention-days 30',
|
|
' bash scripts/install-runtime-slo-report-agent.sh',
|
|
' bash scripts/install-runtime-slo-soak-agent.sh',
|
|
' node scripts/runtime-worker-drain.mjs drain goosed-3',
|
|
' node scripts/runtime-worker-drain.mjs undrain goosed-3',
|
|
' node scripts/check-tool-runtime.mjs',
|
|
' node scripts/check-agent-code-run-entry.mjs',
|
|
' curl -sk https://m.tkmind.cn/api/runtime/status # includes toolRuntime.queue',
|
|
' node scripts/agent-run-worker.mjs --status',
|
|
' node scripts/agent-run-worker.mjs --once',
|
|
' bash scripts/install-agent-run-worker-agent.sh # installs disabled by default',
|
|
' node scripts/check-agent-run-worker.mjs # read-only LaunchAgent/queue check',
|
|
' node scripts/agent-run-guard.mjs # dry-run auto-pause guard check',
|
|
' node scripts/agent-run-guard.mjs --apply # first-time pause only; repeat checks are no-ops',
|
|
' bash scripts/install-agent-run-guard-agent.sh # installs guard LaunchAgent',
|
|
' bash scripts/resume-agent-run-prod.sh # manual recovery after guard pause',
|
|
'',
|
|
].join('\n'),
|
|
);
|
|
}
|
|
|
|
async function bundleWechatMp() {
|
|
console.log('==> 打包 WeChat MP runtime bundle');
|
|
await run('node', ['scripts/build-wechat-mp-runtime.mjs']);
|
|
}
|
|
|
|
async function main() {
|
|
await buildFrontend();
|
|
await copyRuntimeAssets();
|
|
await bundleServer();
|
|
await bundleCanaryProxy();
|
|
await bundleDeepseekCompatProxy();
|
|
await bundleWechatMp();
|
|
await bundleSandboxMcp();
|
|
await bundleMindSearchMcp();
|
|
await bundleExcelMcp();
|
|
await bundleAgentRunWorker();
|
|
if (runtimeInstallMode === 'bundle-node-modules' && !skipNodeModules) {
|
|
await copyNodeModules();
|
|
}
|
|
await copyMindspacePublicLinkTools();
|
|
await writeMetadata();
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-prod.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-portal-candidate.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'run-memind-canary-proxy-prod.sh'), 0o755);
|
|
await fs.chmod(
|
|
path.join(runtimeRoot, 'scripts', 'run-deepseek-compat-proxy-candidate.sh'),
|
|
0o755,
|
|
);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'wechat-mp-menu.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-stream-runtime.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-drain.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-metrics.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-worker-heartbeat.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'runtime-slo-report.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'agent-run-worker.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-runtime-metrics-agent.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-runtime-heartbeat-agent.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-runtime-slo-report-agent.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-runtime-slo-soak-agent.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-agent-run-worker-agent.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'agent-run-guard.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'install-agent-run-guard-agent.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'resume-agent-run-prod.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'ensure-goose-session-postgres.sh'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-tool-runtime.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'check-agent-run-worker.mjs'), 0o755);
|
|
await fs.chmod(path.join(runtimeRoot, 'scripts', 'memind-portal-tunnel.sh'), 0o755);
|
|
console.log('');
|
|
console.log(`Portal runtime 已生成: ${runtimeRoot}`);
|
|
}
|
|
|
|
main().catch((error) => {
|
|
console.error(error instanceof Error ? error.message : error);
|
|
process.exit(1);
|
|
});
|