Merge branch 'master' of http://82.157.175.59:3000/GenarrativeAI/Genarrative
CI / verify (push) Has been cancelled
CI / verify (push) Has been cancelled
This commit is contained in:
@@ -31,7 +31,7 @@
|
||||
- 涉及前端或 Node 侧的 SpacetimeDB TypeScript SDK、订阅、绑定使用时,按 `spacetimedb-typescript` 与 `spacetimedb-concepts` 执行。
|
||||
- 若仓库内旧实现或旧文档与这些 skill 冲突,先修正文档和方案,再继续编码。
|
||||
- 修改后端代码后,必须使用 `npm run api-server:maincloud` 自动重新运行后端,并执行相应自动测试;不要再使用旧的后端重启命令。
|
||||
|
||||
- 数据库表结构更改后,需要对齐migration.rs
|
||||
|
||||
## 文档图谱
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
当前阶段只解决一件事:
|
||||
|
||||
1. 由 `Axum` 根据服务端配置,返回当前环境启用的登录方式列表。
|
||||
2. 密码登录入口由 Rust `password_entry` 固定承载,作为登录弹窗的保底入口。
|
||||
|
||||
本阶段明确不包含:
|
||||
|
||||
@@ -31,7 +32,7 @@
|
||||
|
||||
```json
|
||||
{
|
||||
"availableLoginMethods": ["phone", "wechat"]
|
||||
"availableLoginMethods": ["phone", "password", "wechat"]
|
||||
}
|
||||
```
|
||||
|
||||
@@ -40,6 +41,7 @@
|
||||
1. `availableLoginMethods` 为字符串数组
|
||||
2. 当前阶段只允许出现:
|
||||
- `phone`
|
||||
- `password`
|
||||
- `wechat`
|
||||
|
||||
### 3.3 返回顺序
|
||||
@@ -47,7 +49,8 @@
|
||||
返回顺序固定为:
|
||||
|
||||
1. 先 `phone`
|
||||
2. 再 `wechat`
|
||||
2. 再 `password`
|
||||
3. 再 `wechat`
|
||||
|
||||
这样可以保证前端按钮顺序稳定,不因配置解析顺序变化而漂移。
|
||||
|
||||
@@ -61,8 +64,9 @@
|
||||
映射规则固定为:
|
||||
|
||||
1. `SMS_AUTH_ENABLED=true` 时返回 `phone`
|
||||
2. `WECHAT_AUTH_ENABLED=true` 时返回 `wechat`
|
||||
3. 两者都关闭时返回空数组
|
||||
2. Rust 密码登录主链可用时固定返回 `password`
|
||||
3. `WECHAT_AUTH_ENABLED=true` 时返回 `wechat`
|
||||
4. 短信与微信都关闭时仍返回 `["password"]`
|
||||
|
||||
## 5. crate 边界
|
||||
|
||||
@@ -84,13 +88,15 @@
|
||||
|
||||
1. 根据 `availableLoginMethods` 决定是否展示手机号 / 微信入口
|
||||
2. 不再假设某种登录方式一定存在
|
||||
3. 若 `/api/auth/login-options` 联调失败或返回空数组,前端仍保留 `password` 入口,避免登录弹窗显示“当前登录入口暂不可用”后无法继续操作。
|
||||
|
||||
## 6. 测试要求
|
||||
|
||||
至少覆盖:
|
||||
|
||||
1. 默认配置下返回空数组
|
||||
2. 同时启用短信与微信时返回 `["phone", "wechat"]`
|
||||
1. 默认配置下返回 `["password"]`
|
||||
2. 同时启用短信、密码与微信时返回 `["phone", "password", "wechat"]`
|
||||
3. 前端在 `login-options` 读取失败或返回空数组时,仍展示密码登录表单
|
||||
|
||||
## 7. 完成定义
|
||||
|
||||
|
||||
@@ -2,26 +2,34 @@
|
||||
|
||||
## 背景
|
||||
|
||||
当前大鱼运行时使用左下固定虚拟摇杆,玩家必须点到摇杆区域才能移动。移动端实际体验应改为屏幕任意位置触控:第一次触点只建立方向原点,不直接产生移动;后续触点相对原点形成方向向量,角色按恒定速度朝该方向行动。
|
||||
当前大鱼运行时使用左下固定虚拟摇杆,玩家必须点到摇杆区域才能移动。移动端实际体验应改为屏幕任意位置触控:按住屏幕时不再用“触点相对按下原点”的距离判断方向,而是按固定采样间隔比较“上一次触点位置”和“当前触点位置”的位移方向,角色按恒定速度朝采样方向行动。
|
||||
|
||||
## 交互规则
|
||||
|
||||
1. 玩家在玩法舞台内按下时,记录第一个触点坐标为本次操作原点。
|
||||
1. 玩家在玩法舞台内按下时,记录当前触点坐标为采样起点。
|
||||
2. 按下瞬间提交 `{ x: 0, y: 0 }`,保证一开始玩家不动。
|
||||
3. 手指/鼠标移动后,用“当前触点 - 原点”的向量计算方向。
|
||||
4. 输入只表达方向,不表达速度;超过死区后归一化为单位方向向量。
|
||||
5. 松开或取消触控后,清空操作原点并提交 `{ x: 0, y: 0 }`。
|
||||
6. 前端继续定时提交当前方向,即使没有玩家输入也提交零向量,让后端或本地直达局持续推进世界 tick。
|
||||
3. 按住期间每 `0.1` 秒采样一次当前触点坐标,并用“当前触点 - 上一次采样触点”的位移计算方向。
|
||||
4. 输入只表达方向,不表达速度;超过采样死区后归一化为单位方向向量,未超过死区则沿用上一段有效方向。
|
||||
5. 每次采样完成后,把当前触点写为下一次采样的“上一次位置”。
|
||||
6. 松开或取消触控后,清空采样状态并提交 `{ x: 0, y: 0 }`。
|
||||
7. 前端继续定时提交当前方向,即使没有玩家输入也提交零向量,让后端或本地直达局持续推进世界 tick。
|
||||
|
||||
## 本地直达局边界
|
||||
|
||||
- `/big-fish` 的本地占位局必须在玩家未操作时继续移动野生对象。
|
||||
- 大鱼吃小鱼的最终游玩部分统一放在前端本地 runtime,不再调用后端 start/input/get run 接口。
|
||||
- 后端只保留 Agent 创作、草稿编译、资产生成、发布和作品列表;不负责移动、碰撞、刷鱼、合成、屏外清理或胜负模拟。
|
||||
- `/big-fish` 的本地直达局和平台内测试玩法必须共用前端本地 runtime,并在玩家未操作时继续移动野生对象。
|
||||
- 玩家速度保持恒定,只由方向决定移动方向。
|
||||
- 野生对象使用确定性游动规则,避免直达入口看起来像静态截图。
|
||||
|
||||
## 验收口径
|
||||
|
||||
1. 在舞台任意位置按下时玩家不立即移动。
|
||||
2. 按住并拖动后,玩家朝拖动方向恒速移动。
|
||||
2. 按住并拖动后,玩家方向来自最近 `0.1` 秒位移,而不是来自按下原点。
|
||||
3. 松开后玩家停止。
|
||||
4. 不操作时野生对象仍会持续游动。
|
||||
|
||||
## 资产生成补充口径
|
||||
|
||||
- 大鱼实体主图、`idle_float` 和 `move_swim` 动作图都按 RPG 角色资产口径处理:单体完整入镜、中心构图、轮廓清晰、透明背景、无 UI/文字/水印。
|
||||
- 场地背景只生成环境,不生成规则说明、UI 或巨大主体遮挡;画面元素要少,中央活动区域要大,边缘只保留少量出生区提示。
|
||||
|
||||
@@ -26,3 +26,7 @@
|
||||
2. 新建拼图创作工作区后,顶部创作进度显示 `0%`。
|
||||
3. 发送第一条用户消息后,进度按模型回包或后续操作正常推进。
|
||||
4. 生成草稿、生成资产、发布等后续阶段的进度值不受本次调整影响。
|
||||
|
||||
## 2026-04-27 复核记录
|
||||
|
||||
大鱼吃小鱼创建链路曾回退为 `progress_percent = 20`。本次重新将 `create_big_fish_session_tx` 的初始进度锁定为 `0`,并用 `INITIAL_BIG_FISH_CREATION_PROGRESS_PERCENT` 常量表达该约束,避免欢迎语或初始锚点占位再次被误计为创作进度。
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# SpacetimeDB JSON 字符串迁移 procedure 设计
|
||||
|
||||
## 背景
|
||||
|
||||
`spacetime sql` 只能稳定读取 public 表或数据库 owner 可见表。当前 `ai_result_reference` 等运行真相表保持 private,直接 SQL 导出会遇到 `no such table` 或 private table 提示,不能作为跨服务器迁移的稳定方案。
|
||||
|
||||
SpacetimeDB reducer 必须保持确定性,不能访问文件系统和网络。procedure 可以返回数据,也可以在事务中读取 private 表,因此迁移改为:
|
||||
|
||||
1. `spacetime-module` 内的导出 procedure 读取迁移白名单表,并直接返回迁移 JSON 字符串。
|
||||
2. Node 运维脚本默认通过 `spacetime call` 调用导出 procedure,把返回的 JSON 字符串写入本地文件。
|
||||
3. Node 运维脚本读取本地 JSON 文件内容。导入时默认先通过 `POST /v1/identity` 创建临时 Web API identity/token,再用当前 CLI 登录态把该 identity 授权为迁移操作员,最后通过 HTTP request body 把 JSON 字符串传给导入 procedure。
|
||||
4. 导入 procedure 校验 JSON 与表白名单后,在事务中写入目标数据库。
|
||||
|
||||
procedure 不再访问 HTTP 文件桥,也不接收部署机本地文件路径。这样可以避开 SpacetimeDB 对 private/special-purpose 地址的 HTTP 访问限制,并避免把 private 表内容通过临时 HTTP 服务转发。
|
||||
|
||||
`spacetime login show --token` 输出的是 CLI 登录 token,不是 HTTP `/v1/database/.../call` 所需的数据库连接 token。导入脚本如果没有显式传 `--token`,会自动调用 `POST /v1/identity` 获取 Web API token;迁移时不要把 CLI token 传给 `--token`。
|
||||
|
||||
## 接口
|
||||
|
||||
### 迁移操作员授权
|
||||
|
||||
迁移 procedure 会读取并写入 private 表,不能对任意登录身份开放。模块内新增私有表 `database_migration_operator` 作为迁移操作员白名单:
|
||||
|
||||
- `operator_identity`: 被授权调用迁移 procedure 的 SpacetimeDB identity。
|
||||
- `created_at`: 授权写入时间。
|
||||
- `created_by`: 发起授权的 identity。
|
||||
- `note`: 运维备注,只用于区分来源、环境或临时用途。
|
||||
|
||||
`database_migration_operator` 只控制迁移 procedure 调用权限,不会被导出或导入,避免把源库的运维权限复制到目标库。
|
||||
|
||||
首次授权时,操作员表为空,必须通过编译进模块的 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` 引导密钥授权第一位操作员。发布脚本会在构建或发布 SpacetimeDB 模块时自动生成一份强随机引导密钥、注入 wasm 编译环境,并在控制台显示;运维人员必须记录对应数据库本次发布输出的密钥。表内已经存在操作员后,后续授权与撤销只能由已有操作员发起;此时不再接受引导密钥越权扩权。
|
||||
|
||||
新增 procedure:
|
||||
|
||||
- `authorize_database_migration_operator`: 授权或更新迁移操作员备注。
|
||||
- `revoke_database_migration_operator`: 撤销迁移操作员。
|
||||
|
||||
运维流程:
|
||||
|
||||
```bash
|
||||
npm run spacetime:publish:maincloud -- --database <database>
|
||||
# 控制台会输出:
|
||||
# [spacetime:maincloud] 迁移引导密钥: <本次发布随机密钥>
|
||||
```
|
||||
|
||||
发布完成后,在同一台机器上用当前 `spacetime login` 身份授权操作员:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-authorize-migration-operator.mjs \
|
||||
--server maincloud \
|
||||
--database xushi-p4wfr \
|
||||
--bootstrap-secret <本次发布随机密钥> \
|
||||
--operator-identity <identity-hex> \
|
||||
--note "2026-04-27 migration"
|
||||
```
|
||||
|
||||
迁移完成后可以撤销临时操作员:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-revoke-migration-operator.mjs \
|
||||
--server maincloud \
|
||||
--database xushi-p4wfr \
|
||||
--operator-identity <identity-hex>
|
||||
```
|
||||
|
||||
生产环境建议迁移完成后用 `--no-migration-bootstrap-secret` 重新发布一个未设置 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET` 的模块版本,避免引导密钥长期留在 wasm 中。
|
||||
|
||||
### 发布脚本密钥行为
|
||||
|
||||
当前所有会构建或发布 `spacetime-module` 的脚本默认都会生成并显示迁移引导密钥:
|
||||
|
||||
- `npm run spacetime:publish:maincloud`:在本机 `cargo build` 前生成密钥,控制台输出 `[spacetime:maincloud] 迁移引导密钥: ...`。
|
||||
- `npm run dev:rust`:在本地 `spacetime publish --module-path` 前生成密钥,控制台输出 `[dev:rust] 迁移引导密钥: ...`。
|
||||
- `npm run deploy:rust:remote`:在构建发布包 wasm 前生成密钥,控制台输出 `[deploy:rust] 迁移引导密钥: ...`,并把同一份密钥写入发布包根目录的 `migration-bootstrap-secret.txt`。服务器执行 `./start.sh` 发布 wasm 时也会再次显示该文件里的密钥。
|
||||
|
||||
如果迁移完成后不希望 wasm 继续携带引导密钥,重新发布时传 `--no-migration-bootstrap-secret`。远端发布包若使用 `--skip-spacetime-build`,必须同时传 `--no-migration-bootstrap-secret`,否则脚本会拒绝生成一个无法注入旧 wasm 的新密钥。
|
||||
|
||||
### 导出
|
||||
|
||||
`export_database_migration_to_file(ctx, input)`
|
||||
|
||||
输入字段:
|
||||
|
||||
- `include_tables`: 可选表名白名单。为空时导出当前实现支持的全部迁移表。
|
||||
|
||||
返回字段:
|
||||
|
||||
- `ok`: 是否成功。
|
||||
- `schema_version`: 迁移 JSON 结构版本。
|
||||
- `migration_json`: 成功时包含完整迁移 JSON 字符串,失败时为空。
|
||||
- `table_stats`: 表级导出统计。
|
||||
- `error_message`: 失败原因。
|
||||
|
||||
### 导入
|
||||
|
||||
`import_database_migration_from_file(ctx, input)`
|
||||
|
||||
`import_database_migration_incremental_from_file(ctx, input)`
|
||||
|
||||
输入字段:
|
||||
|
||||
- `migration_json`: 导出 procedure 生成的完整迁移 JSON 字符串。
|
||||
- `include_tables`: 可选表名白名单。为空时导入文件内所有支持表。
|
||||
- `replace_existing`: 是否先清空本次迁移文件内实际导入的目标表。不会清空迁移文件未包含的表;分批迁移时只覆盖当前批次。
|
||||
- `dry_run`: 只解析和统计,不写表。
|
||||
|
||||
导入模式:
|
||||
|
||||
- 默认严格追加:不清空目标表,逐行插入;遇到主键或唯一约束冲突时失败并回滚,适合确认目标库没有同表旧数据时使用。
|
||||
- 增量追加:调用 `import_database_migration_incremental_from_file`,不清空目标表;遇到已存在或唯一约束冲突的行会跳过并计入 `skipped_row_count`,只插入目标库缺失的行。该模式不会更新目标库已有行。
|
||||
- 覆盖导入:`replace_existing = true` 时先删除覆盖范围内的目标表旧数据,再插入迁移文件中的数据;只适合迁移文件是这些表完整快照的场景。
|
||||
|
||||
返回字段:
|
||||
|
||||
- `ok`: 是否成功。
|
||||
- `schema_version`: 迁移 JSON 结构版本。
|
||||
- `migration_json`: 导入场景恒为空,避免重复回传大 JSON。
|
||||
- `table_stats`: 表级导入或跳过统计。
|
||||
- `error_message`: 失败原因。
|
||||
|
||||
保留 `export_database_migration_to_file` / `import_database_migration_from_file` 名称,是为了减少已经记住的 procedure 名变更;语义上不再代表 module 直接读写文件。
|
||||
|
||||
## Node 脚本
|
||||
|
||||
本机导出时,先确保本机 SpacetimeDB 服务和源数据库可访问,然后授权本机调用身份:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-authorize-migration-operator.mjs \
|
||||
--server dev \
|
||||
--database xushi-p4wfr \
|
||||
--bootstrap-secret <本机源库发布时输出的随机密钥> \
|
||||
--operator-identity <本机 spacetime login show 中的 identity> \
|
||||
--note "local export"
|
||||
```
|
||||
|
||||
导出脚本负责调用本机源库 procedure 并保存返回 JSON:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-export-migration-json.mjs \
|
||||
--server dev \
|
||||
--database xushi-p4wfr \
|
||||
--out tmp/spacetime-migrations/source-2026-04-27.json
|
||||
```
|
||||
|
||||
把 `tmp/spacetime-migrations/source-2026-04-27.json` 复制到服务器后,在服务器上登录目标 SpacetimeDB,并授权服务器侧调用身份:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-authorize-migration-operator.mjs \
|
||||
--server maincloud \
|
||||
--database xushi-p4wfr \
|
||||
--bootstrap-secret <服务器目标库发布时输出的随机密钥> \
|
||||
--operator-identity <服务器 spacetime login show 中的 identity> \
|
||||
--note "server import"
|
||||
```
|
||||
|
||||
导入脚本负责读取服务器本地文件并把 JSON 字符串通过 Web API request body 传入目标库 procedure。因为 JSON 不再放进 `spacetime call` 命令行参数,所以不会触发 Linux `spawn E2BIG`:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-import-migration-json.mjs \
|
||||
--server maincloud \
|
||||
--database xushi-p4wfr \
|
||||
--bootstrap-secret <服务器目标库发布时输出的随机密钥> \
|
||||
--in tmp/spacetime-migrations/source-2026-04-27.json
|
||||
```
|
||||
|
||||
如果目标库已有部分数据,且只想补充缺失行,使用增量模式:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-import-migration-json.mjs \
|
||||
--server maincloud \
|
||||
--database xushi-p4wfr \
|
||||
--bootstrap-secret <服务器目标库发布时输出的随机密钥> \
|
||||
--in tmp/spacetime-migrations/source-2026-04-27.json \
|
||||
--incremental
|
||||
```
|
||||
|
||||
如果目标库对应表已有数据,并且本次文件应作为这些表的覆盖来源,再显式追加 `--replace-existing`。脚本会把覆盖范围限定为迁移文件内实际包含且本次会导入的表,避免分批导入时清空文件外的其它表。
|
||||
|
||||
默认情况下,脚本会自动完成三步:
|
||||
|
||||
1. `POST /v1/identity` 创建临时 Web API identity/token。
|
||||
2. 使用当前机器 `spacetime` CLI 登录态调用 `authorize_database_migration_operator`,授权这个临时 identity。
|
||||
3. 使用 `Authorization: Bearer <临时 token>` 调用 `import_database_migration_from_file`,把完整迁移 JSON 放在 HTTP body 中。
|
||||
4. 导入请求结束后,脚本会用同一个临时 Web API token 调用 `revoke_database_migration_operator`,撤销该临时 identity。
|
||||
|
||||
所有直接访问 SpacetimeDB Web API 的 POST 请求必须显式发送 `Content-Type: application/json`。部分 SpacetimeDB 版本不会接受省略 content type 或附带非预期 media type 的请求,即使 body 本身是合法 JSON,也会返回 `HTTP 415`。
|
||||
|
||||
如果你已经有可用的数据库连接 token,也可以显式传 `--token <web-api-token>`。这种情况下脚本不会自动授权;该 token 对应的 identity 必须已经是迁移操作员。
|
||||
|
||||
正式导入前建议先加 `--dry-run`,确认 JSON 可解析、版本匹配、表名都在迁移白名单内。
|
||||
|
||||
`--dry-run` 不会模拟目标库主键或唯一约束冲突,因此增量模式的 `skipped_row_count` 只有真实导入时才准确。
|
||||
|
||||
不要在只想追加数据时使用 `--replace-existing`。该参数会先删除覆盖范围内的目标表旧数据,再插入迁移文件中的数据;如果源文件不是完整快照,会造成目标表数据丢失。
|
||||
|
||||
如需分批迁移,可用逗号分隔表名:
|
||||
|
||||
```bash
|
||||
node scripts/spacetime-export-migration-json.mjs \
|
||||
--database xushi-p4wfr \
|
||||
--out tmp/spacetime-migrations/ai.json \
|
||||
--include ai_task,ai_task_stage,ai_text_chunk,ai_result_reference
|
||||
```
|
||||
|
||||
`--server` 支持 `dev`、`local`、`maincloud`,也可以直接传 SpacetimeDB 服务器 URL。导出、授权、撤销默认走 `spacetime call`,使用当前机器的 CLI 登录态;导入默认走 Web API request body,避免大 JSON 触发命令行长度限制。数据库名可通过 `--database`、`GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE` 或 `GENARRATIVE_SPACETIME_DATABASE` 提供。
|
||||
|
||||
授权脚本额外支持:
|
||||
|
||||
- `--bootstrap-secret` 或 `GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET`
|
||||
- `--operator-identity` 或 `GENARRATIVE_SPACETIME_MIGRATION_OPERATOR_IDENTITY`
|
||||
- `--note`
|
||||
|
||||
## 表范围
|
||||
|
||||
首版覆盖当前 private table 报错相关与主运行真相表:
|
||||
|
||||
- 认证:`auth_store_snapshot`、`user_account`、`auth_identity`、`refresh_session`
|
||||
- AI:`ai_task`、`ai_task_stage`、`ai_text_chunk`、`ai_result_reference`
|
||||
- 运行存档与账户投影:`runtime_snapshot`、`runtime_setting`、`user_browse_history`、`profile_dashboard_state`、`profile_wallet_ledger`、`profile_invite_code`、`profile_referral_relation`、`profile_played_world`、`profile_membership`、`profile_recharge_order`、`profile_save_archive`
|
||||
- RPG 运行真相:`player_progression`、`chapter_progression`、`npc_state`、`story_session`、`story_event`、`inventory_slot`、`battle_state`、`treasure_record`、`quest_record`、`quest_log`
|
||||
- 自定义世界:`custom_world_profile`、`custom_world_session`、`custom_world_agent_session`、`custom_world_agent_message`、`custom_world_agent_operation`、`custom_world_draft_card`、`custom_world_gallery_entry`
|
||||
- 资产索引:`asset_object`、`asset_entity_binding`
|
||||
- 拼图:`puzzle_agent_session`、`puzzle_agent_message`、`puzzle_work_profile`、`puzzle_runtime_run`
|
||||
- 大鱼:`big_fish_creation_session`、`big_fish_agent_message`、`big_fish_asset_slot`、`big_fish_runtime_run`
|
||||
|
||||
后续新增 SpacetimeDB 表时,必须同步把表加入迁移白名单与本文档。
|
||||
|
||||
## 风险与限制
|
||||
|
||||
迁移 JSON 作为 procedure 返回值和 HTTP request body 传递,会受 SpacetimeDB 调用响应体、请求体以及中间代理大小限制。数据量较大时,先按 `include_tables` 分批迁移;若单表本身过大,再补充分片 procedure,而不是恢复 HTTP 文件桥。
|
||||
|
||||
`spacetime call` 在 PowerShell 中手写 JSON 容易被剥掉双引号。导入大文件时也不能把完整 JSON 放进命令行参数,否则 Linux 会在启动子进程时返回 `spawn E2BIG`。推荐使用仓库里的 Node 脚本,由脚本直接走 Web API request body,避免 shell 二次处理和命令行长度限制。
|
||||
@@ -185,7 +185,3 @@ export type BigFishRuntimeSnapshotResponse = {
|
||||
eventLog: string[];
|
||||
updatedAt: string;
|
||||
};
|
||||
|
||||
export type BigFishRunResponse = {
|
||||
run: BigFishRuntimeSnapshotResponse;
|
||||
};
|
||||
|
||||
@@ -27,7 +27,8 @@ usage() {
|
||||
--skip-upload 只生成本地发布包,不上传服务器
|
||||
--skip-web-build 跳过 Vite 构建,仅用于调试
|
||||
--skip-api-build 跳过 api-server 构建,仅用于调试
|
||||
--skip-spacetime-build 跳过 wasm 构建,仅用于调试
|
||||
--skip-spacetime-build 跳过 wasm 构建,仅用于调试;此时必须同时传 --no-migration-bootstrap-secret
|
||||
--no-migration-bootstrap-secret 构建不带迁移引导密钥的 spacetime-module wasm
|
||||
|
||||
目标服务器要求:
|
||||
Ubuntu x86_64,已安装 node、spacetime CLI,并允许执行目标目录内的 start.sh / stop.sh。
|
||||
@@ -127,6 +128,36 @@ replace_placeholder_in_file() {
|
||||
sed -i "s|${placeholder}|${escaped_value}|g" "${file_path}"
|
||||
}
|
||||
|
||||
generate_migration_bootstrap_secret() {
|
||||
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
|
||||
}
|
||||
|
||||
prepare_migration_bootstrap_secret() {
|
||||
case "${MIGRATION_BOOTSTRAP_SECRET_MODE}" in
|
||||
auto)
|
||||
MIGRATION_BOOTSTRAP_SECRET="$(generate_migration_bootstrap_secret)"
|
||||
;;
|
||||
manual)
|
||||
if [[ "${#MIGRATION_BOOTSTRAP_SECRET}" -lt 16 ]]; then
|
||||
echo "[deploy:rust] 迁移引导密钥至少需要 16 个字符。" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
disabled)
|
||||
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
|
||||
echo "[deploy:rust] 未启用迁移引导密钥。"
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "[deploy:rust] 未知迁移引导密钥模式: ${MIGRATION_BOOTSTRAP_SECRET_MODE}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
echo "[deploy:rust] 迁移引导密钥: ${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
SERVER_RS_DIR="${REPO_ROOT}/server-rs"
|
||||
@@ -147,6 +178,8 @@ SKIP_WEB_BUILD=0
|
||||
SKIP_API_BUILD=0
|
||||
SKIP_SPACETIME_BUILD=0
|
||||
BUILD_COMPLETED=0
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="auto"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
@@ -214,6 +247,16 @@ while [[ $# -gt 0 ]]; do
|
||||
SKIP_SPACETIME_BUILD=1
|
||||
shift
|
||||
;;
|
||||
--migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET="${2:?缺少 --migration-bootstrap-secret 的值}"
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="manual"
|
||||
shift 2
|
||||
;;
|
||||
--no-migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="disabled"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "[deploy:rust] 未知参数: $1" >&2
|
||||
usage >&2
|
||||
@@ -227,6 +270,12 @@ if [[ ! "${BUILD_NAME}" =~ ^[0-9A-Za-z._-]+$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ "${SKIP_SPACETIME_BUILD}" -eq 1 && "${MIGRATION_BOOTSTRAP_SECRET_MODE}" != "disabled" ]]; then
|
||||
echo "[deploy:rust] --skip-spacetime-build 无法把迁移引导密钥注入 wasm。" >&2
|
||||
echo "[deploy:rust] 请移除 --skip-spacetime-build,或同时传 --no-migration-bootstrap-secret。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
TARGET_DIR="${BUILD_ROOT}/${BUILD_NAME}"
|
||||
WEB_DIR="${TARGET_DIR}/web"
|
||||
API_BINARY_SOURCE="${SERVER_RS_DIR}/target/x86_64-unknown-linux-gnu/release/api-server"
|
||||
@@ -249,6 +298,8 @@ fi
|
||||
require_command node
|
||||
require_command cargo
|
||||
|
||||
prepare_migration_bootstrap_secret
|
||||
|
||||
if [[ "${SKIP_WEB_BUILD}" -ne 1 ]]; then
|
||||
require_command npm
|
||||
fi
|
||||
@@ -310,6 +361,11 @@ fi
|
||||
|
||||
copy_required_file "${WASM_SOURCE}" "${TARGET_DIR}/spacetime_module.wasm" "spacetime-module wasm"
|
||||
|
||||
if [[ "${MIGRATION_BOOTSTRAP_SECRET_MODE}" != "disabled" ]]; then
|
||||
printf "%s\n" "${MIGRATION_BOOTSTRAP_SECRET}" >"${TARGET_DIR}/migration-bootstrap-secret.txt"
|
||||
chmod 600 "${TARGET_DIR}/migration-bootstrap-secret.txt"
|
||||
fi
|
||||
|
||||
cat >"${TARGET_DIR}/web-server.mjs" <<'WEB_SERVER'
|
||||
import http from 'node:http';
|
||||
import fs from 'node:fs';
|
||||
@@ -529,6 +585,7 @@ API_PORT="${GENARRATIVE_API_PORT:-__GENARRATIVE_DEFAULT_API_PORT__}"
|
||||
API_LOG="${GENARRATIVE_API_LOG:-info,tower_http=info}"
|
||||
WEB_HOST="${GENARRATIVE_WEB_HOST:-__GENARRATIVE_DEFAULT_WEB_HOST__}"
|
||||
WEB_PORT="${GENARRATIVE_WEB_PORT:-__GENARRATIVE_DEFAULT_WEB_PORT__}"
|
||||
MIGRATION_BOOTSTRAP_SECRET_FILE="${SCRIPT_DIR}/migration-bootstrap-secret.txt"
|
||||
|
||||
# 日志默认落文件,显式关闭 ANSI 颜色码,避免控制字符写入 *.log。
|
||||
export NO_COLOR="${NO_COLOR:-1}"
|
||||
@@ -778,6 +835,11 @@ if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then
|
||||
fi
|
||||
|
||||
echo "[start] 发布 SpacetimeDB wasm: ${SPACETIME_DATABASE}"
|
||||
if [[ -f "${MIGRATION_BOOTSTRAP_SECRET_FILE}" ]]; then
|
||||
echo "[start] 迁移引导密钥: $(cat "${MIGRATION_BOOTSTRAP_SECRET_FILE}")"
|
||||
else
|
||||
echo "[start] 未启用迁移引导密钥。"
|
||||
fi
|
||||
if ! spacetime --root-dir="${SPACETIME_ROOT_DIR}" "${PUBLISH_ARGS[@]}"; then
|
||||
echo "[start] SpacetimeDB 发布失败。" >&2
|
||||
echo "[start] 如果错误包含 403 Forbidden 或 is not authorized,通常是当前 CLI 身份无权更新目标数据库。" >&2
|
||||
@@ -868,6 +930,7 @@ cat >"${TARGET_DIR}/README.md" <<EOF
|
||||
- \`web/\`:Vite release 静态资源
|
||||
- \`api-server\`:x86_64-unknown-linux-gnu release 可执行文件
|
||||
- \`spacetime_module.wasm\`:wasm32-unknown-unknown release 模块
|
||||
- \`migration-bootstrap-secret.txt\`:本发布包 wasm 编译时注入的迁移引导密钥;服务器 \`start.sh\` 发布时会显示,迁移授权完成后可删除
|
||||
- \`web-server.mjs\`:静态网站与 API 反代入口
|
||||
- \`start.sh\` / \`stop.sh\`:目标服务器启动与停止脚本
|
||||
|
||||
@@ -896,6 +959,7 @@ cat >"${TARGET_DIR}/README.md" <<EOF
|
||||
- \`GENARRATIVE_SPACETIME_ROOT_DIR\`:默认使用发布目录下的 \`.spacetimedb/\`,同时承载本地 SpacetimeDB 运行数据与 CLI 身份。
|
||||
- \`GENARRATIVE_SPACETIME_TIMEOUT_SECONDS\`:等待 SpacetimeDB 就绪的秒数,默认 \`60\`。
|
||||
- OSS、LLM、短信、微信等业务密钥仍通过目标服务器环境变量或同目录 \`.env.local\` 管理。
|
||||
- 迁移引导密钥由构建发布包时随机生成,构建日志和服务器 \`start.sh\` 发布日志都会显示同一份密钥。
|
||||
EOF
|
||||
|
||||
BUILD_COMPLETED=1
|
||||
|
||||
@@ -10,6 +10,7 @@ usage() {
|
||||
./scripts/dev-rust-stack.sh --api-timeout-seconds 600
|
||||
./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish
|
||||
./scripts/dev-rust-stack.sh --preserve-database
|
||||
./scripts/dev-rust-stack.sh --no-migration-bootstrap-secret
|
||||
npm run dev:rust:logs -- --follow
|
||||
|
||||
说明:
|
||||
@@ -17,6 +18,7 @@ usage() {
|
||||
2. 当前开发阶段默认 publish server-rs/crates/spacetime-module 时追加 -c=on-conflict 在结构冲突时清理旧模块数据。
|
||||
3. 只有显式传入 --preserve-database 时,才会跳过 -c=on-conflict。
|
||||
4. SpacetimeDB 默认使用 server-rs/.spacetimedb/local 作为本地数据与日志目录。
|
||||
5. 默认在发布模块前随机生成迁移引导密钥,注入 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 并显示在控制台。
|
||||
EOF
|
||||
}
|
||||
|
||||
@@ -223,6 +225,36 @@ sync_local_spacetime_install() {
|
||||
fi
|
||||
}
|
||||
|
||||
generate_migration_bootstrap_secret() {
|
||||
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
|
||||
}
|
||||
|
||||
prepare_migration_bootstrap_secret() {
|
||||
case "${MIGRATION_BOOTSTRAP_SECRET_MODE}" in
|
||||
auto)
|
||||
MIGRATION_BOOTSTRAP_SECRET="$(generate_migration_bootstrap_secret)"
|
||||
;;
|
||||
manual)
|
||||
if [[ "${#MIGRATION_BOOTSTRAP_SECRET}" -lt 16 ]]; then
|
||||
echo "[dev:rust] 迁移引导密钥至少需要 16 个字符。" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
disabled)
|
||||
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
|
||||
echo "[dev:rust] 未启用迁移引导密钥。"
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "[dev:rust] 未知迁移引导密钥模式: ${MIGRATION_BOOTSTRAP_SECRET_MODE}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
echo "[dev:rust] 迁移引导密钥: ${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
}
|
||||
|
||||
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
|
||||
SERVER_RS_DIR="${REPO_ROOT}/server-rs"
|
||||
@@ -244,6 +276,8 @@ API_SERVER_TIMEOUT_SECONDS="300"
|
||||
SKIP_SPACETIME=0
|
||||
SKIP_PUBLISH=0
|
||||
PRESERVE_DATABASE=0
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="auto"
|
||||
PIDS=()
|
||||
NAMES=()
|
||||
|
||||
@@ -334,6 +368,16 @@ while [[ $# -gt 0 ]]; do
|
||||
PRESERVE_DATABASE=1
|
||||
shift
|
||||
;;
|
||||
--migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET="${2:?缺少 --migration-bootstrap-secret 的值}"
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="manual"
|
||||
shift 2
|
||||
;;
|
||||
--no-migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="disabled"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "[dev:rust] 未知参数: $1" >&2
|
||||
usage >&2
|
||||
@@ -417,6 +461,7 @@ fi
|
||||
if [[ "${SKIP_PUBLISH}" -ne 1 ]]; then
|
||||
echo "[dev:rust] 等待 SpacetimeDB 就绪"
|
||||
wait_for_spacetime "${SPACETIME_SERVER}" "${SPACETIME_TIMEOUT_SECONDS}" "${SPACETIME_ROOT_DIR}" "${PIDS[0]:-}"
|
||||
prepare_migration_bootstrap_secret
|
||||
|
||||
PUBLISH_ARGS=(
|
||||
publish
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
callSpacetimeProcedureViaCli,
|
||||
ensureProcedureOk,
|
||||
parseArgs,
|
||||
} from './spacetime-migration-common.mjs';
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (!options.operatorIdentity) {
|
||||
throw new Error('必须传入 --operator-identity。');
|
||||
}
|
||||
|
||||
const input = {
|
||||
bootstrap_secret: options.bootstrapSecret || '',
|
||||
operator_identity_hex: options.operatorIdentity,
|
||||
note: options.note || '',
|
||||
};
|
||||
const result = await callSpacetimeProcedureViaCli(
|
||||
options,
|
||||
'authorize_database_migration_operator',
|
||||
input,
|
||||
);
|
||||
ensureProcedureOk(result);
|
||||
|
||||
console.log(
|
||||
`[spacetime:migration:operator] 已授权 ${result.operator_identity_hex ?? options.operatorIdentity}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[spacetime:migration:operator] ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
callSpacetimeProcedureAuto,
|
||||
ensureParentDir,
|
||||
ensureProcedureOk,
|
||||
parseArgs,
|
||||
} from './spacetime-migration-common.mjs';
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (!options.out) {
|
||||
throw new Error('必须传入 --out。');
|
||||
}
|
||||
|
||||
const input = {
|
||||
include_tables: options.includeTables,
|
||||
};
|
||||
const result = await callSpacetimeProcedureAuto(
|
||||
options,
|
||||
'export_database_migration_to_file',
|
||||
input,
|
||||
);
|
||||
ensureProcedureOk(result);
|
||||
|
||||
if (typeof result.migration_json !== 'string' || result.migration_json.trim() === '') {
|
||||
throw new Error('导出 procedure 没有返回 migration_json。');
|
||||
}
|
||||
|
||||
const outPath = path.resolve(options.out);
|
||||
await ensureParentDir(outPath);
|
||||
await writeFile(outPath, result.migration_json, 'utf8');
|
||||
|
||||
console.log(`[spacetime:migration:export] 已写入 ${outPath}`);
|
||||
printTableStats(result.table_stats);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[spacetime:migration:export] ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
function printTableStats(tableStats) {
|
||||
if (!Array.isArray(tableStats) || tableStats.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = tableStats.map((stat) => ({
|
||||
table: stat.table_name,
|
||||
exported: stat.exported_row_count,
|
||||
}));
|
||||
console.table(rows);
|
||||
}
|
||||
@@ -0,0 +1,179 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
assertReadableFile,
|
||||
callSpacetimeProcedure,
|
||||
callSpacetimeProcedureViaCli,
|
||||
createSpacetimeWebIdentity,
|
||||
ensureProcedureOk,
|
||||
parseArgs,
|
||||
} from './spacetime-migration-common.mjs';
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (!options.in) {
|
||||
throw new Error('必须传入 --in。');
|
||||
}
|
||||
if (options.incremental === true && options.replaceExisting === true) {
|
||||
throw new Error('--incremental 不能和 --replace-existing 同时使用。');
|
||||
}
|
||||
|
||||
const inPath = path.resolve(options.in);
|
||||
await assertReadableFile(inPath);
|
||||
const migrationJson = await readFile(inPath, 'utf8');
|
||||
if (!migrationJson.trim()) {
|
||||
throw new Error(`迁移文件为空: ${inPath}`);
|
||||
}
|
||||
|
||||
const webOptions = await prepareWebImportOptions(options);
|
||||
let result;
|
||||
try {
|
||||
result = await importMigrationJsonDirect(webOptions, migrationJson);
|
||||
} finally {
|
||||
await revokeTemporaryWebIdentity(webOptions);
|
||||
}
|
||||
ensureProcedureOk(result);
|
||||
|
||||
console.log(
|
||||
`[spacetime:migration:import] ${options.dryRun ? 'dry-run 完成' : '导入完成'}: ${inPath}`,
|
||||
);
|
||||
printTableStats(result.table_stats);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[spacetime:migration:import] ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
async function prepareWebImportOptions(options) {
|
||||
if (options.token) {
|
||||
return { ...options, useHttp: true };
|
||||
}
|
||||
|
||||
const identity = await createSpacetimeWebIdentity(options);
|
||||
console.log(
|
||||
`[spacetime:migration:import] 已通过 Web API 创建临时 identity: ${identity.identity}`,
|
||||
);
|
||||
|
||||
const authorizeResult = await callSpacetimeProcedureViaCli(
|
||||
options,
|
||||
'authorize_database_migration_operator',
|
||||
{
|
||||
bootstrap_secret: options.bootstrapSecret || '',
|
||||
operator_identity_hex: identity.identity,
|
||||
note: options.note || 'temporary web api migration import',
|
||||
},
|
||||
);
|
||||
ensureProcedureOk(authorizeResult);
|
||||
console.log(`[spacetime:migration:import] 已授权临时 Web API identity`);
|
||||
|
||||
return {
|
||||
...options,
|
||||
token: identity.token,
|
||||
temporaryWebIdentity: identity.identity,
|
||||
useHttp: true,
|
||||
};
|
||||
}
|
||||
|
||||
async function importMigrationJsonDirect(options, migrationJson) {
|
||||
const includeTables = resolveImportIncludeTables(options, migrationJson);
|
||||
const procedureName =
|
||||
options.incremental === true
|
||||
? 'import_database_migration_incremental_from_file'
|
||||
: 'import_database_migration_from_file';
|
||||
const input = {
|
||||
migration_json: migrationJson,
|
||||
include_tables: includeTables,
|
||||
replace_existing: options.replaceExisting === true,
|
||||
dry_run: options.dryRun === true,
|
||||
};
|
||||
if (options.replaceExisting === true) {
|
||||
console.log(
|
||||
`[spacetime:migration:import] replace-existing 仅覆盖本次文件内的表: ${includeTables.join(', ') || '无'}`,
|
||||
);
|
||||
} else if (options.incremental === true) {
|
||||
console.log(`[spacetime:migration:import] 使用增量模式,已存在或冲突的行会跳过`);
|
||||
}
|
||||
return callSpacetimeProcedure(options, procedureName, input);
|
||||
}
|
||||
|
||||
function resolveImportIncludeTables(options, migrationJson) {
|
||||
if (options.replaceExisting !== true) {
|
||||
return options.includeTables;
|
||||
}
|
||||
|
||||
const migrationTables = readMigrationTableNames(migrationJson);
|
||||
if (options.includeTables.length === 0) {
|
||||
return migrationTables;
|
||||
}
|
||||
|
||||
const requestedTables = new Set(options.includeTables);
|
||||
return migrationTables.filter((tableName) => requestedTables.has(tableName));
|
||||
}
|
||||
|
||||
function readMigrationTableNames(migrationJson) {
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(migrationJson);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`迁移文件 JSON 解析失败: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
if (!payload || !Array.isArray(payload.tables)) {
|
||||
throw new Error('迁移文件缺少 tables 数组。');
|
||||
}
|
||||
|
||||
const tableNames = [];
|
||||
const seen = new Set();
|
||||
for (const table of payload.tables) {
|
||||
if (!table || typeof table.name !== 'string' || !table.name.trim()) {
|
||||
throw new Error('迁移文件 tables 内存在缺少 name 的表项。');
|
||||
}
|
||||
const tableName = table.name.trim();
|
||||
if (!seen.has(tableName)) {
|
||||
tableNames.push(tableName);
|
||||
seen.add(tableName);
|
||||
}
|
||||
}
|
||||
|
||||
return tableNames;
|
||||
}
|
||||
|
||||
async function revokeTemporaryWebIdentity(options) {
|
||||
if (!options.temporaryWebIdentity) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const revokeResult = await callSpacetimeProcedure(
|
||||
options,
|
||||
'revoke_database_migration_operator',
|
||||
{ operator_identity_hex: options.temporaryWebIdentity },
|
||||
);
|
||||
ensureProcedureOk(revokeResult);
|
||||
console.log(`[spacetime:migration:import] 已撤销临时 Web API identity`);
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`[spacetime:migration:import] 撤销临时 Web API identity 失败: ${
|
||||
error instanceof Error ? error.message : String(error)
|
||||
}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function printTableStats(tableStats) {
|
||||
if (!Array.isArray(tableStats) || tableStats.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = tableStats.map((stat) => ({
|
||||
table: stat.table_name,
|
||||
imported: stat.imported_row_count,
|
||||
skipped: stat.skipped_row_count,
|
||||
}));
|
||||
console.table(rows);
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
import { spawn } from 'node:child_process';
|
||||
import { access, mkdir } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export function parseArgs(argv) {
|
||||
const options = {
|
||||
database:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE ||
|
||||
process.env.GENARRATIVE_SPACETIME_DATABASE ||
|
||||
'',
|
||||
bootstrapSecret: process.env.GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET || '',
|
||||
includeTables: [],
|
||||
operatorIdentity: process.env.GENARRATIVE_SPACETIME_MIGRATION_OPERATOR_IDENTITY || '',
|
||||
passthrough: [],
|
||||
note: '',
|
||||
server:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_SERVER ||
|
||||
process.env.GENARRATIVE_SPACETIME_SERVER ||
|
||||
'',
|
||||
serverUrl:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL ||
|
||||
process.env.GENARRATIVE_SPACETIME_SERVER_URL ||
|
||||
'',
|
||||
token:
|
||||
process.env.GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN ||
|
||||
process.env.GENARRATIVE_SPACETIME_TOKEN ||
|
||||
'',
|
||||
};
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
const readValue = (name) => {
|
||||
const value = argv[index + 1];
|
||||
if (!value || value.startsWith('--')) {
|
||||
throw new Error(`${name} 缺少参数值。`);
|
||||
}
|
||||
index += 1;
|
||||
return value;
|
||||
};
|
||||
|
||||
if (arg === '--server') {
|
||||
options.server = readValue(arg);
|
||||
} else if (arg === '--use-http') {
|
||||
options.useHttp = true;
|
||||
} else if (arg === '--server-url') {
|
||||
options.serverUrl = readValue(arg);
|
||||
} else if (arg === '--token') {
|
||||
options.token = readValue(arg);
|
||||
} else if (arg === '--bootstrap-secret') {
|
||||
options.bootstrapSecret = readValue(arg);
|
||||
} else if (arg === '--operator-identity') {
|
||||
options.operatorIdentity = readValue(arg);
|
||||
} else if (arg === '--note') {
|
||||
options.note = readValue(arg);
|
||||
} else if (arg === '--root-dir') {
|
||||
options.rootDir = readValue(arg);
|
||||
} else if (arg === '--database') {
|
||||
options.database = readValue(arg);
|
||||
} else if (arg === '--out') {
|
||||
options.out = readValue(arg);
|
||||
} else if (arg === '--in') {
|
||||
options.in = readValue(arg);
|
||||
} else if (arg === '--include') {
|
||||
options.includeTables = readValue(arg)
|
||||
.split(',')
|
||||
.map((value) => value.trim())
|
||||
.filter(Boolean);
|
||||
} else if (arg === '--replace-existing') {
|
||||
options.replaceExisting = true;
|
||||
} else if (arg === '--incremental') {
|
||||
options.incremental = true;
|
||||
} else if (arg === '--dry-run') {
|
||||
options.dryRun = true;
|
||||
} else if (arg === '--anonymous' || arg === '--no-config') {
|
||||
options.passthrough.push(arg);
|
||||
} else {
|
||||
throw new Error(`未知参数: ${arg}`);
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
|
||||
export function buildSpacetimeCallArgs(options, procedureName, input) {
|
||||
if (!options.database) {
|
||||
throw new Error('必须传入 --database。');
|
||||
}
|
||||
|
||||
const args = [];
|
||||
if (options.rootDir) {
|
||||
args.push(`--root-dir=${options.rootDir}`);
|
||||
}
|
||||
args.push('call');
|
||||
if (options.server) {
|
||||
args.push('-s', options.server);
|
||||
}
|
||||
args.push(...options.passthrough);
|
||||
args.push(options.database, procedureName, JSON.stringify(input), '-y');
|
||||
return args;
|
||||
}
|
||||
|
||||
export async function callSpacetimeProcedure(options, procedureName, input) {
|
||||
if (!options.database) {
|
||||
throw new Error('必须传入 --database,或设置 GENARRATIVE_SPACETIME_DATABASE。');
|
||||
}
|
||||
|
||||
const serverUrl = resolveServerUrl(options).replace(/\/+$/u, '');
|
||||
const url = `${serverUrl}/v1/database/${encodeURIComponent(options.database)}/call/${encodeURIComponent(procedureName)}`;
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (options.token) {
|
||||
headers.Authorization = `Bearer ${options.token}`;
|
||||
}
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify([input]),
|
||||
});
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`SpacetimeDB HTTP 请求失败: ${url}; ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(
|
||||
`SpacetimeDB HTTP ${response.status}: ${trimPreview(text)}${buildHttpAuthHint(text)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return parseProcedureResult(text);
|
||||
}
|
||||
|
||||
export async function createSpacetimeWebIdentity(options) {
|
||||
const serverUrl = resolveServerUrl(options).replace(/\/+$/u, '');
|
||||
const url = `${serverUrl}/v1/identity`;
|
||||
const headers = {
|
||||
Accept: 'application/json',
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
let response;
|
||||
try {
|
||||
response = await fetch(url, { method: 'POST', headers });
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`SpacetimeDB identity 请求失败: ${url}; ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const text = await response.text();
|
||||
if (!response.ok) {
|
||||
throw new Error(`SpacetimeDB identity HTTP ${response.status}: ${trimPreview(text)}`);
|
||||
}
|
||||
|
||||
let payload;
|
||||
try {
|
||||
payload = JSON.parse(text);
|
||||
} catch (error) {
|
||||
throw new Error(
|
||||
`SpacetimeDB identity 响应不是合法 JSON: ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
}
|
||||
|
||||
const identity =
|
||||
payload.identity ?? payload.Identity ?? payload.identity_hex ?? payload.identityHex;
|
||||
const token = payload.token ?? payload.Token;
|
||||
if (typeof identity !== 'string' || typeof token !== 'string') {
|
||||
throw new Error(`SpacetimeDB identity 响应缺少 identity/token: ${trimPreview(text)}`);
|
||||
}
|
||||
|
||||
return { identity, token };
|
||||
}
|
||||
|
||||
export async function callSpacetimeProcedureAuto(options, procedureName, input) {
|
||||
if (options.useHttp) {
|
||||
return callSpacetimeProcedure(options, procedureName, input);
|
||||
}
|
||||
|
||||
return callSpacetimeProcedureViaCli(options, procedureName, input);
|
||||
}
|
||||
|
||||
export async function callSpacetimeProcedureViaCli(options, procedureName, input) {
|
||||
const args = buildSpacetimeCallArgs(options, procedureName, input);
|
||||
const output = await runSpacetimeCli(args);
|
||||
return parseProcedureResult(output);
|
||||
}
|
||||
|
||||
export function parseProcedureResult(output) {
|
||||
const candidates = [];
|
||||
const trimmed = output.trim();
|
||||
if (trimmed) {
|
||||
candidates.push(trimmed);
|
||||
}
|
||||
|
||||
for (const line of output.split(/\r?\n/u)) {
|
||||
const value = line.trim();
|
||||
if (value.startsWith('{') || value.startsWith('[')) {
|
||||
candidates.push(value);
|
||||
}
|
||||
}
|
||||
|
||||
for (const candidate of candidates) {
|
||||
try {
|
||||
return normalizeProcedureResult(JSON.parse(candidate));
|
||||
} catch {
|
||||
// SpacetimeDB CLI 在不同版本中可能附带说明文本,继续尝试后续候选。
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`无法解析 procedure 返回值: ${trimmed}`);
|
||||
}
|
||||
|
||||
export function ensureProcedureOk(result) {
|
||||
if (!result.ok) {
|
||||
throw new Error(result.error_message ?? '迁移 procedure 返回失败。');
|
||||
}
|
||||
}
|
||||
|
||||
export async function ensureParentDir(filePath) {
|
||||
await mkdir(path.dirname(path.resolve(filePath)), { recursive: true });
|
||||
}
|
||||
|
||||
export async function assertReadableFile(filePath) {
|
||||
await access(path.resolve(filePath));
|
||||
}
|
||||
|
||||
function normalizeProcedureResult(value) {
|
||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||
return value;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
return normalizeSatsProduct(value);
|
||||
}
|
||||
|
||||
throw new Error('procedure 返回值不是对象。');
|
||||
}
|
||||
|
||||
function normalizeSatsProduct(value) {
|
||||
if (value.length === 3) {
|
||||
return {
|
||||
ok: normalizeSatsValue(value[0]),
|
||||
operator_identity_hex: normalizeSatsOption(value[1]),
|
||||
error_message: normalizeSatsOption(value[2]),
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
ok: normalizeSatsValue(value[0]),
|
||||
schema_version: normalizeSatsValue(value[1]),
|
||||
migration_json: normalizeSatsOption(value[2]),
|
||||
table_stats: normalizeTableStats(value[3]),
|
||||
error_message: normalizeSatsOption(value[4]),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSatsValue(value) {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((item) => normalizeSatsValue(item));
|
||||
}
|
||||
|
||||
if (value && typeof value === 'object') {
|
||||
return Object.fromEntries(
|
||||
Object.entries(value).map(([key, entry]) => [key, normalizeSatsValue(entry)]),
|
||||
);
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
function normalizeSatsOption(value) {
|
||||
if (Array.isArray(value)) {
|
||||
if (value.length === 2 && value[0] === 0) {
|
||||
return normalizeSatsValue(value[1]);
|
||||
}
|
||||
if (value.length === 0 || value[0] === 1) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return normalizeSatsValue(value);
|
||||
}
|
||||
|
||||
function normalizeTableStats(value) {
|
||||
if (!Array.isArray(value)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return value.map((entry) => {
|
||||
if (entry && typeof entry === 'object' && !Array.isArray(entry)) {
|
||||
return normalizeSatsValue(entry);
|
||||
}
|
||||
|
||||
if (Array.isArray(entry)) {
|
||||
return {
|
||||
table_name: normalizeSatsValue(entry[0]),
|
||||
exported_row_count: normalizeSatsValue(entry[1]),
|
||||
imported_row_count: normalizeSatsValue(entry[2]),
|
||||
skipped_row_count: normalizeSatsValue(entry[3]),
|
||||
};
|
||||
}
|
||||
|
||||
return entry;
|
||||
});
|
||||
}
|
||||
|
||||
export function resolveServerUrl(options) {
|
||||
if (options.serverUrl) {
|
||||
return options.serverUrl;
|
||||
}
|
||||
|
||||
const server = (options.server || 'maincloud').trim();
|
||||
if (server.startsWith('http://') || server.startsWith('https://')) {
|
||||
return server;
|
||||
}
|
||||
if (server === 'dev') {
|
||||
return 'http://127.0.0.1:3101';
|
||||
}
|
||||
if (server === 'local') {
|
||||
return 'http://127.0.0.1:3000';
|
||||
}
|
||||
if (!server || server === 'maincloud') {
|
||||
return 'https://maincloud.spacetimedb.com';
|
||||
}
|
||||
|
||||
throw new Error(`未知 SpacetimeDB server: ${server}。请改用 --server-url 显式传入地址。`);
|
||||
}
|
||||
|
||||
function trimPreview(text) {
|
||||
const trimmed = text.trim();
|
||||
if (trimmed.length <= 4000) {
|
||||
return trimmed;
|
||||
}
|
||||
|
||||
return `${trimmed.slice(0, 4000)}...`;
|
||||
}
|
||||
|
||||
function buildHttpAuthHint(text) {
|
||||
if (!text.includes('InvalidSignature') && !text.includes('TokenError')) {
|
||||
return '';
|
||||
}
|
||||
|
||||
return '。提示:这里需要 SpacetimeDB 客户端连接 token,不是 `spacetime login show --token` 输出的 CLI 登录 token;授权/撤销请直接使用 CLI 登录态,不要传 --token。';
|
||||
}
|
||||
|
||||
function runSpacetimeCli(args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const child = spawn('spacetime', args, {
|
||||
cwd: process.cwd(),
|
||||
shell: false,
|
||||
stdio: ['ignore', 'pipe', 'pipe'],
|
||||
});
|
||||
let output = '';
|
||||
|
||||
child.stdout.on('data', (chunk) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
child.stderr.on('data', (chunk) => {
|
||||
output += chunk.toString();
|
||||
});
|
||||
child.on('error', reject);
|
||||
child.on('exit', (code, signal) => {
|
||||
if (signal) {
|
||||
reject(new Error(`spacetime call 被信号中断: ${signal}`));
|
||||
return;
|
||||
}
|
||||
if (code !== 0) {
|
||||
reject(new Error(`spacetime call 失败,退出码 ${code}: ${trimPreview(output)}`));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(output);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -7,6 +7,8 @@ SERVER_RS_DIR="${REPO_ROOT}/server-rs"
|
||||
MODULE_PATH="${SERVER_RS_DIR}/target/wasm32-unknown-unknown/release/spacetime_module.wasm"
|
||||
SPACETIME_SERVER_ALIAS="maincloud"
|
||||
CLEAR_DATABASE=0
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="auto"
|
||||
|
||||
load_env_file() {
|
||||
local env_file="$1"
|
||||
@@ -39,13 +41,45 @@ usage() {
|
||||
npm run spacetime:publish:maincloud
|
||||
npm run spacetime:publish:maincloud -- --database <database>
|
||||
npm run spacetime:publish:maincloud -- --clear-database
|
||||
npm run spacetime:publish:maincloud -- --no-migration-bootstrap-secret
|
||||
|
||||
说明:
|
||||
发布 server-rs/crates/spacetime-module 到 SpacetimeDB Maincloud。
|
||||
数据库名优先读取 --database,其次读取 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE。
|
||||
默认在构建 wasm 前随机生成迁移引导密钥,注入 GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET 并显示在控制台。
|
||||
EOF
|
||||
}
|
||||
|
||||
generate_migration_bootstrap_secret() {
|
||||
node -e 'const crypto = require("crypto"); process.stdout.write(crypto.randomBytes(32).toString("hex"));'
|
||||
}
|
||||
|
||||
prepare_migration_bootstrap_secret() {
|
||||
case "${MIGRATION_BOOTSTRAP_SECRET_MODE}" in
|
||||
auto)
|
||||
MIGRATION_BOOTSTRAP_SECRET="$(generate_migration_bootstrap_secret)"
|
||||
;;
|
||||
manual)
|
||||
if [[ "${#MIGRATION_BOOTSTRAP_SECRET}" -lt 16 ]]; then
|
||||
echo "[spacetime:maincloud] 迁移引导密钥至少需要 16 个字符。" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
disabled)
|
||||
unset GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET
|
||||
echo "[spacetime:maincloud] 未启用迁移引导密钥。"
|
||||
return
|
||||
;;
|
||||
*)
|
||||
echo "[spacetime:maincloud] 未知迁移引导密钥模式: ${MIGRATION_BOOTSTRAP_SECRET_MODE}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
export GENARRATIVE_SPACETIME_MIGRATION_BOOTSTRAP_SECRET="${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
echo "[spacetime:maincloud] 迁移引导密钥: ${MIGRATION_BOOTSTRAP_SECRET}"
|
||||
}
|
||||
|
||||
load_env_file "${REPO_ROOT}/.env"
|
||||
load_env_file "${REPO_ROOT}/.env.local"
|
||||
|
||||
@@ -70,6 +104,16 @@ while [[ $# -gt 0 ]]; do
|
||||
CLEAR_DATABASE=1
|
||||
shift
|
||||
;;
|
||||
--migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET="${2:?缺少 --migration-bootstrap-secret 的值}"
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="manual"
|
||||
shift 2
|
||||
;;
|
||||
--no-migration-bootstrap-secret)
|
||||
MIGRATION_BOOTSTRAP_SECRET=""
|
||||
MIGRATION_BOOTSTRAP_SECRET_MODE="disabled"
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
echo "[spacetime:maincloud] 未知参数: $1" >&2
|
||||
usage >&2
|
||||
@@ -89,11 +133,18 @@ if ! command -v cargo >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v node >/dev/null 2>&1; then
|
||||
echo "[spacetime:maincloud] 缺少 node 命令,无法生成迁移引导密钥。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! command -v spacetime >/dev/null 2>&1; then
|
||||
echo "[spacetime:maincloud] 缺少 spacetime CLI,请先安装并登录 Maincloud。" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
prepare_migration_bootstrap_secret
|
||||
|
||||
echo "[spacetime:maincloud] 构建 spacetime-module wasm"
|
||||
cargo build \
|
||||
--manifest-path "${SERVER_RS_DIR}/Cargo.toml" \
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import {
|
||||
callSpacetimeProcedureViaCli,
|
||||
ensureProcedureOk,
|
||||
parseArgs,
|
||||
} from './spacetime-migration-common.mjs';
|
||||
|
||||
try {
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
if (!options.operatorIdentity) {
|
||||
throw new Error('必须传入 --operator-identity。');
|
||||
}
|
||||
|
||||
const input = {
|
||||
operator_identity_hex: options.operatorIdentity,
|
||||
};
|
||||
const result = await callSpacetimeProcedureViaCli(
|
||||
options,
|
||||
'revoke_database_migration_operator',
|
||||
input,
|
||||
);
|
||||
ensureProcedureOk(result);
|
||||
|
||||
console.log(
|
||||
`[spacetime:migration:operator] 已撤销 ${result.operator_identity_hex ?? options.operatorIdentity}`,
|
||||
);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`[spacetime:migration:operator] ${error instanceof Error ? error.message : String(error)}`,
|
||||
);
|
||||
process.exit(1);
|
||||
}
|
||||
Generated
+1
@@ -2698,6 +2698,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"shared-kernel",
|
||||
"spacetimedb",
|
||||
"spacetimedb-lib",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
||||
@@ -65,7 +65,6 @@ const DATABASE_OVERVIEW_TABLES: &[&str] = &[
|
||||
"big_fish_creation_session",
|
||||
"big_fish_agent_message",
|
||||
"big_fish_asset_slot",
|
||||
"big_fish_runtime_run",
|
||||
"puzzle_work_profile",
|
||||
"puzzle_agent_session",
|
||||
"puzzle_agent_message",
|
||||
|
||||
@@ -33,9 +33,9 @@ use crate::{
|
||||
auth_public_user::{get_public_user_by_code, get_public_user_by_id},
|
||||
auth_sessions::auth_sessions,
|
||||
big_fish::{
|
||||
create_big_fish_session, delete_big_fish_work, execute_big_fish_action, get_big_fish_run,
|
||||
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, start_big_fish_run,
|
||||
stream_big_fish_message, submit_big_fish_input, submit_big_fish_message,
|
||||
create_big_fish_session, delete_big_fish_work, execute_big_fish_action,
|
||||
get_big_fish_session, get_big_fish_works, list_big_fish_gallery, stream_big_fish_message,
|
||||
submit_big_fish_message,
|
||||
},
|
||||
character_animation_assets::{
|
||||
generate_character_animation, get_character_animation_job, get_character_workflow_cache,
|
||||
@@ -575,27 +575,6 @@ pub fn build_router(state: AppState) -> Router {
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/sessions/{session_id}/runs",
|
||||
post(start_big_fish_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/runs/{run_id}",
|
||||
get(get_big_fish_run).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/big-fish/runs/{run_id}/input",
|
||||
post(submit_big_fish_input).route_layer(middleware::from_fn_with_state(
|
||||
state.clone(),
|
||||
require_bearer_auth,
|
||||
)),
|
||||
)
|
||||
.route(
|
||||
"/api/runtime/puzzle/agent/sessions",
|
||||
post(create_puzzle_agent_session).route_layer(middleware::from_fn_with_state(
|
||||
@@ -1445,6 +1424,34 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn auth_login_options_keeps_password_entry_when_external_methods_disabled() {
|
||||
let app = build_router(AppState::new(AppConfig::default()).expect("state should build"));
|
||||
let response = app
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
.uri("/api/auth/login-options")
|
||||
.body(Body::empty())
|
||||
.expect("request should build"),
|
||||
)
|
||||
.await
|
||||
.expect("request should succeed");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
let body = response
|
||||
.into_body()
|
||||
.collect()
|
||||
.await
|
||||
.expect("body should collect")
|
||||
.to_bytes();
|
||||
let payload: Value = serde_json::from_slice(&body).expect("body should be valid json");
|
||||
|
||||
assert_eq!(
|
||||
payload["availableLoginMethods"],
|
||||
serde_json::json!(["password"])
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn send_phone_code_returns_mock_cooldown_and_expire_seconds() {
|
||||
let config = AppConfig {
|
||||
|
||||
@@ -23,10 +23,8 @@ use shared_contracts::big_fish::{
|
||||
BigFishActionResponse, BigFishAgentMessageResponse, BigFishAnchorItemResponse,
|
||||
BigFishAnchorPackResponse, BigFishAssetCoverageResponse, BigFishAssetSlotResponse,
|
||||
BigFishBackgroundBlueprintResponse, BigFishGameDraftResponse, BigFishLevelBlueprintResponse,
|
||||
BigFishRunResponse, BigFishRuntimeEntityResponse, BigFishRuntimeParamsResponse,
|
||||
BigFishRuntimeSnapshotResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
|
||||
BigFishVector2Response, CreateBigFishSessionRequest, ExecuteBigFishActionRequest,
|
||||
SendBigFishMessageRequest, SubmitBigFishInputRequest,
|
||||
BigFishRuntimeParamsResponse, BigFishSessionResponse, BigFishSessionSnapshotResponse,
|
||||
CreateBigFishSessionRequest, ExecuteBigFishActionRequest, SendBigFishMessageRequest,
|
||||
};
|
||||
use shared_contracts::big_fish_works::{BigFishWorkSummaryResponse, BigFishWorksResponse};
|
||||
use shared_kernel::{build_prefixed_uuid_id, format_timestamp_micros};
|
||||
@@ -34,10 +32,8 @@ use spacetime_client::{
|
||||
BigFishAgentMessageRecord, BigFishAnchorItemRecord, BigFishAnchorPackRecord,
|
||||
BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, BigFishAssetSlotRecord,
|
||||
BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord, BigFishLevelBlueprintRecord,
|
||||
BigFishMessageSubmitRecordInput, BigFishRunInputSubmitRecordInput, BigFishRunStartRecordInput,
|
||||
BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRecord,
|
||||
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record,
|
||||
BigFishWorkSummaryRecord, SpacetimeClientError,
|
||||
BigFishMessageSubmitRecordInput, BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput,
|
||||
BigFishSessionRecord, BigFishWorkSummaryRecord, SpacetimeClientError,
|
||||
};
|
||||
use tokio::time::sleep;
|
||||
|
||||
@@ -577,99 +573,6 @@ pub async fn execute_big_fish_action(
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn start_big_fish_run(
|
||||
State(state): State<AppState>,
|
||||
Path(session_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, &session_id, "sessionId")?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.start_big_fish_run(BigFishRunStartRecordInput {
|
||||
run_id: build_prefixed_uuid_id("big-fish-run-"),
|
||||
session_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
started_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
BigFishRunResponse {
|
||||
run: map_big_fish_runtime_response(run),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn get_big_fish_run(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.get_big_fish_run(run_id, authenticated.claims().user_id().to_string())
|
||||
.await
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
BigFishRunResponse {
|
||||
run: map_big_fish_runtime_response(run),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
pub async fn submit_big_fish_input(
|
||||
State(state): State<AppState>,
|
||||
Path(run_id): Path<String>,
|
||||
Extension(request_context): Extension<RequestContext>,
|
||||
Extension(authenticated): Extension<AuthenticatedAccessToken>,
|
||||
payload: Result<Json<SubmitBigFishInputRequest>, JsonRejection>,
|
||||
) -> Result<Json<Value>, Response> {
|
||||
let Json(payload) = payload.map_err(|error| {
|
||||
big_fish_error_response(
|
||||
&request_context,
|
||||
AppError::from_status(StatusCode::BAD_REQUEST).with_details(json!({
|
||||
"provider": "big-fish",
|
||||
"message": error.body_text(),
|
||||
})),
|
||||
)
|
||||
})?;
|
||||
ensure_non_empty(&request_context, &run_id, "runId")?;
|
||||
|
||||
let run = state
|
||||
.spacetime_client()
|
||||
.submit_big_fish_input(BigFishRunInputSubmitRecordInput {
|
||||
run_id,
|
||||
owner_user_id: authenticated.claims().user_id().to_string(),
|
||||
input_x: payload.x,
|
||||
input_y: payload.y,
|
||||
submitted_at_micros: current_utc_micros(),
|
||||
})
|
||||
.await
|
||||
.map_err(|error| {
|
||||
big_fish_error_response(&request_context, map_big_fish_client_error(error))
|
||||
})?;
|
||||
|
||||
Ok(json_success_body(
|
||||
Some(&request_context),
|
||||
BigFishRunResponse {
|
||||
run: map_big_fish_runtime_response(run),
|
||||
},
|
||||
))
|
||||
}
|
||||
|
||||
fn map_big_fish_session_response(session: BigFishSessionRecord) -> BigFishSessionSnapshotResponse {
|
||||
BigFishSessionSnapshotResponse {
|
||||
session_id: session.session_id,
|
||||
@@ -910,32 +813,6 @@ fn map_big_fish_agent_message_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_runtime_response(run: BigFishRuntimeRecord) -> BigFishRuntimeSnapshotResponse {
|
||||
BigFishRuntimeSnapshotResponse {
|
||||
run_id: run.run_id,
|
||||
session_id: run.session_id,
|
||||
status: run.status,
|
||||
tick: run.tick,
|
||||
player_level: run.player_level,
|
||||
win_level: run.win_level,
|
||||
leader_entity_id: run.leader_entity_id,
|
||||
owned_entities: run
|
||||
.owned_entities
|
||||
.into_iter()
|
||||
.map(map_big_fish_entity_response)
|
||||
.collect(),
|
||||
wild_entities: run
|
||||
.wild_entities
|
||||
.into_iter()
|
||||
.map(map_big_fish_entity_response)
|
||||
.collect(),
|
||||
camera_center: map_big_fish_vector_response(run.camera_center),
|
||||
last_input: map_big_fish_vector_response(run.last_input),
|
||||
event_log: run.event_log,
|
||||
updated_at: run.updated_at,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_work_summary_response(
|
||||
item: BigFishWorkSummaryRecord,
|
||||
) -> BigFishWorkSummaryResponse {
|
||||
@@ -957,25 +834,6 @@ fn map_big_fish_work_summary_response(
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_entity_response(
|
||||
entity: BigFishRuntimeEntityRecord,
|
||||
) -> BigFishRuntimeEntityResponse {
|
||||
BigFishRuntimeEntityResponse {
|
||||
entity_id: entity.entity_id,
|
||||
level: entity.level,
|
||||
position: map_big_fish_vector_response(entity.position),
|
||||
radius: entity.radius,
|
||||
offscreen_seconds: entity.offscreen_seconds,
|
||||
}
|
||||
}
|
||||
|
||||
fn map_big_fish_vector_response(vector: BigFishVector2Record) -> BigFishVector2Response {
|
||||
BigFishVector2Response {
|
||||
x: vector.x,
|
||||
y: vector.y,
|
||||
}
|
||||
}
|
||||
|
||||
fn build_big_fish_welcome_text(seed_text: &str) -> String {
|
||||
if seed_text.trim().is_empty() {
|
||||
return "我会先帮你确定大鱼吃小鱼的核心锚点。可以从主题生态、成长阶梯或风险节奏开始。"
|
||||
@@ -1013,7 +871,8 @@ struct BigFishFormalAssetContext {
|
||||
|
||||
const BIG_FISH_TEXT_TO_IMAGE_MODEL: &str = "wan2.2-t2i-flash";
|
||||
const BIG_FISH_ENTITY_KIND: &str = "big_fish_session";
|
||||
const BIG_FISH_DEFAULT_NEGATIVE_PROMPT: &str = "文字,水印,logo,UI界面,对话框,边框,多余肢体,畸形鱼体,低清晰度,模糊,压缩噪点,现代摄影棚,写实照片背景";
|
||||
const BIG_FISH_DEFAULT_NEGATIVE_PROMPT: &str = "文字,水印,logo,UI界面,对话框,边框,多余肢体,畸形鱼体,低清晰度,模糊,压缩噪点,现代摄影棚,写实照片背景,复杂背景";
|
||||
const BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT: &str = "文字,水印,logo,UI界面,对话框,边框,多余肢体,畸形鱼体,低清晰度,模糊,压缩噪点,现代摄影棚,写实照片背景,场景背景,水草背景,气泡背景,多只主体,阴影地面";
|
||||
|
||||
async fn generate_big_fish_formal_asset(
|
||||
state: &AppState,
|
||||
@@ -1087,7 +946,7 @@ fn build_big_fish_formal_asset_context(
|
||||
Ok(BigFishFormalAssetContext {
|
||||
entity_id: session.session_id.clone(),
|
||||
prompt: build_big_fish_level_main_image_prompt(draft, level),
|
||||
negative_prompt: BIG_FISH_DEFAULT_NEGATIVE_PROMPT.to_string(),
|
||||
negative_prompt: BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.to_string(),
|
||||
size: "1024*1024".to_string(),
|
||||
asset_object_kind: "big_fish_level_main_image".to_string(),
|
||||
binding_slot: format!("level_main_image:{level_part}"),
|
||||
@@ -1114,7 +973,7 @@ fn build_big_fish_formal_asset_context(
|
||||
Ok(BigFishFormalAssetContext {
|
||||
entity_id: session.session_id.clone(),
|
||||
prompt: build_big_fish_level_motion_prompt(draft, level, motion_key),
|
||||
negative_prompt: BIG_FISH_DEFAULT_NEGATIVE_PROMPT.to_string(),
|
||||
negative_prompt: BIG_FISH_TRANSPARENT_ASSET_NEGATIVE_PROMPT.to_string(),
|
||||
size: "1024*1024".to_string(),
|
||||
asset_object_kind: "big_fish_level_motion".to_string(),
|
||||
binding_slot: format!("level_motion:{level_part}:{motion_key}"),
|
||||
@@ -1190,8 +1049,8 @@ fn build_big_fish_level_main_image_prompt(
|
||||
),
|
||||
format!("轮廓方向:{}。", level.silhouette_direction),
|
||||
format!("视觉提示词种子:{}。", level.visual_prompt_seed),
|
||||
"画面要求:单体游戏生物完整入镜,轮廓清晰,适合作为大鱼吃小鱼等级角色主图,2D 高完成度游戏插画,深海发光质感,中央构图。".to_string(),
|
||||
"不要出现 UI、文字、logo、水印、对话框或边框;背景保持干净的深海渐变或透明感,不要出现多只主体。".to_string(),
|
||||
"画面要求:按 RPG 角色资产口径生成,单体鱼形游戏生物完整入镜,轮廓清晰,中心构图,2D 高完成度游戏插画,深海发光质感。".to_string(),
|
||||
"背景要求:透明背景 PNG 风格,不出现任何场景、水草、气泡、阴影地面、UI、文字、logo、水印、对话框或边框;不要出现多只主体。".to_string(),
|
||||
]
|
||||
.join("")
|
||||
}
|
||||
@@ -1217,8 +1076,8 @@ fn build_big_fish_level_motion_prompt(
|
||||
),
|
||||
format!("动作提示词种子:{}。", level.motion_prompt_seed),
|
||||
format!("动作要求:{motion_text}"),
|
||||
"画面要求:单体生物完整入镜,轮廓清晰,动作方向明确,2D 高完成度游戏插画,适合作为 Big Fish 动作槽位的静态 keyframe。".to_string(),
|
||||
"不要出现 UI、文字、logo、水印、对话框或边框;不要生成序列帧拼图,不要出现多只主体。".to_string(),
|
||||
"画面要求:按 RPG 角色动画资产口径生成,单体鱼形生物完整入镜,轮廓清晰,动作方向明确,2D 高完成度游戏插画,适合作为 Big Fish 动作槽位的静态 keyframe。".to_string(),
|
||||
"背景要求:透明背景 PNG 风格,不出现任何场景、水草、气泡、阴影地面、UI、文字、logo、水印、对话框或边框;不要生成序列帧拼图,不要出现多只主体。".to_string(),
|
||||
]
|
||||
.join("")
|
||||
}
|
||||
@@ -1238,8 +1097,8 @@ fn build_big_fish_stage_background_prompt(draft: &BigFishGameDraftRecord) -> Str
|
||||
format!("安全操作区:{}。", background.safe_play_area_hint),
|
||||
format!("出生边缘:{}。", background.spawn_edge_hint),
|
||||
format!("背景提示词种子:{}。", background.background_prompt_seed),
|
||||
"画面要求:竖屏 9:16,中央 70% 保持清爽可读,边缘有深海生态层次和微弱生物光,适合作为大鱼吃小鱼运行态背景。".to_string(),
|
||||
"不要出现 UI、文字、logo、水印、对话框、边框或巨大主体遮挡;不要把中央操作区画得过暗或过复杂。".to_string(),
|
||||
"画面要求:竖屏 9:16,大场地,全屏运行态背景,中央 80% 保持开阔清爽,边缘只保留少量出生区环境提示。".to_string(),
|
||||
"元素要求:整体元素少,不出现大型主体、密集装饰、鱼群主角、UI、文字、logo、水印、对话框或边框;不要把中央操作区画得过暗或过复杂。".to_string(),
|
||||
]
|
||||
.join("")
|
||||
}
|
||||
@@ -1769,8 +1628,7 @@ fn big_fish_sse_error_event_message(message: String) -> Event {
|
||||
fn map_big_fish_client_error(error: SpacetimeClientError) -> AppError {
|
||||
let status = match &error {
|
||||
SpacetimeClientError::Procedure(message)
|
||||
if message.contains("big_fish_creation_session 不存在")
|
||||
|| message.contains("big_fish_runtime_run 不存在") =>
|
||||
if message.contains("big_fish_creation_session 不存在") =>
|
||||
{
|
||||
StatusCode::NOT_FOUND
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -215,7 +215,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn available_login_methods_keep_phone_then_wechat_order() {
|
||||
fn available_login_methods_keep_phone_password_wechat_order() {
|
||||
let methods = build_available_login_methods(true, true, true);
|
||||
|
||||
assert_eq!(
|
||||
@@ -228,6 +228,13 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn available_login_methods_keep_password_as_default_entry() {
|
||||
let methods = build_available_login_methods(false, true, false);
|
||||
|
||||
assert_eq!(methods, vec![AUTH_LOGIN_METHOD_PASSWORD.to_string()]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn password_entry_request_uses_camel_case_fields() {
|
||||
let payload = serde_json::to_value(PasswordEntryRequest {
|
||||
|
||||
@@ -26,13 +26,6 @@ pub struct ExecuteBigFishActionRequest {
|
||||
pub motion_key: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SubmitBigFishInputRequest {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishAnchorItemResponse {
|
||||
@@ -169,47 +162,6 @@ pub struct BigFishActionResponse {
|
||||
pub session: BigFishSessionSnapshotResponse,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishVector2Response {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishRuntimeEntityResponse {
|
||||
pub entity_id: String,
|
||||
pub level: u32,
|
||||
pub position: BigFishVector2Response,
|
||||
pub radius: f32,
|
||||
pub offscreen_seconds: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishRuntimeSnapshotResponse {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub status: String,
|
||||
pub tick: u64,
|
||||
pub player_level: u32,
|
||||
pub win_level: u32,
|
||||
pub leader_entity_id: Option<String>,
|
||||
pub owned_entities: Vec<BigFishRuntimeEntityResponse>,
|
||||
pub wild_entities: Vec<BigFishRuntimeEntityResponse>,
|
||||
pub camera_center: BigFishVector2Response,
|
||||
pub last_input: BigFishVector2Response,
|
||||
pub event_log: Vec<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct BigFishRunResponse {
|
||||
pub run: BigFishRuntimeSnapshotResponse,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -251,76 +251,4 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn start_big_fish_run(
|
||||
&self,
|
||||
input: BigFishRunStartRecordInput,
|
||||
) -> Result<BigFishRuntimeRecord, SpacetimeClientError> {
|
||||
let procedure_input = BigFishRunStartInput {
|
||||
run_id: input.run_id,
|
||||
session_id: input.session_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
started_at_micros: input.started_at_micros,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.start_big_fish_run_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_big_fish_run_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn submit_big_fish_input(
|
||||
&self,
|
||||
input: BigFishRunInputSubmitRecordInput,
|
||||
) -> Result<BigFishRuntimeRecord, SpacetimeClientError> {
|
||||
let procedure_input = BigFishRunInputSubmitInput {
|
||||
run_id: input.run_id,
|
||||
owner_user_id: input.owner_user_id,
|
||||
input_x: input.input_x,
|
||||
input_y: input.input_y,
|
||||
submitted_at_micros: input.submitted_at_micros,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection.procedures().submit_big_fish_input_then(
|
||||
procedure_input,
|
||||
move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_big_fish_run_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
},
|
||||
);
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn get_big_fish_run(
|
||||
&self,
|
||||
run_id: String,
|
||||
owner_user_id: String,
|
||||
) -> Result<BigFishRuntimeRecord, SpacetimeClientError> {
|
||||
let procedure_input = BigFishRunGetInput {
|
||||
run_id,
|
||||
owner_user_id,
|
||||
};
|
||||
|
||||
self.call_after_connect(move |connection, sender| {
|
||||
connection
|
||||
.procedures()
|
||||
.get_big_fish_run_then(procedure_input, move |_, result| {
|
||||
let mapped = result
|
||||
.map_err(|error| SpacetimeClientError::Procedure(error.to_string()))
|
||||
.and_then(map_big_fish_run_procedure_result);
|
||||
send_once(&sender, mapped);
|
||||
});
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,10 +10,8 @@ pub use mapper::{
|
||||
BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput,
|
||||
BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord,
|
||||
BigFishLevelBlueprintRecord, BigFishMessageFinalizeRecordInput,
|
||||
BigFishMessageSubmitRecordInput, BigFishRunInputSubmitRecordInput, BigFishRunStartRecordInput,
|
||||
BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRecord,
|
||||
BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record,
|
||||
BigFishWorkSummaryRecord, CustomWorldAgentActionExecuteRecord,
|
||||
BigFishMessageSubmitRecordInput, BigFishRuntimeParamsRecord, BigFishSessionCreateRecordInput,
|
||||
BigFishSessionRecord, BigFishWorkSummaryRecord, CustomWorldAgentActionExecuteRecord,
|
||||
CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentCheckpointRecord,
|
||||
CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord,
|
||||
CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationProgressRecordInput,
|
||||
|
||||
@@ -1262,26 +1262,6 @@ pub(crate) fn map_big_fish_works_procedure_result(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_run_procedure_result(
|
||||
result: BigFishRunProcedureResult,
|
||||
) -> Result<BigFishRuntimeRecord, SpacetimeClientError> {
|
||||
if !result.ok {
|
||||
return Err(SpacetimeClientError::Procedure(
|
||||
result
|
||||
.error_message
|
||||
.unwrap_or_else(|| "SpacetimeDB procedure 返回未知错误".to_string()),
|
||||
));
|
||||
}
|
||||
|
||||
let run = result.run.ok_or_else(|| {
|
||||
SpacetimeClientError::Procedure(
|
||||
"SpacetimeDB procedure 未返回 big fish runtime 快照".to_string(),
|
||||
)
|
||||
})?;
|
||||
|
||||
Ok(map_big_fish_runtime_snapshot(run))
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_procedure_result(
|
||||
result: StorySessionProcedureResult,
|
||||
) -> Result<StorySessionResultRecord, SpacetimeClientError> {
|
||||
@@ -2492,53 +2472,6 @@ pub(crate) fn map_big_fish_agent_message_snapshot(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_runtime_snapshot(
|
||||
snapshot: BigFishRuntimeSnapshot,
|
||||
) -> BigFishRuntimeRecord {
|
||||
BigFishRuntimeRecord {
|
||||
run_id: snapshot.run_id,
|
||||
session_id: snapshot.session_id,
|
||||
status: format_big_fish_run_status(snapshot.status).to_string(),
|
||||
tick: snapshot.tick,
|
||||
player_level: snapshot.player_level,
|
||||
win_level: snapshot.win_level,
|
||||
leader_entity_id: snapshot.leader_entity_id,
|
||||
owned_entities: snapshot
|
||||
.owned_entities
|
||||
.into_iter()
|
||||
.map(map_big_fish_runtime_entity)
|
||||
.collect(),
|
||||
wild_entities: snapshot
|
||||
.wild_entities
|
||||
.into_iter()
|
||||
.map(map_big_fish_runtime_entity)
|
||||
.collect(),
|
||||
camera_center: map_big_fish_vector2(snapshot.camera_center),
|
||||
last_input: map_big_fish_vector2(snapshot.last_input),
|
||||
event_log: snapshot.event_log,
|
||||
updated_at: format_timestamp_micros(snapshot.updated_at_micros),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_runtime_entity(
|
||||
snapshot: BigFishRuntimeEntity,
|
||||
) -> BigFishRuntimeEntityRecord {
|
||||
BigFishRuntimeEntityRecord {
|
||||
entity_id: snapshot.entity_id,
|
||||
level: snapshot.level,
|
||||
position: map_big_fish_vector2(snapshot.position),
|
||||
radius: snapshot.radius,
|
||||
offscreen_seconds: snapshot.offscreen_seconds,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_big_fish_vector2(snapshot: BigFishVector2) -> BigFishVector2Record {
|
||||
BigFishVector2Record {
|
||||
x: snapshot.x,
|
||||
y: snapshot.y,
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn map_story_session_snapshot(snapshot: StorySessionSnapshot) -> StorySessionRecord {
|
||||
StorySessionRecord {
|
||||
story_session_id: snapshot.story_session_id,
|
||||
@@ -3244,14 +3177,6 @@ pub(crate) fn format_big_fish_asset_status(value: BigFishAssetStatus) -> &'stati
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_big_fish_run_status(value: BigFishRunStatus) -> &'static str {
|
||||
match value {
|
||||
BigFishRunStatus::Running => "running",
|
||||
BigFishRunStatus::Won => "won",
|
||||
BigFishRunStatus::Failed => "failed",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn format_custom_world_theme_mode(value: DomainCustomWorldThemeMode) -> &'static str {
|
||||
match value {
|
||||
DomainCustomWorldThemeMode::Martial => "martial",
|
||||
@@ -4503,23 +4428,6 @@ pub struct BigFishAssetGenerateRecordInput {
|
||||
pub generated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishRunStartRecordInput {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub started_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishRunInputSubmitRecordInput {
|
||||
pub run_id: String,
|
||||
pub owner_user_id: String,
|
||||
pub input_x: f32,
|
||||
pub input_y: f32,
|
||||
pub submitted_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct BigFishAnchorItemRecord {
|
||||
pub key: String,
|
||||
@@ -4652,38 +4560,6 @@ pub struct BigFishWorkSummaryRecord {
|
||||
pub background_ready: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishVector2Record {
|
||||
pub x: f32,
|
||||
pub y: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishRuntimeEntityRecord {
|
||||
pub entity_id: String,
|
||||
pub level: u32,
|
||||
pub position: BigFishVector2Record,
|
||||
pub radius: f32,
|
||||
pub offscreen_seconds: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct BigFishRuntimeRecord {
|
||||
pub run_id: String,
|
||||
pub session_id: String,
|
||||
pub status: String,
|
||||
pub tick: u64,
|
||||
pub player_level: u32,
|
||||
pub win_level: u32,
|
||||
pub leader_entity_id: Option<String>,
|
||||
pub owned_entities: Vec<BigFishRuntimeEntityRecord>,
|
||||
pub wild_entities: Vec<BigFishRuntimeEntityRecord>,
|
||||
pub camera_center: BigFishVector2Record,
|
||||
pub last_input: BigFishVector2Record,
|
||||
pub event_log: Vec<String>,
|
||||
pub updated_at: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct ResolveNpcBattleInteractionInput {
|
||||
pub npc_interaction: DomainResolveNpcInteractionInput,
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
// THIS FILE IS AUTOMATICALLY GENERATED BY SPACETIMEDB. EDITS TO THIS FILE
|
||||
// WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD.
|
||||
|
||||
#![allow(unused, clippy::all)]
|
||||
use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
|
||||
|
||||
use super::database_migration_authorize_operator_input_type::DatabaseMigrationAuthorizeOperatorInput;
|
||||
use super::database_migration_operator_procedure_result_type::DatabaseMigrationOperatorProcedureResult;
|
||||
|
||||
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
|
||||
#[sats(crate = __lib)]
|
||||
struct AuthorizeDatabaseMigrationOperatorArgs {
|
||||
pub input: DatabaseMigrationAuthorizeOperatorInput,
|
||||
}
|
||||
|
||||
impl __sdk::InModule for AuthorizeDatabaseMigrationOperatorArgs {
|
||||
type Module = super::RemoteModule;
|
||||
}
|
||||
|
||||
#[allow(non_camel_case_types)]
|
||||
/// Extension trait for access to the procedure `authorize_database_migration_operator`.
|
||||
///
|
||||
/// Implemented for [`super::RemoteProcedures`].
|
||||
pub trait authorize_database_migration_operator {
|
||||
fn authorize_database_migration_operator(
|
||||
&self,
|
||||
input: DatabaseMigrationAuthorizeOperatorInput,
|
||||
) {
|
||||
self.authorize_database_migration_operator_then(input, |_, _| {});
|
||||
}
|
||||
|
||||
fn authorize_database_migration_operator_then(
|
||||
&self,
|
||||
input: DatabaseMigrationAuthorizeOperatorInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<DatabaseMigrationOperatorProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
);
|
||||
}
|
||||
|
||||
impl authorize_database_migration_operator for super::RemoteProcedures {
|
||||
fn authorize_database_migration_operator_then(
|
||||
&self,
|
||||
input: DatabaseMigrationAuthorizeOperatorInput,
|
||||
|
||||
__callback: impl FnOnce(
|
||||
&super::ProcedureEventContext,
|
||||
Result<DatabaseMigrationOperatorProcedureResult, __sdk::InternalError>,
|
||||
) + Send
|
||||
+ 'static,
|
||||
) {
|
||||
self.imp
|
||||
.invoke_procedure_with_callback::<_, DatabaseMigrationOperatorProcedureResult>(
|
||||
"authorize_database_migration_operator",
|
||||
AuthorizeDatabaseMigrationOperatorArgs { input },
|
||||
__callback,
|
||||
);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user