master #14

Merged
kdletters merged 226 commits from master into release 2026-05-13 13:23:09 +08:00
3 changed files with 176 additions and 24 deletions
Showing only changes of commit d6219f1a0c - Show all commits

View File

@@ -111,8 +111,8 @@
- 现象:本地 `npm run dev``3101` 已占用、重复发布 SpacetimeDB wasm 编译太慢,或只想检查 `spacetime-module` 语法而被完整联调链路拖慢。
- 原因:`npm run dev` 默认同时启动 SpacetimeDB standalone、发布 `server-rs/crates/spacetime-module`、启动 Rust `api-server`、主站 Vite 与后台 Vite并非每个阶段都需要完整重启和重新发布。
- 处理:`npm run dev` 启动后会把实际 SpacetimeDB URL 记录到 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。下次启动即使没有传 `--skip-spacetime`,脚本也会先检查 `spacetime.pid` 对应进程和该 URL 是否在线;在线则直接复用现有宿主。`3101` 已被可复用 standalone 占用时也可显式使用 `npm run dev -- --skip-spacetime`;未修改 `spacetime-module` 时使用 `npm run dev -- --skip-publish`;只查模块语法时执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
- 验证:`--skip-spacetime` 后脚本复用现有 `http://127.0.0.1:3101``--skip-publish` 后不再进入 publish 阶段;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能完成 Rust 语法和类型检查。
- 处理:`npm run dev` 启动后会把实际 SpacetimeDB URL 记录到 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`。下次启动即使没有传 `--skip-spacetime`,脚本也会先检查 `spacetime.pid` 对应进程和该 URL 是否在线;在线则直接复用现有宿主。确认需要新启动 SpacetimeDB 时,脚本先检测 `3101`,被占用则输出占用进程并选择最近可用端口,保证 publish 与 `api-server` 都连接同一个实际 SpacetimeDB URL。`api-server` 启动前也会检测 `8082` 并选择最近可用端口。Windows / Git Bash 下不要用 `tr/head/xargs` 管道读取 `spacetime.pid` 或 URL 记录,脚本应使用 Node 读取并短重试,避免 `tr: read error: Device or resource busy`;未修改 `spacetime-module` 时使用 `npm run dev -- --skip-publish`;只查模块语法时执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`
- 验证:`--skip-spacetime` 后脚本复用现有 `http://127.0.0.1:3101``3101``8082` 被其他进程占用时,脚本输出占用进程并使用最近可用端口;`--skip-publish` 后不再进入 publish 阶段;`cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml` 能完成 Rust 语法和类型检查。
- 关联:`docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md``scripts/dev-rust-stack.sh`
## 本地 SpacetimeDB publish 401 可清本地库重发

View File

