# Rust 本地联调与远端发布脚本方案 日期:`2026-04-22` ## 1. 目标 本方案补齐 `server-rs` 在 M7 切流前需要的两类工程脚本: 1. 本地一键联调脚本:同时启动本地 SpacetimeDB、Rust `api-server` 与 Web 前端,并通过现有 Vite 代理开关把运行时 API 指向 Rust。 2. Ubuntu 发布包构建脚本:在仓库根目录生成 `build/<当前时间>/` 发布目录,内含前端 release、Linux `api-server`、SpacetimeDB wasm、启动脚本、停止脚本,以及从仓库根目录复制的 `.env` / `.env.local`,并默认通过 `scp` 上传到目标服务器。 脚本只做部署与联调编排,不改变 HTTP contract、SpacetimeDB schema 命名和对象存储键规划。 ## 2. 本地脚本 入口: ```powershell npm run dev:rust ``` 默认入口直接执行 Bash 版 `scripts/dev-rust-stack.sh`。Windows 下 `dev:rust`、`dev:rust:logs`、`deploy:rust:remote` 与 `build:rust:ubuntu` 会通过 `scripts/run-bash-script.mjs` 优先查找 Git Bash;如安装路径不标准,可用 `GENARRATIVE_BASH` 指定 `bash` 可执行文件。 默认端口: 1. Web 前端:`http://127.0.0.1:3000` 2. Rust `api-server`:`http://127.0.0.1:8082` 3. SpacetimeDB standalone:`http://127.0.0.1:3101` 4. SpacetimeDB database:优先读取仓库根目录 `spacetime.local.json` 的 `database` 字段;没有该字段时才回退到 `genarrative-dev` 5. SpacetimeDB 本地数据与日志目录:`server-rs/.spacetimedb/local` 默认流程: 1. 检查 `cargo`、`node` 与 `spacetime` CLI。 2. Windows Git Bash 下如 `server-rs/.spacetimedb/local/bin/current/spacetimedb-cli.exe` 不存在,先把本机 `spacetime` 所在安装目录的 `bin/` 与 `spacetime.exe` 同步到 `server-rs/.spacetimedb/local/`。 3. 启动 `spacetime --root-dir=server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101`,确保本地数据库与 SpacetimeDB 内部日志不会落到开发者全局目录。 4. 等待 SpacetimeDB 就绪:优先接受 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:3101/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI 在 `502 Bad Gateway` 时也可能返回退出码 `0`。 5. 执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`,确保 publish 的签名身份与 standalone 的本地控制库一致,并在当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。 6. 注入 `GENARRATIVE_API_*` 与 `GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。 7. 等待 `http://127.0.0.1:/healthz` 返回 HTTP 响应后再启动 Vite,避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:`。 8. 注入 `RUST_SERVER_TARGET`、`GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。 9. 任一子进程退出时,脚本回收其余子进程。 Vite 代理覆盖范围: 1. `/api/runtime/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖旧 runtime story 兼容接口。 2. `/api/story/*` 会在 Rust 栈下代理到 Rust `api-server`,覆盖新 story session、battle 查询与 NPC battle 切片接口。 3. 其他 `/api/auth`、`/api/assets`、`/api/custom-world`、`/api/llm` 等路径仍由同一个 `GENARRATIVE_RUNTIME_SERVER_TARGET` 控制,便于 M7 按服务能力逐项做对比 smoke。 安全边界: 1. 当前开发阶段默认执行 `-c=on-conflict`,允许本地开发库在表结构变化时清除旧模块数据后重发。 2. 只有显式传入 `--preserve-database` 时,才跳过 `-c=on-conflict` 并保留现有数据。 3. 如需要复用已经启动的 SpacetimeDB,可传 `--skip-spacetime`。 4. 如只想启动进程不发布模块,可传 `--skip-publish`。 5. 后续进入正式版本前,涉及表结构变化时必须在开发阶段补齐迁移表与迁移函数,不能依赖清库发布作为正式升级策略。 常用示例: ```bash npm run dev:rust ./scripts/dev-rust-stack.sh ./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110 --database genarrative-dev ./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish ./scripts/dev-rust-stack.sh --preserve-database ``` bindings 生成: ```bash npm run spacetime:generate npm run spacetime:generate -- --rust-only ``` 生成规则: 1. `npm run spacetime:generate` 是本仓库刷新 SpacetimeDB Rust client bindings 的唯一推荐入口。 2. 前端不直连 SpacetimeDB,不生成 TypeScript bindings;前端只通过 Rust `api-server` 暴露的 HTTP / SSE contract 访问后端。 3. Rust bindings 不生成私有表绑定,不追加 `--include-private`,生成到短临时目录后同步到 `server-rs/crates/spacetime-client/src/module_bindings`。 4. Windows 下 SpacetimeDB CLI `2.1.0` 会在生成结束后把所有生成文件路径一次性传给 formatter;Rust bindings 文件较多,若直接输出到仓库深目录,可能触发 `Could not format generated files: 文件名或扩展名太长。`。该错误不是单个最长文件路径超过限制,而是 formatter 子进程参数总长超过 Windows `CreateProcess` 限制。 5. CLI 在上述 formatter 失败时仍可能返回退出码 `0`;脚本会捕获输出,只要出现 `Could not format generated files` 就视为失败。 6. 根目录 `spacetime.json` 不配置 `generate` 目标,避免裸 `spacetime generate` 在 Windows 上直接碰到 Rust formatter 路径总长限制,也避免误生成前端 bindings。 7. 不直接手写或局部补 `server-rs/crates/spacetime-client/src/module_bindings` 下的生成文件;schema 变化后重新执行本脚本,并补跑编码检查与对应类型检查。 日志提取: ```bash npm run dev:rust:logs npm run dev:rust:logs -- --follow ./scripts/spacetime-logs-local.sh --lines 500 --output logs/spacetime/local.log ``` 日志提取规则: 1. SpacetimeDB 模块日志以 `spacetime --root-dir=server-rs/.spacetimedb/local logs ` 为唯一提取入口,脚本不直接读取内部日志文件结构。 2. 默认读取 `spacetime.local.json` 的 `database` 字段,默认 server 为 `http://127.0.0.1:3101`。 3. 默认输出到 `logs/spacetime/-.log`,并通过 `tee` 同步显示在终端。 4. `--follow` 仅用于本地追踪,会持续追加到同一个输出文件;停止时用 `Ctrl+C`。 联调排错补充: 1. 如果首页公开广场出现 `上游服务请求失败`,优先检查 `api-server` 错误详情里的 `ws://.../v1/database//subscribe` 是否指向了未发布的库。 2. `spacetime --root-dir=server-rs/.spacetimedb/local list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`。 3. 发布库名与 `GENARRATIVE_SPACETIME_DATABASE` 不一致时,`/api/runtime/custom-world-gallery` 会从 Rust `api-server` 返回 `502`,前端首页只能展示空态或错误提示,无法自行修复。 4. 如果 Vite 输出 `/api/auth/refresh`、`/api/auth/login-options` 或 `/api/runtime/custom-world-gallery` 的 `ECONNREFUSED`,先确认当前脚本是否已经打印 `等待 api-server 就绪` 并通过;正常情况下 Vite 只会在 `/healthz` 可访问后启动,不应再因为 Rust 监听未完成而代理失败。 5. 如果 `spacetime server ping` 打印 `Server could not be reached (502 Bad Gateway)`,即使命令退出码为 `0` 也不能直接视为已就绪;本地脚本会继续探测 `/v1/ping`。若 `/v1/ping` 返回 `200`,说明 standalone 已经可用,可以继续发布模块;若 `/v1/ping` 也失败,脚本会继续等待新启动实例,或在 root-dir 已被其他实例占用时输出占用进程。 编译警告治理: 1. Rust 本地栈启动日志应保持可行动,运行态未使用函数不应长期保留为普通编译警告。 2. 仅供测试断言使用的辅助函数使用 `#[cfg(test)]` 限定,避免进入 `cargo run -p api-server` 的普通二进制编译。 3. 已无调用入口且无迁移价值的映射函数直接删除;如果后续新增同类 SpacetimeDB 记录映射,再按实际调用路径补回,避免提前保留死代码。 Maincloud API 重启补充: 1. `npm run api-server:maincloud` 会先读取 `.env`、`.env.local`,把 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 映射为 `api-server` 使用的 `GENARRATIVE_SPACETIME_*`,再运行 `cargo run -p api-server --manifest-path server-rs/Cargo.toml`。 2. Windows 下脚本会尽力停止本仓库 `server-rs/target/debug/api-server.exe` 对应的旧进程,避免 cargo 重新编译时 exe 被占用。 3. 旧进程已经退出或清理过程中出现瞬时等待失败时,不应阻断新的 `api-server` 启动;脚本只记录清理失败并继续启动。 ## 3. Ubuntu 发布包脚本 入口: ```bash npm run build:rust:ubuntu ``` 兼容入口: ```bash npm run deploy:rust:remote ``` 保留 `deploy:rust:remote` 是为了不打断既有命令习惯;当前语义已调整为“生成 Ubuntu 发布包”,不再通过 SSH 进入服务器执行部署。 默认流程: 1. 在仓库根目录创建 `build/`。 2. 在 `build/` 下创建当前时间命名的目标目录,例如 `build/20260422-153000/`。 3. 使用 Vite 构建前端 release 到目标目录的 `web/`。 4. 执行 `cargo build -p api-server --release --target x86_64-unknown-linux-gnu --manifest-path server-rs/Cargo.toml`,并把 `api-server` 复制到目标目录。 5. 执行 `cargo build -p spacetime-module --release --target wasm32-unknown-unknown --manifest-path server-rs/Cargo.toml`,并把 `spacetime_module.wasm` 复制到目标目录。 6. 把仓库根目录的 `.env` 与 `.env.local` 分别复制到目标目录根部和目标目录的 `web/` 下;复制后统一移除 UTF-8 BOM 与 CRLF,并把 `GENARRATIVE_SPACETIME_DATABASE` 覆盖为本次 `--database` 参数,避免 Jenkins 工作区里残留的旧 `.env.local` 覆盖发布包目标库。 7. 在目标目录写入 `web-server.mjs`,用于托管 `web/` 并把 `/api/*`、`/generated-*`、`/healthz` 反代到本包内的 `api-server`。 8. 在目标目录写入 `start.sh` 与 `stop.sh`;`start.sh` 会先按 `KEY=value` 子集加载发布目录根部的 `.env`、`.env.local`,兼容 UTF-8 BOM 与 CRLF,再回退到构建时通过 `--database`、`--api-port`、`--web-host`、`--web-port`、`--spacetime-host`、`--spacetime-port` 写入的默认值,其中 Web 默认只监听 `127.0.0.1`;并默认导出 `NO_COLOR=1` 与 `CARGO_TERM_COLOR=never`,避免 ANSI 控制码写入日志文件;同时按 Ubuntu 发布环境使用发布目录内 `.spacetimedb/` 作为 root-dir,不再额外设置 `--data-dir`,启动前先执行 `sync_ubuntu_spacetime_install`,优先从 `/usr/.local/share/spacetime/bin//spacetimedb-cli` 或 `$HOME/.local/share/spacetime/bin//spacetimedb-cli` 同步到 `.spacetimedb/bin/current/spacetimedb-cli`,当前线上 `spacetime` 入口为 `/usr/local/bin/spacetime`;启动参数为 `spacetime --root-dir ./.spacetimedb start --edition standalone --listen-addr :`,探活必须确认 `server ping` 输出包含 `Server is online:`;普通启动先无清库发布,若 publish 输出可判定为 schema 冲突,则自动导出旧库、清库发布新 wasm、导入回灌;如果以 `--clear-database` 启动,则内部 `spacetime publish` 会追加 `-c=on-conflict`,代表人工确认清库,不触发自动回灌。 9. 默认执行 `scp -r -i ~\.ssh\dsk.pem build/ ubuntu@82.157.175.59:/home/ubuntu/genarrative/` 上传发布包。 SpacetimeDB database 名称必须匹配 `^[a-z0-9]+(-[a-z0-9]+)*$`:只能使用小写字母、数字,并用单个短横线分隔;大写字母、点号、下划线、首尾短横线和连续短横线都会触发 `spacetime publish` 的 `invalid characters in database name`。发布包构建脚本和 `start.sh` 都会提前拦截这类非法名称。 发布包构建日志会输出 `SpacetimeDB 发布数据库: `;目标服务器执行 `start.sh` 时会在发布前输出最终加载后的 `database/server/root-dir`,用于确认 `.env.local` 或 Jenkins 参数覆盖后的实际发布目标。 发布包结构: ```text build// ├─ .env ├─ .env.local ├─ web/ │ ├─ .env │ └─ .env.local ├─ api-server ├─ spacetime_module.wasm ├─ migration-bootstrap-secret.txt ├─ scripts/ │ └─ spacetime-*.mjs ├─ web-server.mjs ├─ start.sh ├─ stop.sh └─ README.md ``` 常用示例: ```bash npm run build:rust:ubuntu -- --name 20260422-153000 npm run build:rust:ubuntu -- --database genarrative-dev --web-port 3000 --api-port 8082 --spacetime-port 3101 npm run build:rust:ubuntu -- --database genarrative-dev --web-host 127.0.0.1 --web-port 3000 npm run build:rust:ubuntu -- --skip-upload ``` 目标服务器启动: ```bash cd build/ ./start.sh ./stop.sh ``` 如果后续通过 Jenkins 的部署脚本把发布包覆盖到固定部署目录,部署阶段默认只替换 `web/`、`api-server`、`spacetime_module.wasm`、`migration-bootstrap-secret.txt`、`scripts/`、`.env*`、`start.sh`、`stop.sh`、`web-server.mjs`、`README.md` 等发布产物;文件产物使用普通复制,`web/`、`scripts/` 等目录产物递归复制,不会删除部署目录中的 `.spacetimedb/`、`logs/`、`run/`、`deploy-state/`、`database-migrations/` 这类运行态目录。 安全边界: 1. 构建脚本会把仓库根目录已有的 `.env`、`.env.local` 一并复制进发布包,因此运行前必须确认这些文件内容适合被带入目标环境。 2. 如果仓库根目录不存在 `.env` 或 `.env.local`,脚本会打印跳过日志,但不会因此失败;此时 `start.sh` 仅使用构建时写入的默认值与运行时显式传入的环境变量。 3. `start.sh` 只解析合法 `KEY=value` 环境行,支持不加引号、双引号和单引号;不执行复杂 shell 表达式,避免把环境文件变成脚本入口。 4. `start.sh` 默认不追加清理参数;普通发布如果遇到 SpacetimeDB schema 冲突,会调用发布包内的 `scripts/spacetime-export-migration-json.mjs` 导出旧库,再清库发布新 wasm,并调用 `scripts/spacetime-import-migration-json.mjs --replace-existing` 回灌。可通过 `GENARRATIVE_SPACETIME_MIGRATE_ON_CONFLICT=false` 禁用该行为。 5. 自动迁移导出旧库时优先读取 `deploy-state/migration-bootstrap-secret.previous.txt`,导入新库时读取当前发布包 `migration-bootstrap-secret.txt`;Jenkins 部署脚本会在覆盖发布包前保存旧密钥。该快照属于部署状态,不放入 `run/`,避免启停 hook 通过 `sudo` 运行后把部署阶段要写的文件变成 root 私有。手工覆盖发布包时,也应在覆盖前保留旧模块的引导密钥,否则旧库导出可能无法授权。 6. 自动迁移 JSON 默认写入发布目录下 `database-migrations//`;可通过 `GENARRATIVE_SPACETIME_MIGRATION_DIR` 改写。该目录属于运行态,不应被 Jenkins 覆盖部署删除。 7. 只有显式执行 `./start.sh --clear-database` 才追加 `-c=on-conflict`,该模式代表人工确认清库,不执行导出和回灌。 8. `start.sh` 会先复用已经按目标地址就绪的 SpacetimeDB;如果同一个 `.spacetimedb/` root-dir 已被其他未就绪实例占用,则只输出命令名为 `spacetime` 或 `spacetimedb-cli` 且命令行包含当前 root-dir 的真实占用进程并失败,避免把排查用的 `grep` / `awk` 误判为 SpacetimeDB 实例。 9. 如果 `spacetime publish` 报 `403 Forbidden`,优先确认 `spacetime --root-dir ./.spacetimedb login show` 输出的身份是否有权更新目标库;`--clear-database` 不能绕过身份授权。 10. 当前脚本是单目录进程启动方案,不替代生产 systemd、Nginx、TLS、日志轮转与守护进程配置。 11. 如只需要本地生成发布包,可传 `--skip-upload` 跳过默认 scp 上传。 目标服务器最小要求: 1. Ubuntu x86_64。 2. 已安装 `node`,用于运行发布包内的 `web-server.mjs`。 3. 已安装 `spacetime` CLI,`start.sh` 会启动本地 SpacetimeDB 并发布 wasm。 4. 业务密钥通过目标服务器环境变量或发布包同目录 `.env.local` 提供。 ## 4. 与 M7 的关系 这套脚本补齐 M7 的部署执行入口,但不等价于完成灰度切流。M7 后续仍需要在真实 OSS、LLM、短信、微信、SpacetimeDB 数据库和反向代理环境下完成全链路 smoke、关键 SSE 联调和灰度切流验收。