收口 Rust Web 入口与 AI 写入链

- 将 3000 主入口继续收口到 mnote-web,补齐 /favicon.ico、/api/auth、session alias、AI run 等 Rust Web 路由边界。

- 更新登录页与 Convex Auth 代理,支持测试账号快速登录写入真实 Convex Auth cookie。

- 推进页面设置、Wolai 对齐、Phase 7 AI kernel/CLI-first 设计文档与相关 smoke 脚本。

- 更新 leptos-tiptap 生成资产、mnote-cli/bridge-runtime、前端依赖和 dev/prod 启动脚本。
This commit is contained in:
lix-2026
2026-05-06 21:44:20 +08:00
parent 98b6360595
commit e8ba12e461
86 changed files with 8872 additions and 1716 deletions
+54 -35
View File
@@ -1,19 +1,21 @@
#!/usr/bin/env node
/**
* 同时热启动前端、FastAPI Celery。
* 同时热启动前端、FastAPI,以及按需启用的 Celery。
* 可使用以下环境变量调整行为:
* - FRONTEND_CMD:覆盖前端启动命令,默认为 "pnpm dev"
* - FRONTEND_CMD:覆盖历史 Next 启动命令,仅在显式启用 legacy compat 或跳过 Rust gateway 时生效
* - BACKEND_CMD:覆盖 FastAPI 启动命令,默认为 "python -m uvicorn app.main:app --reload --port 8000"
* - CELERY_CMD:覆盖 Celery 启动命令,默认为 "celery -A app.workers.celery_app worker --loglevel=info"
* - ENABLE_CELERY:设为 "1" or "true" 时启用默认 Celery worker
* - CELERY_CMD:覆盖 Celery 启动命令;设置后即视为显式启用 Celery
* - CELERY_POOL:只在 CELERY_CMD 未覆盖时生效,设置 Celery worker poolWindows 默认 "solo",其他平台默认使用 Celery 自身默认值
* - PYTHON_BIN:只在 BACKEND_CMD 未覆盖时,设置 Python 可执行文件,默认 "python"
* - CELERY_BIN:只在 CELERY_CMD 未覆盖时,设置 Celery 可执行文件,默认 "celery"
* - REDIS_URL:仅用于探测 Redis 是否就绪,默认 "redis://localhost:6379/0"
* - SKIP_CELERY:设为 "1" or "true" 可跳过 Celery。
* - SKIP_CELERY:设为 "1" or "true" 可强制跳过 Celery。
* - MNOTE_WEB_SKIP_GATEWAY:设为 "1" or "true" 临时恢复旧 Next 3000 入口。
* - NEXT_LEGACY_PORTRust gateway 模式下 Next legacy upstream 端口,默认 3100
* - SKIP_NEXT_LEGACY:设为 "1" or "true" 时只启动 Rust gateway,不启动 Next legacy upstream
* - MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT:设为 "1" or "true" 时才启动 Next legacy upstream。
* - NEXT_LEGACY_PORT:显式启用 legacy compat 时的 Next upstream 端口,默认 3100
* - SKIP_NEXT_LEGACY:兼容旧环境变量;设为 "1" or "true" 时强制只启动 Rust gateway。
*/
const { spawn, execSync } = require("child_process");
@@ -55,29 +57,34 @@ function resolveBackendExecutable(envName, fallbackName) {
const pythonBin = resolveBackendExecutable("PYTHON_BIN", "python");
const celeryBin = resolveBackendExecutable("CELERY_BIN", "celery");
const celeryPoolFromEnv = (process.env.CELERY_POOL || "").trim();
const skipCelery =
(process.env.SKIP_CELERY || "").toLowerCase() === "1" ||
(process.env.SKIP_CELERY || "").toLowerCase() === "true";
const celeryCmdFromEnv = process.env.CELERY_CMD;
const celeryCmdFromEnv = (process.env.CELERY_CMD || "").trim();
const redisUrl = process.env.REDIS_URL || "redis://localhost:6379/0";
const frontendPortFromEnv = Number(process.env.FRONTEND_PORT || 3000);
const backendPortFromEnv = Number(process.env.BACKEND_PORT || 8000);
const nextLegacyPortFromEnv = Number(process.env.NEXT_LEGACY_PORT || 3100);
function isEnabledEnv(value) {
const normalized = String(value || "").toLowerCase();
return normalized === "1" || normalized === "true";
}
function shouldStartCelery(env = process.env) {
if (isEnabledEnv(env.SKIP_CELERY)) return false;
if (String(env.CELERY_CMD || "").trim()) return true;
return isEnabledEnv(env.ENABLE_CELERY);
}
function resolveRuntimePlan(env = process.env) {
const frontendPort = Number(env.FRONTEND_PORT || 3000);
const nextLegacyPort = Number(env.NEXT_LEGACY_PORT || 3100);
const skipGateway = isEnabledEnv(env.MNOTE_WEB_SKIP_GATEWAY);
const skipNextLegacy = !skipGateway && isEnabledEnv(env.SKIP_NEXT_LEGACY);
const legacyCompatRequested =
!skipGateway &&
isEnabledEnv(env.MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT) &&
!isEnabledEnv(env.SKIP_NEXT_LEGACY);
const skipNextLegacy = !skipGateway && !legacyCompatRequested;
const publicPort = Number.isFinite(frontendPort) ? Math.floor(frontendPort) : 3000;
const legacyPort = Number.isFinite(nextLegacyPort) ? Math.floor(nextLegacyPort) : 3100;
const legacyUrl = `http://127.0.0.1:${legacyPort}`;
const legacyCompatEnabled = skipNextLegacy ? "0" : env.MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT || "1";
const legacyCompatEnabled = legacyCompatRequested ? "1" : "0";
return {
skipGateway,
@@ -489,16 +496,18 @@ async function checkRedisReachable(urlString, timeoutMs = 2000) {
}
async function main() {
// 说明:Next dev 在异常退出时可能残留 `.next/dev/lock`,会导致后续启动直接失败。
// 这里在启动前做一次“安全清理”,避免重启后仍卡住
const nextDevLockPath = path.join(frontendDir, ".next", "dev", "lock");
try {
if (fs.existsSync(nextDevLockPath)) {
fs.rmSync(nextDevLockPath, { force: true });
logPrefix("frontend", `检测到残留的 Next dev lock,已移除:${nextDevLockPath}`);
if (runtimePlan.frontendTaskName) {
// 说明:Next dev 在异常退出时可能残留 `.next/dev/lock`,会导致后续启动直接失败
// 只有显式启动历史 Next 时才清理该 lock,避免默认热启动继续触碰旧前端目录。
const nextDevLockPath = path.join(frontendDir, ".next", "dev", "lock");
try {
if (fs.existsSync(nextDevLockPath)) {
fs.rmSync(nextDevLockPath, { force: true });
logPrefix("frontend", `检测到残留的 Next dev lock,已移除:${nextDevLockPath}`);
}
} catch (error) {
logPrefix("frontend", `尝试移除 Next dev lock 失败:${error.message}`);
}
} catch (error) {
logPrefix("frontend", `尝试移除 Next dev lock 失败:${error.message}`);
}
// 说明:你外网绑定了 3000 端口,这里默认强制使用 3000。
@@ -514,7 +523,7 @@ async function main() {
const frontendUrl = `http://localhost:${frontendPort}`;
let nextLegacyPort = null;
if (!skipMnoteWebGateway) {
if (runtimePlan.frontendTaskName === "next-legacy") {
nextLegacyPort = runtimePlan.legacyPort;
const nextLegacyPortOk = await ensurePortFree(nextLegacyPort, "next-legacy");
if (!nextLegacyPortOk) {
@@ -523,20 +532,25 @@ async function main() {
}
}
// 说明:在 Windows 的 cmd.exe 下,`pnpm dev -- -p 3000` 会把 `--` 原样传给 next,导致 next 把 `-p` 误当成目录。
// 用 `pnpm dev -p 3000` 在 PowerShell/cmd.exe 下都能正确传参。
const frontendTask = skipMnoteWebGateway ? findTask("frontend") : findTask("next-legacy");
if (!frontendTask) {
throw new Error("缺少前端任务配置");
const frontendTask = runtimePlan.frontendTaskName ? findTask(runtimePlan.frontendTaskName) : null;
if (runtimePlan.frontendTaskName && !frontendTask) {
throw new Error(`缺少前端任务配置:${runtimePlan.frontendTaskName}`);
}
if (frontendTask) {
// 说明:在 Windows 的 cmd.exe 下,`pnpm dev -- -p 3000` 会把 `--` 原样传给 next,导致 next 把 `-p` 误当成目录。
// 用 `pnpm dev -p 3000` 在 PowerShell/cmd.exe 下都能正确传参。
frontendTask.command = runtimePlan.frontendCommand;
logPrefix(frontendTask.name, `前端目录:${frontendDir}`);
}
frontendTask.command = runtimePlan.frontendCommand;
logPrefix(frontendTask.name, `前端目录:${frontendDir}`);
if (skipMnoteWebGateway) {
logPrefix("frontend", `前端地址:${frontendUrl}`);
} else {
const legacyUrl = runtimePlan.legacyUrl;
logPrefix("mnote-web", `Rust gateway 公开入口:${frontendUrl}`);
logPrefix("next-legacy", `Next legacy upstream${legacyUrl}`);
if (runtimePlan.frontendTaskName === "next-legacy") {
logPrefix("next-legacy", `Next legacy upstream${runtimePlan.legacyUrl}`);
} else {
logPrefix("next-legacy", "默认不启动历史 Next upstream;如需临时兼容请设置 MNOTE_WEB_ENABLE_LEGACY_NEXT_COMPAT=1。");
}
const gatewayTask = tasks.find((task) => task.name === "mnote-web");
if (gatewayTask) {
gatewayTask.command = runtimePlan.mnoteWebCommand;
@@ -558,7 +572,7 @@ async function main() {
backendTask.command = `${pythonBin} -m uvicorn app.main:app --reload --port ${desiredBackendPort}`;
}
if (!skipCelery) {
if (shouldStartCelery(process.env)) {
const defaultCeleryCommand = buildDefaultCeleryCommand();
const celeryTask = {
name: "celery",
@@ -578,9 +592,13 @@ async function main() {
// Redis 未就绪时直接跳过 Celery,避免热调试流程整体退出。
logPrefix(
"celery",
`检测到 ${redisUrl} 无法连接,自动跳过 Celery。请先启动 Redis 或设置 SKIP_CELERY=1 显式跳过`,
`检测到 ${redisUrl} 无法连接,自动跳过 Celery。请先启动 Redis,或继续保持当前默认关闭策略`,
);
}
} else if (isEnabledEnv(process.env.SKIP_CELERY)) {
logPrefix("celery", "已跳过 CelerySKIP_CELERY=1)。");
} else {
logPrefix("celery", "默认不启动 Celery;当前主线页面不依赖 Redis/Celery。如需启用请设置 ENABLE_CELERY=1 或 CELERY_CMD。");
}
if (tasks.length === 0) {
@@ -607,5 +625,6 @@ module.exports = {
isPortFree,
resolveRuntimePlan,
resolveBackendExecutable,
shouldStartCelery,
terminatePid,
};