@@ -36,10 +36,10 @@ npm run dev:rust
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. 启动 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` 中解析最近一次监听地址兜底。否则按正常流程重新启动。
4. 正常启动 `spacetime start --data-dir server-rs/.spacetimedb/local/data --listen-addr 127.0.0.1:3101`,确保本地数据库与 SpacetimeDB 内部日志落在项目数据目录中;启动成功后把实际 URL 写入 `server-rs/.spacetimedb/local/data/dev-rust-spacetime-url`
4. 如果确认需要新启动 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
5. 等待 SpacetimeDB 就绪:优先接受 `spacetime server ping http://127.0.0.1:<spacetime-port>` 输出中的 `Server is online:`;如果 Windows 下 SpacetimeDB CLI `2.1.0` 对已经监听的 standalone 仍打印 `502 Bad Gateway`,脚本会兜底请求 `http://127.0.0.1:<spacetime-port>/v1/ping`,只有该健康端点返回 `2xx` 时才放行。不能只依赖 CLI 退出码,因为 CLI 在 `502 Bad Gateway` 时也可能返回退出码 `0`
6. 执行 `spacetime publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`,确保 publish 仍由 SpacetimeDB CLI 负责构建和发布模块,同时使用 debug 构建参数降低本地开发等待时间;当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
7. 注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*`启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
6. 执行 `spacetime publish <本地数据库名> --server <实际 SpacetimeDB URL> --module-path server-rs/crates/spacetime-module --build-options="--debug" -c=on-conflict --yes`,确保 publish 仍由 SpacetimeDB CLI 负责构建和发布模块,同时使用 debug 构建参数降低本地开发等待时间;当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
7. 启动 `api-server` 前先检测默认 API 端口 `8082` 是否可监听;若已占用,输出占用进程并选择从 `8082` 起向后的最近可用端口。随后注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*`启动默认 debug profile 的 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
8. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`
9. 注入 `RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
10. 任一子进程退出时,脚本回收其余子进程。
@@ -61,7 +61,7 @@ Vite 代理覆盖范围:
本地联调跳过策略:
1. 如果 `3101` 已被当前可复用的 SpacetimeDB standalone 占用,可使用 `npm run dev -- --skip-spacetime` 跳过 SpacetimeDB 宿主启动,只复用现有监听实例并继续后续发布、`api-server` 与前端启动。若占用方不是本仓库本地 SpacetimeDB先停止占用进程或改`--spacetime-port`
1. 如果 `3101` 已被当前可复用的 SpacetimeDB standalone 占用,脚本会优先按 `spacetime.pid``dev-rust-spacetime-url` 复用该宿主;如果确认不是可复用宿主,则会先输出占用进程并选择最近可用端口。也可显式使用 `npm run dev -- --skip-spacetime` 跳过 SpacetimeDB 宿主启动,或`--spacetime-port` 指定起始探测端口
2. 如果当前没有修改 `server-rs/crates/spacetime-module`,可使用 `npm run dev -- --skip-publish` 跳过数据库发布,降低本地启动时的 SpacetimeDB wasm 编译耗时。
3. 如果当前阶段只需要检查 `spacetime-module` 语法,不需要重新发布本地数据库,可执行 `cargo check -p spacetime-module --manifest-path server-rs/Cargo.toml`。该命令只做 Rust 编译检查,不生成新数据库,也不刷新 bindings。
@@ -118,6 +118,7 @@ npm run dev:rust:logs -- --follow
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 已被其他实例占用时输出占用进程。
6. 如果本地 `spacetime publish` 显示 `401` 无权限,且确认本地开发数据可以丢弃,可执行 `spacetime --root-dir=server-rs/.spacetimedb/local server clear` 清除本地 SpacetimeDB 数据库后重新发布。重新发布时日志应表现为创建新的数据库,而不是更新旧数据库;如果仍显示更新旧库或继续无权限,说明 root-dir、库名或 CLI 身份仍未对齐。
7. Windows / Git Bash 下读取 `spacetime.pid``dev-rust-spacetime-url` 时,如果文件正被 SpacetimeDB 更新,不能用 `tr/head/xargs` 管道直接读;脚本使用 Node 读取并短重试,避免出现 `tr: read error: Device or resource busy` 后直接中断。
编译警告治理:

View File

