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