@@ -19,7 +19,7 @@ usage() {
1. 默认同时启动 SpacetimeDB standalone、Rust api-server、主站 Vite 与后台 Vite。
2. 当前开发阶段默认 publish server-rs/crates/spacetime-module 时追加 -c=on-conflict 在结构冲突时清理旧模块数据。
3. 只有显式传入 --preserve-database 时,才会跳过 -c=on-conflict。
4. SpacetimeDB 默认使用 server-rs/.spacetimedb/local/data 作为本地数据目录;端口被占用时自动接受 SpacetimeDB 建议的最近可用端口。
4. SpacetimeDB 默认使用 server-rs/.spacetimedb/local/data 作为本地数据目录;需要新启动时会先检测端口并选择最近可用端口。
5. 默认在发布模块前随机生成迁移引导密钥,注入 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 并显示在控制台。
EOF
}
@@ -191,6 +191,144 @@ port_from_listen_addr() {
echo "${listen_addr##*:}"
}
read_digits_from_file() {
local file_path="$1"
node -e '
const fs = require("fs");
const filePath = process.argv[1];
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const digits = fs.readFileSync(filePath, "utf8").replace(/\D/gu, "").slice(0, 20);
if (digits) {
process.stdout.write(digits);
process.exit(0);
}
process.exit(1);
} catch {
sleep(100);
}
}
process.exit(1);
' "${file_path}" 2>/dev/null
}
read_trimmed_first_line_from_file() {
local file_path="$1"
node -e '
const fs = require("fs");
const filePath = process.argv[1];
const sleep = (ms) => Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
for (let attempt = 0; attempt < 5; attempt += 1) {
try {
const firstLine = fs.readFileSync(filePath, "utf8").split(/\r?\n/u)[0]?.trim() ?? "";
if (firstLine) {
process.stdout.write(firstLine);
process.exit(0);
}
process.exit(1);
} catch {
sleep(100);
}
}
process.exit(1);
' "${file_path}" 2>/dev/null
}
validate_tcp_port() {
local port="$1"
local label="$2"
if ! [[ "${port}" =~ ^[0-9]+$ ]] || ((port < 1 || port > 65535)); then
echo "[dev:rust] ${label} 端口无效: ${port}" >&2
exit 1
fi
}
is_tcp_port_available() {
local host="$1"
local port="$2"
node -e '
const net = require("net");
const host = process.argv[1];
const port = Number(process.argv[2]);
if (!Number.isInteger(port) || port < 1 || port > 65535) {
process.exit(2);
}
const server = net.createServer();
server.once("error", () => process.exit(1));
server.once("listening", () => server.close(() => process.exit(0)));
server.listen({ host, port, exclusive: true });
' "${host}" "${port}" >/dev/null 2>&1
}
describe_tcp_port_owner() {
local port="$1"
if command -v powershell.exe >/dev/null 2>&1; then
PORT_FOR_POWERSHELL="${port}" powershell.exe -NoProfile -Command '
$port = [int]$env:PORT_FOR_POWERSHELL
Get-NetTCPConnection -LocalPort $port -State Listen -ErrorAction SilentlyContinue |
Select-Object -First 5 |
ForEach-Object {
$process = Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue
$name = if ($process) { $process.ProcessName } else { "unknown" }
"pid=$($_.OwningProcess) name=$name local=$($_.LocalAddress):$($_.LocalPort)"
}
' 2>/dev/null || true
return
fi
if command -v ss >/dev/null 2>&1; then
ss -lntp 2>/dev/null | awk -v port=":${port}" 'NR == 1 || index($0, port) > 0 { print }' || true
return
fi
if command -v netstat >/dev/null 2>&1; then
netstat -lntp 2>/dev/null | awk -v port=":${port}" 'NR == 1 || index($0, port) > 0 { print }' || true
fi
}
find_nearest_available_port() {
local host="$1"
local requested_port="$2"
local label="$3"
local max_probe_count="${4:-100}"
validate_tcp_port "${requested_port}" "${label}"
local port="${requested_port}"
local probe_count=0
while ((port <= 65535 && probe_count < max_probe_count)); do
if is_tcp_port_available "${host}" "${port}"; then
if [[ "${port}" != "${requested_port}" ]]; then
echo "[dev:rust] ${label} 端口 ${host}:${requested_port} 已占用,改用最近可用端口 ${host}:${port}" >&2
fi
echo "${port}"
return
fi
if ((probe_count == 0)); then
echo "[dev:rust] ${label} 端口 ${host}:${requested_port} 已占用,开始查找最近可用端口。" >&2
describe_tcp_port_owner "${requested_port}" >&2 || true
fi
port=$((port + 1))
probe_count=$((probe_count + 1))
done
echo "[dev:rust] 无法为 ${label}${host}:${requested_port} 起向后 ${max_probe_count} 个端口内找到可用端口。" >&2
exit 1
}
spacetime_url_record_path() {
local data_dir="$1"
echo "${data_dir}/dev-rust-spacetime-url"
@@ -215,8 +353,7 @@ read_spacetime_pid() {
fi
local pid
pid="$(tr -cd '0-9' <"${pid_file}" | head -c 20)"
if [[ -z "${pid}" ]]; then
if ! pid="$(read_digits_from_file "${pid_file}")"; then
return 1
fi
@@ -230,16 +367,13 @@ try_reuse_existing_spacetime() {
local existing_pid
local recorded_url=""
if ! existing_pid="$(read_spacetime_pid "${data_dir}")"; then
return 1
if [[ -f "${url_file}" ]]; then
if ! recorded_url="$(read_trimmed_first_line_from_file "${url_file}")"; then
recorded_url=""
fi
fi
if ! kill -0 "${existing_pid}" 2>/dev/null; then
echo "[dev:rust] 发现过期 spacetime.pid: ${existing_pid},将重新启动 SpacetimeDB。"
return 1
fi
if [[ ! -f "${url_file}" ]]; then
if [[ -z "${recorded_url}" ]]; then
local start_log
start_log="$(spacetime_start_log_path "${data_dir}")"
if [[ -f "${start_log}" ]]; then
@@ -261,11 +395,9 @@ try_reuse_existing_spacetime() {
fi
fi
if [[ -z "${recorded_url}" ]]; then
echo "[dev:rust] 发现运行中的 SpacetimeDB pid=${existing_pid},但未找到 URL 记录: ${url_file}"
echo "[dev:rust] 未找到可复用的 SpacetimeDB URL 记录: ${url_file}"
return 1
fi
else
recorded_url="$(head -n 1 "${url_file}" | tr -d '\r' | xargs)"
fi
if [[ -z "${recorded_url}" ]]; then
@@ -277,10 +409,24 @@ try_reuse_existing_spacetime() {
SPACETIME_SERVER="${recorded_url}"
SPACETIME_PORT="$(port_from_listen_addr "${recorded_url}")"
SPACETIME_REUSED_EXISTING=1
echo "[dev:rust] 复用已启动 SpacetimeDB: ${SPACETIME_SERVER} (pid=${existing_pid})"
if existing_pid="$(read_spacetime_pid "${data_dir}")"; then
echo "[dev:rust] 复用已启动 SpacetimeDB: ${SPACETIME_SERVER} (pid=${existing_pid})"
else
echo "[dev:rust] 复用已启动 SpacetimeDB: ${SPACETIME_SERVER} (pid 文件暂不可读)"
fi
return 0
fi
if ! existing_pid="$(read_spacetime_pid "${data_dir}")"; then
echo "[dev:rust] URL 不在线且 spacetime.pid 暂不可读: ${recorded_url},将检查 data-dir 占用。"
return 1
fi
if ! kill -0 "${existing_pid}" 2>/dev/null; then
echo "[dev:rust] 发现过期 spacetime.pid: ${existing_pid},将重新启动 SpacetimeDB。"
return 1
fi
echo "[dev:rust] pid=${existing_pid} 存在,但 URL 不在线: ${recorded_url},将重新启动 SpacetimeDB。"
return 1
}
@@ -760,15 +906,17 @@ if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then
exit 1
fi
SPACETIME_PORT="$(find_nearest_available_port "${SPACETIME_HOST}" "${SPACETIME_PORT}" "SpacetimeDB")"
SPACETIME_SERVER="http://${SPACETIME_HOST}:${SPACETIME_PORT}"
SPACETIME_START_LOG="$(spacetime_start_log_path "${SPACETIME_DATA_DIR}")"
mkdir -p "$(dirname -- "${SPACETIME_START_LOG}")"
: >"${SPACETIME_START_LOG}"
echo "[dev:rust] 启动 spacetimedb"
(
cd "${SERVER_RS_DIR}"
# 当目标端口被占用时SpacetimeDB 会询问是否使用最近的可用端口;
# 这里直接发送回车接受默认建议,再从启动日志解析实际监听端口。
printf '\n' | spacetime \
# 启动前已经由脚本选定端口,避免 api-server 和 SpacetimeDB 对数据库地址认知不一致。
spacetime \
start \
--data-dir "${SPACETIME_DATA_DIR}" \
--listen-addr "${SPACETIME_HOST}:${SPACETIME_PORT}" \
@@ -815,8 +963,11 @@ fi
echo "[dev:rust] 启动 api-server"
load_api_server_env_files
API_PORT="$(find_nearest_available_port "${API_HOST}" "${API_PORT}" "api-server")"
API_TARGET_HOST="$(resolve_client_host "${API_HOST}")"
# `.env.local` 可以给单独 `dev:web` 配置代理目标,但完整栈的前端必须跟随本次 `--api-port`。
RUST_SERVER_TARGET="http://${API_TARGET_HOST}:${API_PORT}"
echo "[dev:rust] api actual: ${RUST_SERVER_TARGET}"
(
cd "${REPO_ROOT}"
GENARRATIVE_API_HOST="${API_HOST}" \