diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..422a9ea0 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,36 @@ +.git +.codex-temp +.codex-logs +.codex-runlogs +.idea +.vite +node_modules +target +dist +coverage +logs +tmp +*.log +/*.png +/*.jpg +/*.jpeg +/*.webp + +.env +.env.local +.env.secrets.local +.env.secrets.* +spacetime.local.json +deploy/container/api-server.env + +server-rs/target +server-rs/target-* +server-rs/.data +server-rs/.spacetimedb + +public/generated-* + +scripts/loadtest/data/*.local.json +scripts/loadtest/data/k6-*.log +scripts/loadtest/data/k6-*summary*.md +scripts/loadtest/data/latest-*-prefix.txt diff --git a/.gitignore b/.gitignore index 6f27c449..11a83c89 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,7 @@ temp*build*/ .worktrees/ .env.secrets.local spacetime.local.json +deploy/container/api-server.env # Local load-test data extracted from private migration files scripts/loadtest/data/*.local.json diff --git a/.hermes/shared-memory/decision-log.md b/.hermes/shared-memory/decision-log.md index 482f616d..f34b87e6 100644 --- a/.hermes/shared-memory/decision-log.md +++ b/.hermes/shared-memory/decision-log.md @@ -16,6 +16,25 @@ --- +## 2026-05-17 容器化方案只作为隔离压测与预发模拟路径 + +- 背景:Windows 本机直连极高 VU 压测会放大本地连接与发送缓冲行为,和线上 Linux + Nginx + systemd 拓扑不一致;需要一个更接近生产网络层的模拟方案,但不能扰动当前生产发布链路。 +- 决策:新增 `deploy/container/` 容器化方案,使用 Docker Compose 组合 Linux release `api-server`、容器 Nginx、`otelcol-contrib` debug exporter 和可选 k6。该方案只用于本机或预发压测模拟,不替换当前生产 `systemd + Nginx + Jenkins` 路径。 +- 隔离边界:容器方案使用独立 `deploy/container/api-server.env`、独立 Nginx 配置、独立 compose 命令和默认 `18080` 端口;真实 token 不进入镜像、不提交 Git;生产 systemd 单元、Jenkins 发布脚本和 `deploy/nginx/` 模板仍是正式线上来源。 +- 影响范围:`deploy/container/`、`scripts/container-compose.mjs`、`package.json` 容器命令、开发运维文档和容器 build context 排除规则。 +- 验证方式:执行 `npm run container:config` 展开 compose 配置;需要真实运行时再执行 `npm run container:build`、`npm run container:up`、`npm run container:k6`,并结合容器 Nginx log 与 OTLP debug exporter 判断瓶颈。 +- 关联文档:`deploy/container/README.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + +## 2026-05-16 公开作品列表短期由 BFF 订阅读模型缓存 + +- 背景:作品列表压测和实时性讨论中,曾考虑让浏览器前端直接订阅公开作品列表,减少 HTTP 拉取和 BFF 压力。 +- 决策:本轮不直接把作品列表整体交给前端订阅。短期继续由 `api-server` / BFF 通过 `spacetime-client` 长期订阅 SpacetimeDB 公开 read model 并读取本地 cache,维持首屏、排序、字段归一、权限降级和 HTTP fallback。中期可以新增或统一稳定的专用公开作品列表 read model,例如 `public_work_gallery_entry`,作为前端可选直连订阅对象。 +- 边界:未来前端直订阅只允许面向稳定、低基数、公开的专用 read model。前端不得直接订阅 `puzzle_work_profile`、`custom_world_profile` 等领域源表,也不得在前端自行 join、聚合或执行公开权限逻辑;这些逻辑必须先沉到后端投影 / read model。 +- 后续准入:若要落地前端直订阅,必须先完成并验收权限边界、字段契约、排序 / 分页、埋点和 BFF 回退策略;缺任一项时继续走 `api-server` / BFF 订阅缓存方案。 +- 影响范围:发现页、推荐流、各玩法公开广场、`api-server` 公开列表缓存、SpacetimeDB public view / public 读模型设计。 +- 验证方式:新增公开作品列表订阅能力时,检查前端只消费专用 public read model 或 BFF HTTP DTO;检查源表 row shape、权限判断和跨玩法聚合没有下沉到前端页面。 +- 关联文档:`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`。 + ## 2026-05-16 api-server OpenTelemetry 统一补齐 traces metrics logs - 背景:压测与运行观测需要把 HTTP、SpacetimeDB 调用和应用日志串起来,同时保留本地 `journalctl` / 文件日志做故障排障。 diff --git a/.hermes/shared-memory/development-workflow.md b/.hermes/shared-memory/development-workflow.md index 9b077958..f1268ce4 100644 --- a/.hermes/shared-memory/development-workflow.md +++ b/.hermes/shared-memory/development-workflow.md @@ -198,7 +198,7 @@ npm run check:server-rs-ddd ## 生产压测与观测默认口径 - 作品列表 50 HTTP req/s 压测使用 `scripts/loadtest/README.md` 中的 K6 命令;当前脚本一次 iteration 请求两个公开列表接口,因此目标 50 HTTP req/s 对应 `PEAK_RPS=25`。 -- 生产 `api-server` 默认 backlog、worker threads、systemd 限制、Nginx upstream timing log 和 OTLP 开关以 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` 为准。 +- 生产 `api-server` 默认 backlog、worker threads、HTTP 并发背压、systemd 限制、Nginx upstream timing log 和 OTLP 开关以 `docs/【开发运维】本地开发验证与生产运维-2026-05-15.md` 为准。 - OpenTelemetry 现阶段可选发送 traces / metrics / logs,但不会取代本地 `journalctl -u genarrative-api.service`、`logs/api-server/` 与 `/var/log/nginx/genarrative.*.log`。 - 指标 label 不写 raw URI、userId、profileId 或 request_id;request_id 只用于 trace/log 串联。 diff --git a/.hermes/shared-memory/pitfalls.md b/.hermes/shared-memory/pitfalls.md index 98c55deb..def8d06f 100644 --- a/.hermes/shared-memory/pitfalls.md +++ b/.hermes/shared-memory/pitfalls.md @@ -95,9 +95,33 @@ - 现象:`/api/runtime/puzzle/gallery` 每个请求都走 `spacetime-client.list_puzzle_gallery()` 调用 SpacetimeDB procedure,导致 SpacetimeDB WASM 侧重复组装全量列表,客户端再映射一遍;历史实现还出现过 procedure JSON 字符串往返。 - 原因:`api-server` 的服务器端 `spacetime-client` 没有订阅可公开读取的 gallery 投影,虽然 SDK 支持 client cache,但请求路径仍把列表读取当作 procedure 调用。 -- 处理:`spacetime-module` 中用 public view `puzzle_gallery_view` 暴露已发布拼图作品;`spacetime-client` 建连接后订阅 `SELECT * FROM puzzle_gallery_view` 和 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 并等待 `on_applied`,HTTP gallery 只从 `connection.db().puzzle_gallery_view().iter()` 本地 cache 读取和排序,再用已同步的 `public_work_play_daily_stat` 在本地聚合 7 日播放数。旧 `list_puzzle_gallery` procedure 只作兼容,不再作为 HTTP gallery 主路径。 -- 验证:搜索 `server-rs/crates/spacetime-client/src/puzzle.rs` 不应再出现 gallery 主路径调用 `list_puzzle_gallery_then`;执行 `cargo check --manifest-path server-rs/Cargo.toml -p spacetime-client`、`cargo check --manifest-path server-rs/Cargo.toml -p api-server` 和 schema/runtime access 检查。 -- 关联:`server-rs/crates/spacetime-module/src/puzzle.rs`、`server-rs/crates/spacetime-client/src/lib.rs`、`server-rs/crates/spacetime-client/src/puzzle.rs`、`/api/runtime/puzzle/gallery`。 +- 处理:`spacetime-module` 中用 public view `puzzle_gallery_card_view` 暴露已发布拼图作品的列表卡片字段,不携带 `levels` / `anchor_pack` 等详情级载荷;`spacetime-client` 建连接后订阅 `SELECT * FROM puzzle_gallery_card_view` 和 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 并等待 `on_applied`。HTTP gallery 通过 `PuzzleGalleryCache` 缓存最终 `PuzzleGalleryResponse` DTO:`items` 返回前 10 个完整卡片,`previewRefs` 返回后 10 个作品号引用,cache miss / TTL 过期时单飞重建,后台 cleanup task 周期清理旧响应。旧 `list_puzzle_gallery` procedure 只作兼容,不再作为 HTTP gallery 主路径。 +- 验证:搜索 `server-rs/crates/spacetime-client/src/puzzle.rs` 不应再出现 gallery 主路径调用 `list_puzzle_gallery_then`;搜索 `server-rs/crates/spacetime-client/src/lib.rs` 应订阅 `puzzle_gallery_card_view`;执行 `npm run spacetime:generate`、`cargo check --manifest-path server-rs/Cargo.toml -p spacetime-client`、`cargo check --manifest-path server-rs/Cargo.toml -p api-server` 和 schema/runtime access 检查。 +- 关联:`server-rs/crates/spacetime-module/src/puzzle.rs`、`server-rs/crates/spacetime-client/src/lib.rs`、`server-rs/crates/spacetime-client/src/puzzle.rs`、`server-rs/crates/api-server/src/puzzle_gallery_cache.rs`、`/api/runtime/puzzle/gallery`。 + +## Windows 本地直连高 VU 压测不要误判成业务内存泄漏 + +- 现象:本地 Windows release `api-server` 直连 K6 压测时,250 RPS、`PREALLOCATED_VUS=300` 能把进程 private memory 瞬时推到约 7GB;同样配置打 `/healthz` 小响应也能复现,压测结束后回落到 100MB 级。 +- 原因:高水位主要来自本机直连的 K6 VU / 长连接 / Hyper 发送链路和 Windows 连接缓冲,不是 SpacetimeDB procedure、拼图 JSON 缓存或 OTEL exporter。降低到接近真实并发的 VU 后,同样 250 RPS 拼图广场 p95 约 9ms,峰值约 600MB。 +- 处理:本地容量判断时让 `PREALLOCATED_VUS` / `MAX_VUS` 接近真实并发,不要把过高 VU 预分配当作默认吞吐测试;同时观察 `process.memory.*`、`process.windows.handle.count`、`genarrative.http.server.response_bodies.in_flight`、`genarrative.http.server.request_permits.available`、`genarrative.puzzle_gallery.cache.*` 和 `genarrative.spacetime.read.*`。如果内存高但 body in-flight、背压 permit、cache rebuild 和 SpacetimeDB read 都不显示积压,优先按连接 / 发送链路高水位处理。 +- 验证:对照打 `/api/runtime/puzzle/gallery` 与 `/healthz`;对比 `PREALLOCATED_VUS=300 MAX_VUS=800` 和 `PREALLOCATED_VUS=20 MAX_VUS=40`;压测结束后继续采样 10 秒确认 private memory 回落。 +- 关联:`scripts/loadtest/README.md`、`docs/【开发运维】本地开发验证与生产运维-2026-05-15.md`、`server-rs/crates/api-server/src/process_metrics.rs`、`server-rs/crates/api-server/src/telemetry.rs`。 + +## 多玩法公开广场列表优先订阅 public view / read model + +- 现象:抓大鹅、方洞挑战、视觉小说、大鱼吃小鱼等公开列表如果沿用 `list_*_works` procedure,即使只读已发布作品,也会在每个 HTTP 请求里回到 SpacetimeDB WASM 侧扫描、反序列化配置并组装列表,50RPS 以上容易变成热点。 +- 原因:个人作品列表和公开广场列表复用了同一套 procedure 输入,导致公开列表为了通过 owner 校验传固定占位 owner,并把可长期同步的公开读模型当成请求期查询。 +- 处理:每个公开广场新增或复用专用 public view / public read model:`match_3_d_gallery_view`、`square_hole_gallery_view`、`visual_novel_gallery_view`、`big_fish_gallery_view`。`spacetime-client` 建连接后订阅这些 view 和对应 `public_work_play_daily_stat` source_type 桶,HTTP gallery 只读本地 cache。个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍走原有 procedure / reducer。 +- 验证:搜索 `server-rs/crates/spacetime-client/src/{match3d,square_hole,visual_novel,big_fish}.rs`,公开 gallery 主路径应读取 `connection.db().*_gallery_view()`,不应调用 `list_*_works_with_input`;执行 `npm run spacetime:generate`、`cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`、`npm run check:spacetime-schema`。 +- 关联:`server-rs/crates/spacetime-module/src/match3d/mod.rs`、`server-rs/crates/spacetime-module/src/square_hole/mod.rs`、`server-rs/crates/spacetime-module/src/visual_novel.rs`、`server-rs/crates/spacetime-module/src/big_fish/session.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 + +## 自定义世界广场和创作入口配置不要每次 HTTP 请求调用只读 procedure + +- 现象:`/api/runtime/custom-world-gallery` 每次请求调用 `list_custom_world_gallery_entries` procedure;入口熔断中间件每个玩法请求调用 `get_creation_entry_config` procedure,50RPS 以上会把 SpacetimeDB procedure 调用变成热点。 +- 原因:`custom_world_gallery_entry`、`creation_entry_config` 和 `creation_entry_type_config` 已经是可订阅读模型或配置表,但 HTTP 路径仍按“请求到来再查 procedure”处理。 +- 处理:`spacetime-client` 长连接订阅 `custom_world_gallery_entry`、`public_work_play_daily_stat` 的 `custom-world` 桶、`creation_entry_config` 和 `creation_entry_type_config`;custom-world gallery 从本地 cache 排序并聚合 7 日播放数;入口配置优先读订阅 cache,cache 缺失时用最近一次成功内存快照,再兜底调用 `get_creation_entry_config` 完成旧库兼容。旧 `list_custom_world_gallery_entries` procedure 只允许作为旧库缺少 gallery 行时的一次性同步兜底。 +- 验证:搜索 `server-rs/crates/spacetime-client/src/custom_world.rs`,gallery 主路径应是 `read_after_connect` 读取 `custom_world_gallery_entry()`;搜索 `server-rs/crates/spacetime-client/src/runtime.rs`,`get_creation_entry_config` 应优先读取 `creation_entry_config()` 和 `creation_entry_type_config()`。执行 `cargo check -p spacetime-client --manifest-path server-rs/Cargo.toml`、`cargo check -p api-server --manifest-path server-rs/Cargo.toml`。 +- 关联:`server-rs/crates/spacetime-client/src/lib.rs`、`server-rs/crates/spacetime-client/src/custom_world.rs`、`server-rs/crates/spacetime-client/src/runtime.rs`、`docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md`。 ## 陶泥儿 logo 生图慢请求先缩短 prompt 并单张串行 diff --git a/.hermes/todos/【后端架构】前端直订阅公开作品列表准入待办-2026-05-16.md b/.hermes/todos/【后端架构】前端直订阅公开作品列表准入待办-2026-05-16.md new file mode 100644 index 00000000..943d90e3 --- /dev/null +++ b/.hermes/todos/【后端架构】前端直订阅公开作品列表准入待办-2026-05-16.md @@ -0,0 +1,27 @@ +# 前端直订阅公开作品列表准入待办 + +## 背景 + +未来可以考虑让前端直接订阅公开作品列表,以减少列表读取链路中的 HTTP 往返,并复用 SpacetimeDB 的实时同步能力。 + +## 当前结论 + +短期仍由 `api-server` / BFF 订阅 SpacetimeDB public read model,并从本地 cache 读取后对外提供 HTTP 列表接口。前端不直接订阅作品源表,也不把正式列表排序、分页、权限裁剪逻辑下放到 UI。 + +## 落地前置条件 + +- 建立专用、稳定、低基数的 public read model,例如 `public_work_gallery_entry`。 +- 明确权限边界,只暴露公开列表所需字段,不泄露作者私有信息、审核内部状态或运营字段。 +- 固化字段契约,明确字段含义、默认值、兼容策略和生成绑定更新流程。 +- 明确排序与分页语义,避免依赖自增 ID 顺序,优先使用时间戳或显式排序字段。 +- 补齐埋点方案,能区分直订阅首屏、增量更新、分页加载和 fallback 命中。 +- 保留 BFF HTTP fallback,用于低版本客户端、订阅失败、权限策略调整和灰度回滚。 +- 禁止前端订阅 `puzzle_work_profile`、`custom_world_profile` 等作品源表。 + +## 建议验收 + +- 文档确认直订阅只面向专用 public read model,不绕过 BFF 读取源表。 +- schema、绑定、字段契约、排序分页和权限说明同步更新。 +- 前端具备订阅失败后的 BFF HTTP fallback。 +- 自动测试覆盖公开字段裁剪、排序分页稳定性和 fallback 路径。 +- 监控可观察直订阅成功率、首屏耗时、增量更新延迟和 fallback 比例。 diff --git a/deploy/container/README.md b/deploy/container/README.md new file mode 100644 index 00000000..c9eb84c5 --- /dev/null +++ b/deploy/container/README.md @@ -0,0 +1,132 @@ +# Genarrative 容器化压测与隔离部署方案 + +本目录只服务本机或预发的容器化模拟压测,不替换当前生产 `systemd + Nginx + Jenkins` 发布路径。生产服务器仍以 `deploy/systemd/`、`deploy/nginx/`、`scripts/jenkins-*.sh` 和 `scripts/deploy/production-api-deploy.sh` 为准。 + +## 拓扑 + +```text +Docker Compose +├─ nginx :80 -> api-server:8082,负责静态站点、/admin/、/api/ 反代、upstream timing log、连接限制 +├─ api-server :8082,Linux release 构建,连接外部 SpacetimeDB +├─ otelcol :4317/4318,debug exporter,接收 traces / metrics / logs +└─ k6 profile=loadtest 时临时启动,在 compose 网络内压 nginx +``` + +默认 host 端口: + +- `http://127.0.0.1:18080`:容器 Nginx。 +- `127.0.0.1:4317` / `127.0.0.1:4318`:容器 Collector OTLP gRPC / HTTP。 + +如端口冲突,可设置: + +```powershell +$env:GENARRATIVE_CONTAINER_HTTP_PORT="18081" +$env:GENARRATIVE_CONTAINER_OTLP_HTTP_PORT="14318" +$env:GENARRATIVE_CONTAINER_OTLP_GRPC_PORT="14317" +``` + +## 初始化 + +```bash +npm run container:init +``` + +该命令会从 `deploy/container/api-server.env.example` 生成本地 `deploy/container/api-server.env`。真实 token、库名和外部服务密钥只写本地 env 文件,不提交 Git。 + +Docker Desktop 下默认通过 `host.docker.internal:3101` 连接宿主机上 `npm run dev` 启动的 SpacetimeDB: + +```env +GENARRATIVE_SPACETIME_SERVER_URL=http://host.docker.internal:3101 +GENARRATIVE_SPACETIME_DATABASE=genarrative-loadtest +GENARRATIVE_SPACETIME_TOKEN= +``` + +Linux Docker Engine 如果不能解析 `host.docker.internal`,Compose 已配置 `host-gateway`;仍不通时把 `GENARRATIVE_SPACETIME_SERVER_URL` 改成宿主机网关 IP 或同网络内的 SpacetimeDB 地址。 + +## 启动与验证 + +```bash +npm run container:config +npm run container:build +npm run container:up +npm run container:ps +curl -sS http://127.0.0.1:18080/api/runtime/puzzle/gallery +``` + +查看日志: + +```bash +npm run container:logs -- nginx +npm run container:logs -- api-server +npm run container:logs -- otelcol +``` + +`npm run container:config` 默认只校验配置,不打印完整 env。排查 compose 展开结果时可临时使用: + +```bash +npm run container:config -- --print +``` + +如果 `deploy/container/api-server.env` 已写入真实 token,不要把完整展开结果贴到公开渠道。 + +停止: + +```bash +npm run container:down +``` + +如需同时清理容器卷: + +```bash +npm run container:down -- -v +``` + +## 压测 + +k6 在 compose 网络内访问 `http://nginx`,避免 Windows 本机直连连接模型干扰 Linux 容器结果: + +```bash +npm run container:k6 +``` + +作品列表脚本一次 iteration 默认请求两个公开列表接口,因此目标 500 HTTP req/s 对应 `PEAK_RPS=250`: + +```powershell +$env:SCENARIO="spike" +$env:START_RPS="25" +$env:PEAK_RPS="250" +$env:HOLD="60s" +$env:END_RPS="25" +$env:PREALLOCATED_VUS="100" +$env:MAX_VUS="500" +$env:DETAIL_RATIO="0" +npm run container:k6 +``` + +如果要压 1000 HTTP req/s,把 `PEAK_RPS` 调到 `500`;如果要压 5000 HTTP req/s,把 `PEAK_RPS` 调到 `2500`,并同时提高 `PREALLOCATED_VUS` / `MAX_VUS`,观察是否先被带宽、Nginx `limit_conn` 或 api-server 背压限制。 + +## OTLP + +容器内 `otelcol` 默认使用 debug exporter。开启 api-server OTEL: + +```env +GENARRATIVE_OTEL_ENABLED=true +OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318 +``` + +然后重建或重启容器: + +```bash +npm run container:up +npm run container:logs -- otelcol +``` + +Collector 日志会输出 traces / metrics / logs。接 Rider、Jaeger、Tempo、Prometheus、Grafana 或托管平台时,另建独立 Collector 配置,不直接改生产 systemd 或 Nginx 模板。 + +## 隔离边界 + +- 不改生产 systemd 单元。 +- 不改 Jenkins 发布主流程。 +- 不要求真实 HTTPS 证书。 +- 不把真实 `.env`、`.env.local`、`.env.secrets.local` 或 `deploy/container/api-server.env` 放入 Docker build context。 +- 不在容器镜像里内置 SpacetimeDB 数据或 token。 diff --git a/deploy/container/api-server.Dockerfile b/deploy/container/api-server.Dockerfile new file mode 100644 index 00000000..5385b719 --- /dev/null +++ b/deploy/container/api-server.Dockerfile @@ -0,0 +1,49 @@ +FROM rust:1.88-bookworm AS rust-builder +WORKDIR /workspace + +COPY server-rs ./server-rs +RUN cargo build --release -p api-server --manifest-path server-rs/Cargo.toml && \ + cp server-rs/target/release/api-server /tmp/api-server + +FROM debian:bookworm-slim AS api-runtime +WORKDIR /srv/genarrative + +RUN apt-get update && \ + apt-get install -y --no-install-recommends ca-certificates curl && \ + rm -rf /var/lib/apt/lists/* && \ + useradd --system --create-home --home-dir /srv/genarrative --shell /usr/sbin/nologin genarrative + +COPY --from=rust-builder /tmp/api-server /usr/local/bin/api-server + +RUN mkdir -p /var/lib/genarrative/auth && \ + chown -R genarrative:genarrative /srv/genarrative /var/lib/genarrative + +USER genarrative +EXPOSE 8082 + +ENV GENARRATIVE_ENV=container \ + GENARRATIVE_API_HOST=0.0.0.0 \ + GENARRATIVE_API_PORT=8082 \ + GENARRATIVE_AUTH_STORE_PATH=/var/lib/genarrative/auth/auth-store.json + +CMD ["api-server"] + +FROM node:22-bookworm-slim AS web-builder +WORKDIR /workspace + +COPY package.json package-lock.json ./ +COPY apps/admin-web/package.json ./apps/admin-web/package.json +RUN npm ci + +COPY index.html metadata.json tsconfig.json vite.config.ts ./ +COPY src ./src +COPY public ./public +COPY media ./media +COPY packages ./packages +COPY apps/admin-web ./apps/admin-web +RUN npm run build:raw && npm run admin-web:build + +FROM nginx:1.27-alpine AS nginx-runtime +COPY --from=web-builder /workspace/dist /srv/genarrative/web +COPY --from=web-builder /workspace/apps/admin-web/dist /srv/genarrative/web/admin +COPY deploy/container/nginx.conf /etc/nginx/nginx.conf diff --git a/deploy/container/api-server.env.example b/deploy/container/api-server.env.example new file mode 100644 index 00000000..ad4ff549 --- /dev/null +++ b/deploy/container/api-server.env.example @@ -0,0 +1,35 @@ +# 复制为 deploy/container/api-server.env 后填入本机或预发值。 +# 该文件只用于容器隔离方案,不参与 systemd/Jenkins 生产部署。 +# 不要在这里写真实 token 后提交 Git。 + +GENARRATIVE_ENV=container +GENARRATIVE_API_HOST=0.0.0.0 +GENARRATIVE_API_PORT=8082 +GENARRATIVE_API_LOG=info,tower_http=info +GENARRATIVE_API_LISTEN_BACKLOG=1024 +GENARRATIVE_API_WORKER_THREADS=4 +GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512 + +GENARRATIVE_OTEL_ENABLED=false +OTEL_SERVICE_NAME=genarrative-api +OTEL_EXPORTER_OTLP_ENDPOINT=http://otelcol:4318 +OTEL_RESOURCE_ATTRIBUTES=deployment.environment=container,service.namespace=genarrative + +GENARRATIVE_INTERNAL_API_SECRET=CHANGE_ME_FOR_CONTAINER +GENARRATIVE_JWT_ISSUER=genarrative-container +GENARRATIVE_JWT_SECRET=CHANGE_ME_FOR_CONTAINER +AUTH_REFRESH_COOKIE_SECURE=false +GENARRATIVE_AUTH_STORE_PATH=/var/lib/genarrative/auth/auth-store.json + +# Docker Desktop 下连接宿主机 npm run dev 启动的 SpacetimeDB。 +# Linux Docker Engine 可改成宿主机网关 IP,或在 compose 里接入同一网络内的 SpacetimeDB。 +GENARRATIVE_SPACETIME_SERVER_URL=http://host.docker.internal:3101 +GENARRATIVE_SPACETIME_DATABASE=genarrative-loadtest +GENARRATIVE_SPACETIME_TOKEN= +GENARRATIVE_SPACETIME_POOL_SIZE=8 +GENARRATIVE_SPACETIME_PROCEDURE_TIMEOUT_SECONDS=45 + +GENARRATIVE_LLM_PROVIDER=openai-compatible +GENARRATIVE_LLM_BASE_URL= +GENARRATIVE_LLM_API_KEY= +GENARRATIVE_LLM_MODEL= diff --git a/deploy/container/docker-compose.loadtest.yml b/deploy/container/docker-compose.loadtest.yml new file mode 100644 index 00000000..2450e6ec --- /dev/null +++ b/deploy/container/docker-compose.loadtest.yml @@ -0,0 +1,85 @@ +name: genarrative-container-loadtest + +services: + api-server: + build: + context: ../.. + dockerfile: deploy/container/api-server.Dockerfile + target: api-runtime + env_file: + - ./api-server.env + environment: + GENARRATIVE_API_HOST: 0.0.0.0 + GENARRATIVE_API_PORT: 8082 + OTEL_EXPORTER_OTLP_ENDPOINT: http://otelcol:4318 + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - api-auth-store:/var/lib/genarrative/auth + depends_on: + otelcol: + condition: service_started + healthcheck: + test: ["CMD", "curl", "-fsS", "http://127.0.0.1:8082/healthz"] + interval: 10s + timeout: 3s + retries: 12 + start_period: 20s + + nginx: + build: + context: ../.. + dockerfile: deploy/container/api-server.Dockerfile + target: nginx-runtime + depends_on: + api-server: + condition: service_healthy + ports: + - "${GENARRATIVE_CONTAINER_HTTP_PORT:-18080}:80" + extra_hosts: + - "host.docker.internal:host-gateway" + volumes: + - nginx-logs:/var/log/nginx + healthcheck: + test: ["CMD", "wget", "-qO-", "http://127.0.0.1/api/runtime/puzzle/gallery"] + interval: 10s + timeout: 5s + retries: 12 + start_period: 20s + + otelcol: + image: otel/opentelemetry-collector-contrib:0.125.0 + command: ["--config=/etc/otelcol/config.yaml"] + volumes: + - ./otelcol.yaml:/etc/otelcol/config.yaml:ro + ports: + - "${GENARRATIVE_CONTAINER_OTLP_GRPC_PORT:-4317}:4317" + - "${GENARRATIVE_CONTAINER_OTLP_HTTP_PORT:-4318}:4318" + + k6: + image: grafana/k6:0.52.0 + profiles: ["loadtest"] + depends_on: + nginx: + condition: service_healthy + environment: + BASE_URL: http://nginx + WORKS_DATA: data/works-list.sample.json + SCENARIO: ${SCENARIO:-spike} + START_RPS: ${START_RPS:-5} + PEAK_RPS: ${PEAK_RPS:-250} + HOLD: ${HOLD:-60s} + END_RPS: ${END_RPS:-5} + PREALLOCATED_VUS: ${PREALLOCATED_VUS:-100} + MAX_VUS: ${MAX_VUS:-500} + DETAIL_RATIO: ${DETAIL_RATIO:-0} + SLEEP_MIN_SECONDS: ${SLEEP_MIN_SECONDS:-0} + SLEEP_MAX_SECONDS: ${SLEEP_MAX_SECONDS:-0} + volumes: + - ../../scripts/loadtest:/scripts/loadtest:ro + working_dir: /scripts/loadtest + command: ["run", "k6-works-list.js"] + +volumes: + api-auth-store: + nginx-logs: diff --git a/deploy/container/nginx.conf b/deploy/container/nginx.conf new file mode 100644 index 00000000..ae274c96 --- /dev/null +++ b/deploy/container/nginx.conf @@ -0,0 +1,133 @@ +worker_processes auto; + +events { + worker_connections 4096; +} + +http { + include /etc/nginx/mime.types; + default_type application/octet-stream; + + log_format genarrative_upstream + '$remote_addr - $remote_user [$time_local] "$request" ' + '$status $body_bytes_sent "$http_referer" "$http_user_agent" ' + 'request_time=$request_time upstream_connect_time=$upstream_connect_time ' + 'upstream_header_time=$upstream_header_time upstream_response_time=$upstream_response_time ' + 'upstream_status=$upstream_status request_id=$request_id'; + + upstream genarrative_api { + server api-server:8082; + keepalive 64; + } + + limit_conn_zone $binary_remote_addr zone=genarrative_api_conn:10m; + + sendfile on; + keepalive_timeout 65; + + gzip on; + gzip_vary on; + gzip_proxied any; + gzip_comp_level 5; + gzip_min_length 1024; + gzip_types + text/plain + text/css + text/javascript + application/javascript + application/json + application/xml + application/xml+rss + image/svg+xml; + + server { + listen 80; + server_name _; + + access_log /var/log/nginx/genarrative.access.log genarrative_upstream; + error_log /var/log/nginx/genarrative.error.log warn; + limit_conn_status 429; + limit_conn_log_level warn; + + root /srv/genarrative/web; + index index.html; + + location ^~ /admin/api/ { + default_type application/json; + limit_conn genarrative_api_conn 64; + + proxy_pass http://genarrative_api/admin/api/; + proxy_http_version 1.1; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Request-Id $request_id; + } + + location = /admin { + return 301 /admin/; + } + + location ^~ /admin/assets/ { + try_files $uri =404; + } + + location ^~ /admin/ { + try_files $uri $uri/ /admin/index.html; + } + + location ^~ /assets/ { + try_files $uri =404; + } + + location ~ ^/api(?:/|$) { + default_type application/json; + limit_conn genarrative_api_conn 64; + + proxy_pass http://genarrative_api; + proxy_http_version 1.1; + proxy_buffering off; + proxy_read_timeout 3600s; + proxy_send_timeout 3600s; + add_header X-Accel-Buffering no always; + proxy_set_header Connection ""; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + proxy_set_header X-Forwarded-Host $host; + proxy_set_header X-Request-Id $request_id; + } + + location ~ ^/(generated-|healthz) { + return 404; + } + + location ~ ^/v1/database/[^/]+/subscribe$ { + proxy_pass http://host.docker.internal:3101; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 3600s; + } + + location ^~ /v1/identity { + proxy_pass http://host.docker.internal:3101; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "Upgrade"; + proxy_set_header Host $host; + } + + location ^~ /v1/ { + return 404; + } + + location / { + try_files $uri $uri/ /index.html; + } + } +} diff --git a/deploy/container/otelcol.yaml b/deploy/container/otelcol.yaml new file mode 100644 index 00000000..f86d0155 --- /dev/null +++ b/deploy/container/otelcol.yaml @@ -0,0 +1,23 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + http: + endpoint: 0.0.0.0:4318 + +exporters: + debug: + verbosity: detailed + +service: + pipelines: + traces: + receivers: [otlp] + exporters: [debug] + metrics: + receivers: [otlp] + exporters: [debug] + logs: + receivers: [otlp] + exporters: [debug] diff --git a/deploy/env/api-server.env.example b/deploy/env/api-server.env.example index cdcc97e9..c0c4763e 100644 --- a/deploy/env/api-server.env.example +++ b/deploy/env/api-server.env.example @@ -7,6 +7,7 @@ GENARRATIVE_API_PORT=8082 GENARRATIVE_API_LOG=info,tower_http=info GENARRATIVE_API_LISTEN_BACKLOG=1024 GENARRATIVE_API_WORKER_THREADS=4 +GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512 GENARRATIVE_OTEL_ENABLED=false OTEL_SERVICE_NAME=genarrative-api OTEL_EXPORTER_OTLP_ENDPOINT=http://127.0.0.1:4318 diff --git a/deploy/nginx/genarrative-dev-http.conf b/deploy/nginx/genarrative-dev-http.conf index d9e85b16..6c9bede4 100644 --- a/deploy/nginx/genarrative-dev-http.conf +++ b/deploy/nginx/genarrative-dev-http.conf @@ -13,11 +13,15 @@ upstream genarrative_api { keepalive 64; } +limit_conn_zone $binary_remote_addr zone=genarrative_api_conn:10m; + server { listen 80; server_name genarrative.example.com; access_log /var/log/nginx/genarrative.access.log genarrative_upstream; error_log /var/log/nginx/genarrative.error.log warn; + limit_conn_status 429; + limit_conn_log_level warn; gzip on; gzip_vary on; @@ -43,6 +47,7 @@ server { location ^~ /admin/api/ { default_type application/json; + limit_conn genarrative_api_conn 64; if ($genarrative_maintenance) { return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}'; @@ -83,6 +88,7 @@ server { # 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。 location ~ ^/api(?:/|$) { default_type application/json; + limit_conn genarrative_api_conn 64; if ($genarrative_maintenance) { return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}'; diff --git a/deploy/nginx/genarrative.conf b/deploy/nginx/genarrative.conf index e0854442..984dd130 100644 --- a/deploy/nginx/genarrative.conf +++ b/deploy/nginx/genarrative.conf @@ -11,11 +11,15 @@ upstream genarrative_api { keepalive 64; } +limit_conn_zone $binary_remote_addr zone=genarrative_api_conn:10m; + server { listen 80; server_name genarrative.example.com; access_log /var/log/nginx/genarrative.access.log genarrative_upstream; error_log /var/log/nginx/genarrative.error.log warn; + limit_conn_status 429; + limit_conn_log_level warn; location /.well-known/acme-challenge/ { root /var/www/html; @@ -59,6 +63,7 @@ server { location ^~ /admin/api/ { default_type application/json; + limit_conn genarrative_api_conn 64; if ($genarrative_maintenance) { return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}'; @@ -99,6 +104,7 @@ server { # 临时兼容主站仍在使用的 /api/* HTTP facade;前端完成 SpacetimeDB SDK 迁移后删除。 location ~ ^/api(?:/|$) { default_type application/json; + limit_conn genarrative_api_conn 64; if ($genarrative_maintenance) { return 503 '{"ok":false,"error":{"code":"MAINTENANCE","message":"服务维护中"}}'; diff --git a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md index 084b3364..2ac833a4 100644 --- a/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md +++ b/docs/【后端架构】server-rs与SpacetimeDB数据契约-2026-05-15.md @@ -89,7 +89,7 @@ npm run check:server-rs-ddd 3. 删除字段、改名、重排字段、改类型或修改字段属性前,必须先询问用户并确认迁移计划。 4. Vec 字段不要直接写无法 const 求值的 default;需要默认空集合时优先使用 `Option>` 加 `#[default(None::>)]`,业务层归一为空数组。 5. 运行态读表必须按已声明索引访问。只要 table 上存在覆盖查询前缀的 `#[index(...)]` 或主键 / unique accessor,列表、详情、快照组装和计数都先用对应 accessor `.filter(...)` / `.find(...)`,再在内存中处理索引无法覆盖的残余条件;不得用 `.iter().filter(...)` 扫整表替代现成索引。 -6. 面向公开列表的只读投影优先做成 public view,并由 `api-server` 的 `spacetime-client` 长期订阅后读本地 cache。不要让 HTTP 列表接口每次请求都调用 procedure 重新组装全量列表;需要请求时间窗口的轻量统计可订阅公开统计表后在 `api-server` 本地聚合,需要写入副作用的详情、点赞、游玩记录仍可走 procedure / reducer。 +6. 面向公开列表的只读投影优先做成 public view / public 读模型表,并由 `api-server` 的 `spacetime-client` 长期订阅后读本地 cache。短期不把作品列表整体交给浏览器前端直接订阅;不要让 HTTP 列表接口每次请求都调用 procedure 重新组装全量列表。需要请求时间窗口的轻量统计可订阅公开统计表后在 `api-server` 本地聚合,需要写入副作用的详情、点赞、游玩记录仍可走 procedure / reducer。中期如要让前端可选直连订阅,只能新增或统一稳定的专用 public read model,例如 `public_work_gallery_entry`,并保持字段、排序键、公开权限和降级语义由后端投影定义;前端不得直接订阅 `puzzle_work_profile`、`custom_world_profile` 等领域源表,也不得自己做 join、聚合或权限逻辑。首屏、排序、字段归一、权限降级和 HTTP fallback 仍由 `api-server` BFF 维持。 7. 多列索引按 SpacetimeDB 绑定生成的元组参数直接传入,例如 `.filter((source_type, profile_id, played_day))`;前缀查询只传前缀元组,例如 `.filter((scope_kind, scope_id.as_str()))`。不要为了绕过类型问题退回整表遍历。 8. procedure result 必须返回 typed snapshot / typed value。`spacetime-client` mapper 不得再通过 `row_json/session_json/work_json/items_json/run_json/event_json/feedback_json: Option` 做跨层 JSON 字符串传输,也不得在 mapper 里反序列化旧 `*JsonRecord` 兼容结构。业务内部持久化字段如 `profile_payload_json`、`levels_json` 等不属于 procedure result 载荷例外,仍按各自表契约处理。 9. 修改后运行: @@ -243,6 +243,7 @@ npm run check:server-rs-ddd - Rust 结构体:`BigFishCreationSession` - 源码:`server-rs/crates/spacetime-module/src/big_fish/tables.rs` +- 索引:`by_big_fish_session_owner_user_id`、`by_big_fish_session_stage`。公开广场 view 使用 `by_big_fish_session_stage` 读取已发布会话,避免扫整表。 ### `big_fish_event` @@ -254,6 +255,13 @@ npm run check:server-rs-ddd - Rust 结构体:`BigFishRuntimeRun` - 源码:`server-rs/crates/spacetime-module/src/big_fish/tables.rs` +### SpacetimeDB view:`big_fish_gallery_view` + +- Rust view:`big_fish_gallery_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/big_fish/session.rs` +- 说明:大鱼吃小鱼公开广场列表投影,只从 `Published` creation session 组装公开卡片字段;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM big_fish_gallery_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'big-fish'` 后,从本地 cache 构造 `/api/runtime/big-fish/gallery` 响应。公开列表不再每个 HTTP 请求调用 `list_big_fish_works` procedure;个人作品列表、详情、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。 + ### `chapter_progression` - Rust 结构体:`ChapterProgression` @@ -293,6 +301,7 @@ npm run check:server-rs-ddd - Rust 结构体:`CustomWorldGalleryEntry` - 源码:`server-rs/crates/spacetime-module/src/custom_world/mod.rs` +- 作用:自定义世界公开作品列表读模型。`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM custom_world_gallery_entry` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'`,`/api/runtime/custom-world-gallery` 从本地 cache 排序并聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_custom_world_gallery_entries` procedure。旧 procedure 只用于兼容旧库缺少 gallery 读模型行时的一次性同步兜底。 ### `custom_world_profile` @@ -339,6 +348,13 @@ npm run check:server-rs-ddd - Rust 结构体:`Match3DWorkProfileRow` - 源码:`server-rs/crates/spacetime-module/src/match3d/tables.rs` +### SpacetimeDB view:`match_3_d_gallery_view` + +- Rust view:`match3d_gallery_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/match3d/mod.rs` +- 说明:抓大鹅公开广场列表投影,只暴露 `publication_status = published` 的作品卡片字段;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM match_3_d_gallery_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'match3d'` 后,从本地 cache 构造 `/api/runtime/match3d/gallery` 响应。公开列表不再每个 HTTP 请求调用 `list_match3d_works` procedure;个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。 + ### `npc_state` - Rust 结构体:`NpcState` @@ -465,12 +481,54 @@ npm run check:server-rs-ddd - Rust 结构体:`PuzzleWorkProfileRow` - 源码:`server-rs/crates/spacetime-module/src/puzzle.rs` -### `puzzle_gallery_view` +### SpacetimeDB view:`puzzle_gallery_view` - Rust view:`puzzle_gallery_view` - 返回类型:`Vec` - 源码:`server-rs/crates/spacetime-module/src/puzzle.rs` -- 说明:拼图广场公开列表投影,只暴露 `publication_status = Published` 的作品;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM puzzle_gallery_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 后,从本地 cache 构造 `/api/runtime/puzzle/gallery` 响应,并在本地按当前请求时间聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。 +- 说明:拼图广场公开详情兼容投影,只暴露 `publication_status = Published` 的作品,但返回完整 `PuzzleWorkProfile`,包含 levels / anchor_pack 等详情级字段;公开列表主路径不再订阅该 view。 + +### SpacetimeDB view:`puzzle_gallery_card_view` + +- Rust view:`puzzle_gallery_card_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/puzzle.rs` +- 说明:拼图广场公开列表卡片投影,只暴露前端列表卡片需要的公开字段,不携带 levels / anchor_pack 等详情级载荷;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM puzzle_gallery_card_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` 后,从本地 cache 构造 `/api/runtime/puzzle/gallery` 响应,并在本地按当前请求时间聚合 `recentPlayCount7d`,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。 + +### 拼图公开列表 HTTP 窗口缓存 + +- 接口:`GET /api/runtime/puzzle/gallery` +- 响应契约:保留 `items` 字段兼容旧前端;当前 `items` 只返回前 10 个完整卡片,新增 `previewRefs` 返回后 10 个 `workId/profileId` 引用,并返回 `hasMore`、`nextCursor` 与 `totalCount`。 +- 缓存策略:`api-server` 在 `PuzzleGalleryCache` 中缓存最终 `PuzzleGalleryResponse` 的预序列化 data JSON。缓存 miss / 过期时单飞重建,避免并发请求重复排序、映射、DTO 深拷贝和 `serde_json::Value` 树构造;开启响应 envelope 时只按请求拼接轻量 meta,缓存短 TTL 刷新 `recentPlayCount7d`,后台 cleanup task 周期清理超过最大空闲窗口的旧响应。OTLP 通过 `genarrative.puzzle_gallery.cache.*`、`genarrative.spacetime.read.*`、`genarrative.http.server.response_bodies.in_flight` 和 `genarrative.http.server.request_permits.available` 区分缓存重建、SpacetimeDB 本地订阅读、响应 body 生命周期和 HTTP 背压状态。 +- 详情路径:公开详情、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理;前端拿到 `previewRefs` 后如果需要展开更多内容,应优先使用后续列表窗口能力或详情 cache,不要把自动详情预取变成新的 procedure 热点。 + +### api-server 长期订阅读模型 + +`spacetime-client` 建立每个池连接时会等待下列订阅初始同步: + +- `SELECT * FROM puzzle_gallery_card_view` +- `SELECT * FROM custom_world_gallery_entry` +- `SELECT * FROM match_3_d_gallery_view` +- `SELECT * FROM square_hole_gallery_view` +- `SELECT * FROM visual_novel_gallery_view` +- `SELECT * FROM big_fish_gallery_view` + +下列订阅用于统计或配置缓存,订阅失败不会让公开列表连接整体不可用,调用方保留兼容兜底: + +- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'` +- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'` +- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'match3d'` +- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'square-hole'` +- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'visual-novel'` +- `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'big-fish'` +- `SELECT * FROM creation_entry_config` +- `SELECT * FROM creation_entry_type_config` + +拼图、自定义世界、抓大鹅、方洞挑战、视觉小说和大鱼吃小鱼的公开列表 HTTP 路由都从订阅 cache 读取公开 read model / view。各玩法的个人作品列表、详情、发布、点赞、游玩记录、Remix 和其它需要鉴权或写入副作用的路径继续走 procedure / reducer;不要为了公开列表性能把这些 owner-specific 或 mutation 语义混进 public view。 + +`GET /api/creation-entry/config` 和入口熔断优先从订阅 cache 读取创作入口配置;cache 缺失时使用最近一次成功读取的内存快照,再兜底调用 `get_creation_entry_config` procedure 完成空库种子或旧库兼容。 + +未来可选:若发现页、推荐流和各玩法广场需要统一给浏览器前端直接订阅公开作品列表,只新增 / 统一专用 public read model,例如 `public_work_gallery_entry`。该 read model 必须是后端投影后的公开作品卡片契约,覆盖作品类型、公开作品号、标题、摘要、封面、作者展示名、排序键、公开统计和入口开关后的可见性,不暴露玩法领域源表 row shape。前端可选择订阅这个稳定投影来减少 HTTP 拉取,但不能订阅 `puzzle_work_profile`、`custom_world_profile` 等源表后自行拼装列表;BFF 仍保留首屏、SEO / 分享、旧客户端、订阅失败和灰度期间的 HTTP fallback。 ### `quest_log` @@ -517,6 +575,13 @@ npm run check:server-rs-ddd - Rust 结构体:`SquareHoleWorkProfileRow` - 源码:`server-rs/crates/spacetime-module/src/square_hole/tables.rs` +### SpacetimeDB view:`square_hole_gallery_view` + +- Rust view:`square_hole_gallery_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/square_hole/mod.rs` +- 说明:方洞挑战公开广场列表投影,只暴露 `publication_status = published` 的作品卡片字段;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM square_hole_gallery_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'square-hole'` 后,从本地 cache 构造 `/api/runtime/square-hole/gallery` 响应。公开列表不再每个 HTTP 请求调用 `list_square_hole_works` procedure;个人作品列表、详情、发布、点赞、游玩记录和 Remix 仍按原有 procedure / reducer 路径处理。 + ### `story_event` - Rust 结构体:`StoryEvent` @@ -581,3 +646,10 @@ npm run check:server-rs-ddd - Rust 结构体:`VisualNovelWorkProfileRow` - 源码:`server-rs/crates/spacetime-module/src/visual_novel.rs` + +### SpacetimeDB view:`visual_novel_gallery_view` + +- Rust view:`visual_novel_gallery_view` +- 返回类型:`Vec` +- 源码:`server-rs/crates/spacetime-module/src/visual_novel.rs` +- 说明:视觉小说公开广场列表投影,只暴露 `publication_status = published` 的作品卡片字段,不把完整 `draft` 暴露给公开列表订阅;`api-server` 的 `spacetime-client` 长期订阅 `SELECT * FROM visual_novel_gallery_view` 与 `SELECT * FROM public_work_play_daily_stat WHERE source_type = 'visual-novel'` 后,从本地 cache 构造 `/api/runtime/visual-novel/gallery` 响应。公开列表不再每个 HTTP 请求调用 `list_visual_novel_works` procedure;个人历史、详情、运行态和发布仍按原有 procedure / reducer 路径处理。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index 66271c92..f033543e 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -154,11 +154,27 @@ Jenkins 按 web / api / Spacetime module / build / deploy / publish 拆分 50 HTTP req/s 首版压测优化口径: - `api-server` 生产模板默认 `GENARRATIVE_API_LISTEN_BACKLOG=1024`、`GENARRATIVE_API_WORKER_THREADS=4`;本地未设置 worker threads 时继续使用 Tokio 默认值。 +- `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS=512` 开启应用内 HTTP 并发背压,超过并发许可时直接返回 `429 Too Many Requests` 和 `Retry-After: 1`,`/healthz` 不受该限制。该值不是 RPS 限速;如果压测中 429 上升但内存和 p95 收敛,说明背压正在保护进程,需要结合真实容量调阈值或在 Nginx 前置限流。直连 `api-server` 的极高 RPS 压测若出现 `connection refused`,通常已经打到 TCP 监听 / accept 层,应同时检查 backlog、Nginx upstream keepalive 和前置限流。 - `genarrative-api.service` 设置 `LimitNOFILE=65535`、`TasksMax=2048`;上线后用 `systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax` 和 `cat /proc/$(pidof api-server)/limits` 核对。 - Nginx `/api/` 与 `/admin/api/` 通过 `genarrative_api` upstream 代理到 `127.0.0.1:8082`,upstream keepalive 为 64;压测时看 `/var/log/nginx/genarrative.access.log` 中的 `request_time`、`upstream_connect_time`、`upstream_header_time`、`upstream_response_time`、`upstream_status`、`request_id`。 - 作品列表 K6 脚本一次 iteration 默认请求两个公开接口,因此约 50 HTTP req/s 的目标命令使用 `SCENARIO=spike START_RPS=5 PEAK_RPS=25 HOLD=60s END_RPS=5 DETAIL_RATIO=0 npm run loadtest:k6:works`。 +- 作品列表短期继续由 `api-server` / BFF 订阅 SpacetimeDB 公开 read model 后读本地 cache,不让浏览器前端直接订阅完整列表;未来如新增 `public_work_gallery_entry` 等专用公开作品列表 read model,前端只可订阅稳定、低基数、公开的专用投影,禁止订阅 `puzzle_work_profile`、`custom_world_profile` 等玩法源表后自行 join、聚合或判断权限。前端直订阅落地前必须先补齐权限、字段契约、排序 / 分页、埋点和 BFF 回退策略。 - 50 HTTP req/s 验收目标为 `http_req_failed < 1%`、`p95 < 2s`、`dropped_iterations = 0`,同时压测窗口内 Nginx 无新增 502。 +容器化压测与隔离部署方案单独放在 `deploy/container/`,用于本机或预发模拟 Linux release + Nginx + OTLP Collector 拓扑,不替换当前生产 `systemd + Nginx + Jenkins` 发布路径: + +```bash +npm run container:init +npm run container:config +npm run container:build +npm run container:up +npm run container:k6 +npm run container:down +``` + +容器方案默认暴露 `http://127.0.0.1:18080`,`api-server` 在容器内监听 `0.0.0.0:8082`,Nginx 通过 `api-server:8082` upstream 反代 `/api/` 和 `/admin/api/`。SpacetimeDB 默认仍连接宿主机 `http://host.docker.internal:3101`,真实库名、token 和外部服务密钥只写本地 `deploy/container/api-server.env`,不提交 Git。完整拓扑、端口、k6 参数和 OTLP debug exporter 使用方法见 `deploy/container/README.md`。 +`npm run container:config` 默认只做 quiet 校验,避免把本地 env 中的 token 展开到终端;确需排查完整 compose 时再传 `-- --print`。 + OpenTelemetry 现阶段可选 OTLP traces / metrics / logs,但本地日志与 Nginx 文件日志仍保留: - 默认 `GENARRATIVE_OTEL_ENABLED=false`,未开启时 api-server 不依赖 Collector。 @@ -167,6 +183,10 @@ OpenTelemetry 现阶段可选 OTLP traces / metrics / logs,但本地日志与 - api-server 当前发 OTLP HTTP,`OTEL_EXPORTER_OTLP_ENDPOINT` 指向 Collector HTTP base endpoint;不要改到 gRPC `4317` 或 Rider 端口,Rider 由 Collector 通过 `RIDER_OTLP_GRPC_ENDPOINT` 转发。 - 应用日志仍通过 `journalctl -u genarrative-api.service` 查看,Nginx 日志仍写文件;日志等级继续用 `GENARRATIVE_API_LOG` / `RUST_LOG` 控制,例如 `info,tower_http=info,spacetime_client=info`。 - debug exporter / Rider 转发都会同时接收 traces、metrics 和 logs。 +- api-server 会随 metrics 发送进程级指标:`process.memory.usage`、`process.memory.virtual`、`process.thread.count`、`genarrative.process.memory.private`;Windows 额外发送 `process.windows.handle.count`,Linux 额外发送 `process.unix.file_descriptor.count`。这些指标只描述当前进程,不携带请求、用户或作品 label。 +- HTTP 运行态补充发送 `genarrative.http.server.response_bodies.in_flight` 与 `genarrative.http.server.request_permits.available`,用于区分业务 handler / 背压 permit 是否仍被占用;拼图广场热点缓存补充发送 `genarrative.puzzle_gallery.cache.*` 指标,记录命中、未命中、重建耗时和预序列化 data JSON 字节数。 +- SpacetimeDB 观测分为两类:procedure / reducer 调用继续用 `genarrative.spacetime.procedure.*`,订阅本地 cache 读使用 `genarrative.spacetime.read.*`。`read=list_puzzle_gallery` 表示拼图广场当前从 `puzzle_gallery_card_view` 本地 cache 读取,不再每个 HTTP 请求调用 `list_puzzle_gallery` procedure。 +- 本地 Windows 直连压测的内存高水位要结合 K6 VU / 连接数解释。250 RPS 下过高 `PREALLOCATED_VUS` 可能让 300 个本地 Established 连接把 `api-server` private memory 瞬时推到 GB 级,且 `/healthz` 小响应也能复现;若压测结束后回落、`response_bodies.in_flight` 和背压 permit 未显示业务积压,应优先按连接 / 发送链路高水位处理,而不是判断为 SpacetimeDB 或 JSON 缓存泄漏。 - Rider 的 Logs 面板只展示 log event 自身字段,不会自动展开父 span 的全部 attributes;请求完成日志会直接带 `request_id`、`http.request.method`、`http.route`、`url.scheme`、`url.path`、`http.response.status_code`、`status_class`、`latency_ms` 和 `slow_request`,完整链路继续到 Traces 面板按 trace/span 查看。 - 指标 label 只允许低基数字段:HTTP 使用 `method`、`route`、`status_class`,SpacetimeDB 调用使用 `procedure`、`status_class`;`request_id` 只进入 trace/log attribute,不进入 metric label。 diff --git a/package.json b/package.json index 6ddc5166..4f65c0b6 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "admin-web:preview": "npm --prefix apps/admin-web run preview --", "spacetime:generate": "node scripts/generate-spacetime-bindings.mjs", "check:api-server-env": "node scripts/check-api-server-env.mjs", + "check:spacetime-runtime-access": "node scripts/check-spacetime-runtime-access.mjs", "deploy:rust:remote": "node scripts/run-bash-script.mjs scripts/deploy-rust-remote.sh", "build:production-release": "node scripts/run-bash-script.mjs scripts/build-production-release.sh", "build:rust:ubuntu": "node scripts/run-bash-script.mjs scripts/deploy-rust-remote.sh", @@ -31,7 +32,7 @@ "check:visual-novel-vn11": "node scripts/check-visual-novel-vn11-negative-scan.mjs", "check:visual-novel-vn12": "node scripts/check-visual-novel-vn12-acceptance.mjs", "check:wechat-miniprogram-auth": "node scripts/check-wechat-miniprogram-auth-smoke.mjs", - "check:server-rs-ddd": "npm run check:spacetime-schema && node scripts/check-server-rs-ddd-boundaries.mjs", + "check:server-rs-ddd": "npm run check:spacetime-schema && npm run check:spacetime-runtime-access && node scripts/check-server-rs-ddd-boundaries.mjs", "lint:eslint": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --max-warnings 0", "lint:guardrails": "npm run lint:eslint", "typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit", @@ -44,6 +45,14 @@ "test:watch": "vitest", "loadtest:extract-works": "node scripts/loadtest/extract-works-list-data.mjs", "loadtest:k6:works": "k6 run scripts/loadtest/k6-works-list.js", + "container:init": "node scripts/container-compose.mjs init", + "container:build": "node scripts/container-compose.mjs build", + "container:up": "node scripts/container-compose.mjs up", + "container:down": "node scripts/container-compose.mjs down", + "container:logs": "node scripts/container-compose.mjs logs", + "container:ps": "node scripts/container-compose.mjs ps", + "container:config": "node scripts/container-compose.mjs config", + "container:k6": "node scripts/container-compose.mjs k6", "check": "npm run lint && npm run test && npm run build && npm run check:content", "check:data": "node scripts/run-tsx.cjs scripts/validate-content.ts", "check:overrides": "node scripts/run-tsx.cjs scripts/validate-overrides.ts", diff --git a/packages/shared/src/contracts/puzzleWorkSummary.ts b/packages/shared/src/contracts/puzzleWorkSummary.ts index b1e69499..64678bb4 100644 --- a/packages/shared/src/contracts/puzzleWorkSummary.ts +++ b/packages/shared/src/contracts/puzzleWorkSummary.ts @@ -42,6 +42,19 @@ export interface PuzzleWorksResponse { items: PuzzleWorkSummary[]; } +export interface PuzzleGalleryWorkRef { + workId: string; + profileId: string; +} + +export interface PuzzleGalleryResponse { + items: PuzzleWorkSummary[]; + previewRefs?: PuzzleGalleryWorkRef[]; + hasMore?: boolean; + nextCursor?: string | null; + totalCount?: number; +} + export interface PuzzleWorkDetailResponse { item: PuzzleWorkProfile; } diff --git a/scripts/check-spacetime-runtime-access.mjs b/scripts/check-spacetime-runtime-access.mjs new file mode 100644 index 00000000..7ca05f89 --- /dev/null +++ b/scripts/check-spacetime-runtime-access.mjs @@ -0,0 +1,221 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +const repoRoot = process.cwd(); + +function readUtf8(relativePath) { + const absolute = path.join(repoRoot, relativePath); + if (!fs.existsSync(absolute)) { + failures.push(`${relativePath}: 文件不存在,无法执行 SpacetimeDB runtime access 检查`); + return null; + } + return fs.readFileSync(absolute, 'utf8'); +} + +const forbiddenSnippets = [ + { + file: 'server-rs/crates/spacetime-module/src/puzzle.rs', + snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.owner_user_id == input.owner_user_id)', + reason: 'puzzle_work_profile 已有 by_puzzle_work_owner_user_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/puzzle.rs', + snippet: '.puzzle_work_profile()\n .iter()\n .filter(|row| row.publication_status == PuzzlePublicationStatus::Published)', + reason: 'puzzle_work_profile 已有 by_puzzle_work_publication_status 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/puzzle.rs', + snippet: '.puzzle_leaderboard_entry()\n .iter()\n .filter(|row| row.profile_id == profile_id && row.grid_size == grid_size)', + reason: 'puzzle_leaderboard_entry 已有 by_puzzle_leaderboard_profile_grid 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/match3d/mod.rs', + snippet: '.match3d_work_profile()\n .iter()\n .filter(|row| {', + reason: 'match3d_work_profile 已有 owner/status 索引,列表不应整表过滤', + }, + { + file: 'server-rs/crates/spacetime-module/src/visual_novel.rs', + snippet: '.visual_novel_work_profile()\n .iter()\n .filter(|row| {', + reason: 'visual_novel_work_profile 已有 owner/status 索引,列表不应整表过滤', + }, + { + file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs', + snippet: '.asset_object()\n .iter()\n .find(|row| row.bucket == input.bucket && row.object_key == input.object_key)', + reason: 'asset_object 已有 by_bucket_object_key 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/asset_metadata/objects.rs', + snippet: '.asset_object()\n .iter()\n .filter(|row| row.asset_kind == asset_kind)', + reason: 'asset_object 已有 asset_kind 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/ai/stages.rs', + snippet: '.ai_task_stage()\n .iter()\n .filter(|row| row.task_id == task_id)', + reason: 'ai_task_stage 已有 by_ai_task_stage_task_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/ai/stages.rs', + snippet: '.ai_text_chunk()\n .iter()\n .filter(|row| row.task_id == task_id && row.stage_kind == stage_kind)', + reason: 'ai_text_chunk 已有 by_ai_text_chunk_task_id / by_ai_text_chunk_task_stage_sequence 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs', + snippet: '.ai_task_stage()\n .iter()\n .filter(|stage| stage.task_id == row.task_id)', + reason: 'ai_task_stage 快照组装应使用 by_ai_task_stage_task_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/ai/snapshots.rs', + snippet: '.ai_result_reference()\n .iter()\n .filter(|reference| reference.task_id == row.task_id)', + reason: 'ai_result_reference 快照组装应使用 by_ai_result_reference_task_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', + snippet: '.profile_save_archive()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)', + reason: 'profile_save_archive 已有 by_profile_save_archive_user_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', + snippet: '.profile_played_world()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)', + reason: 'profile_played_world 已有 by_profile_played_world_user_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', + snippet: '.profile_wallet_ledger()\n .iter()\n .filter(|row| row.user_id == validated_input.user_id)', + reason: 'profile_wallet_ledger 已有 by_profile_wallet_ledger_user_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', + snippet: '.profile_referral_relation()\n .iter()\n .filter(|row| row.inviter_user_id == user_id)', + reason: 'profile_referral_relation 已有 by_profile_referral_inviter_user_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', + snippet: '.profile_recharge_order()\n .iter()\n .filter(|row| row.user_id == user_id)', + reason: 'profile_recharge_order 已有 by_profile_recharge_order_user_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/runtime/profile.rs', + snippet: '.tracking_daily_stat()\n .iter()\n .filter(|row| {', + reason: 'tracking_daily_stat 已有 by_tracking_daily_stat_scope_day / event_day 索引,analytics 查询不应整表过滤', + }, + { + file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs', + snippet: '.custom_world_profile()\n .iter()\n .find(|row| {', + reason: 'custom_world_profile owner 维度已有 by_custom_world_profile_owner_user_id 索引', + }, + { + file: 'server-rs/crates/spacetime-module/src/custom_world/mod.rs', + snippet: '.custom_world_profile()\n .iter()\n .filter(|profile| {', + reason: 'custom_world_profile Published 同步已有 by_custom_world_profile_publication_status 索引', + }, +]; + +const procedureResultFiles = [ + 'server-rs/crates/module-puzzle/src/application.rs', + 'server-rs/crates/module-big-fish/src/domain.rs', + 'server-rs/crates/spacetime-module/src/match3d/types.rs', + 'server-rs/crates/spacetime-module/src/square_hole/types.rs', + 'server-rs/crates/spacetime-module/src/visual_novel.rs', + 'server-rs/crates/spacetime-module/src/bark_battle/types.rs', +]; + +const mapperCompatibilityFiles = [ + 'server-rs/crates/spacetime-client/src/mapper.rs', + 'server-rs/crates/spacetime-client/src/lib.rs', +]; + +const bigFishRuntimeFiles = [ + 'server-rs/crates/module-big-fish/src/commands.rs', + 'server-rs/crates/spacetime-module/src/big_fish/runtime.rs', + 'server-rs/crates/spacetime-module/src/big_fish/session.rs', +]; + +const legacyMapperPatterns = [ + { + pattern: /\b[A-Za-z0-9_]*JsonRecord\b/u, + reason: 'spacetime-client mapper 不应保留旧 ProcedureResult JSON 兼容 Record', + }, + { + pattern: /\bCompatibleBigFish[A-Za-z0-9_]*\b/u, + reason: 'spacetime-client mapper 不应保留 BigFish 旧 JSON 兼容结构', + }, + { + pattern: /\bmap_[A-Za-z0-9_]*_json\b/u, + reason: 'spacetime-client mapper 不应再通过 map_*_json 反序列化 procedure payload', + }, + { + pattern: /serde_json::from_str::<[A-Za-z0-9_:]*JsonRecord/u, + reason: 'spacetime-client mapper 不应把 procedure result 再反序列化为 JsonRecord', + }, + { + pattern: /\b(?:items|run|work|session|event|feedback)_json:\s*Some\(/u, + reason: 'mapper 测试与兼容路径不应再构造旧 procedure JSON 字符串字段', + }, +]; + +const typedProcedurePayloadFieldPattern = + /\b(?:row|session|work|item|items|run|event|feedback)_json:\s*Option/gu; + +const failures = []; + +for (const rule of forbiddenSnippets) { + const content = readUtf8(rule.file); + if (content === null) { + continue; + } + if (content.includes(rule.snippet)) { + failures.push(`${rule.file}: ${rule.reason}`); + } +} + +for (const file of procedureResultFiles) { + const content = readUtf8(file); + if (content === null) { + continue; + } + const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? []; + for (const block of resultBlocks) { + const jsonFields = block.match(typedProcedurePayloadFieldPattern); + if (jsonFields?.length) { + const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult'; + failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`); + } + } +} + +for (const file of mapperCompatibilityFiles) { + const content = readUtf8(file); + if (content === null) { + continue; + } + for (const rule of legacyMapperPatterns) { + if (rule.pattern.test(content)) { + failures.push(`${file}: ${rule.reason}`); + } + } +} + +for (const file of bigFishRuntimeFiles) { + const content = readUtf8(file); + if (content === null) { + continue; + } + const resultBlocks = content.match(/pub struct [A-Za-z0-9_]*ProcedureResult\s*\{[\s\S]*?\n\}/g) ?? []; + for (const block of resultBlocks) { + const jsonFields = block.match(typedProcedurePayloadFieldPattern); + if (jsonFields?.length) { + const name = block.match(/pub struct ([A-Za-z0-9_]+)/)?.[1] ?? 'ProcedureResult'; + failures.push(`${file}: ${name} 仍通过 ${jsonFields.join(', ')} 跨层返回 JSON 字符串`); + } + } +} + +if (failures.length > 0) { + console.error('SpacetimeDB runtime access 检查失败:'); + for (const failure of failures) { + console.error(`- ${failure}`); + } + process.exit(1); +} + +console.log('SpacetimeDB runtime access 检查通过。'); diff --git a/scripts/container-compose.mjs b/scripts/container-compose.mjs new file mode 100644 index 00000000..0ee92af5 --- /dev/null +++ b/scripts/container-compose.mjs @@ -0,0 +1,99 @@ +import {spawn} from 'node:child_process'; +import {copyFileSync, existsSync} from 'node:fs'; +import path from 'node:path'; + +const [, , rawCommand = 'help', ...args] = process.argv; +const command = rawCommand.trim(); +const printComposeConfig = args.includes('--print'); +const passthroughArgs = args.filter((arg) => arg !== '--print'); +const projectRoot = process.cwd(); +const composeFile = path.join('deploy', 'container', 'docker-compose.loadtest.yml'); +const envExamplePath = path.join('deploy', 'container', 'api-server.env.example'); +const envPath = path.join('deploy', 'container', 'api-server.env'); + +const supportedCommands = new Set(['init', 'build', 'up', 'down', 'logs', 'ps', 'config', 'k6']); + +if (command === 'help' || !supportedCommands.has(command)) { + printHelp(command !== 'help'); + process.exit(command === 'help' ? 0 : 1); +} + +if (command === 'init') { + ensureEnvFile(); + process.exit(0); +} + +if (!existsSync(envPath)) { + ensureEnvFile(); + console.error('[container] 请先检查 deploy/container/api-server.env 中的 SpacetimeDB 地址、库名和 token。'); + process.exit(1); +} + +const composeArgs = buildComposeArgs(command, passthroughArgs); +const child = spawn('docker', composeArgs, { + cwd: projectRoot, + env: process.env, + stdio: 'inherit', + shell: false, +}); + +child.on('error', (error) => { + console.error(`[container] docker compose 启动失败: ${error.message}`); + console.error('[container] 请确认 Docker Desktop 或 Docker Engine 已安装,并且 docker 在 PATH 中。'); + process.exit(1); +}); + +child.on('exit', (code, signal) => { + if (signal) { + console.error(`[container] docker compose 被信号终止: ${signal}`); + process.exit(1); + } + process.exit(code ?? 0); +}); + +function buildComposeArgs(selectedCommand, extraArgs) { + const baseArgs = ['compose', '-f', composeFile]; + switch (selectedCommand) { + case 'build': + return [...baseArgs, 'build', ...extraArgs]; + case 'up': + return [...baseArgs, 'up', '-d', ...extraArgs]; + case 'down': + return [...baseArgs, 'down', ...extraArgs]; + case 'logs': + return [...baseArgs, 'logs', ...extraArgs]; + case 'ps': + return [...baseArgs, 'ps', ...extraArgs]; + case 'config': + return [...baseArgs, 'config', ...(printComposeConfig ? [] : ['--quiet']), ...extraArgs]; + case 'k6': + return [...baseArgs, '--profile', 'loadtest', 'run', '--rm', 'k6', ...extraArgs]; + default: + throw new Error(`unsupported command: ${selectedCommand}`); + } +} + +function ensureEnvFile() { + if (existsSync(envPath)) { + console.log(`[container] 已存在 ${envPath}`); + return; + } + copyFileSync(envExamplePath, envPath); + console.log(`[container] 已从 ${envExamplePath} 生成 ${envPath}`); +} + +function printHelp(isError) { + const output = isError ? console.error : console.log; + output(`Usage: npm run container: -- [docker compose args] + +Commands: + container:init 生成 deploy/container/api-server.env + container:build 构建 api-server 容器镜像 + container:up 后台启动 api-server + nginx + otelcol + container:down 停止并清理容器 + container:logs 查看容器日志 + container:ps 查看容器状态 + container:config 校验 compose 配置,传 -- --print 可展开完整配置 + container:k6 在 compose 网络内运行 k6 +`); +} diff --git a/scripts/loadtest/README.md b/scripts/loadtest/README.md index be788df5..ef2e0307 100644 --- a/scripts/loadtest/README.md +++ b/scripts/loadtest/README.md @@ -226,6 +226,8 @@ npm run loadtest:k6:works ## 排障 - 如果公开 gallery 返回 `creation_entry_disabled` 或 503,检查本地 creation entry 配置是否禁用了对应入口。 +- 如果高压下返回 429,优先确认目标环境是否设置了 `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS`。429 表示 api-server 应用层背压已生效,不等同于业务错误;继续看内存、p95、`http_req_failed` 和 OTLP / Nginx timing 判断阈值是否偏低。 +- 如果直连 `api-server` 压测出现 `connection refused` 或 status 0,说明压力已经打到 TCP 监听 / accept 层;此时同时检查 `GENARRATIVE_API_LISTEN_BACKLOG`、Nginx upstream keepalive 和是否需要在 Nginx 前置限流,不能只靠应用层背压解释。 - 如果个人作品列表返回 401,确认 `AUTH_TOKEN` 是当前 api-server 可识别的 access token。 - 如果详情全部 404,确认是否已向目标环境导入与 `WORKS_DATA` 一致的数据。 @@ -310,11 +312,31 @@ OTLP logs 是远端观测增量,不替代本地日志;api-server 日志仍 Rider 的 Logs 面板展示的是 OTLP log event 自身字段,不会自动把父 span 的全部 attributes 摊平到每一条日志。请求完成日志会直接携带 `request_id`、`http.request.method`、`http.route`、`url.scheme`、`url.path`、`http.response.status_code`、`status_class`、`latency_ms` 和 `slow_request`;更完整的请求链路仍在 Traces 面板中按同一个 trace/span 关联查看。 +压测期间可在 Metrics 面板或 debug exporter 中观察进程内存指标: + +- `process.memory.usage`:进程常驻内存 / RSS。 +- `process.memory.virtual`:进程虚拟内存;Windows 当前按 `PrivateUsage` 上报,Linux 取 `VmSize`。 +- `genarrative.process.memory.private`:进程私有内存,Windows 来自 `PrivateUsage`,Linux 近似取 `/proc/self/status` 的 `VmData`。 +- `process.thread.count`:线程数。 +- `process.windows.handle.count`:Windows 句柄数。 +- `process.unix.file_descriptor.count`:Linux 文件描述符数。 +- `genarrative.http.server.response_bodies.in_flight`:Axum / Hyper 仍持有的响应 body 数;如果内存高但该值很低,说明热点不在业务 handler 生命周期内。 +- `genarrative.http.server.request_permits.available`:应用层 HTTP 背压剩余 permit 数;如果该值未接近 0,说明没有打满 `GENARRATIVE_API_MAX_CONCURRENT_REQUESTS`。 +- `genarrative.puzzle_gallery.cache.hits` / `genarrative.puzzle_gallery.cache.misses` / `genarrative.puzzle_gallery.cache.rebuilds`:拼图广场响应缓存命中、未命中和重建次数。 +- `genarrative.puzzle_gallery.cache.rebuild.duration`:拼图广场缓存重建耗时。 +- `genarrative.puzzle_gallery.cache.data_json_bytes`:拼图广场缓存内预序列化 data JSON 大小。 +- `genarrative.spacetime.read.calls` / `genarrative.spacetime.read.duration_ms`:SpacetimeDB 订阅本地 cache 读次数和耗时;`read=list_puzzle_gallery` 表示当前路径走 view / local cache,不是 procedure。 + +若 `/api/runtime/puzzle/gallery` 单接口压测出现 GB 级瞬时内存峰值,先区分“持续泄漏”和“请求期分配峰值”:关闭 OTEL 后若峰值仍复现且压测结束后回落,主因通常不是 Collector / exporter。当前拼图广场列表命中缓存时应复用 `PuzzleGalleryCache` 中的预序列化 data JSON,只按请求拼接 envelope meta,不应每个请求重新深拷贝 `PuzzleGalleryResponse` 或构造完整 `serde_json::Value`。 + +本地 Windows 直连 `api-server` 压测还要单独看 K6 的 VU / 连接模型。已验证在 250 RPS、`PREALLOCATED_VUS=300` 时,哪怕打 `/healthz` 这种小响应,也可能因为本地 300 个 Established 连接触发 `api-server` private memory 瞬时升到约 7GB,压测结束后回落到 100MB 级;同样 250 RPS 改成 `PREALLOCATED_VUS=20 MAX_VUS=40` 后,拼图广场 p95 约 9ms,峰值降到约 600MB。这个现象说明高水位主要来自本机直连连接 / 发送链路,不等价于 SpacetimeDB 或拼图 JSON 缓存泄漏。做本地容量判断时优先让 VU 接近真实并发,避免用过高预分配 VU 把测试变成 Windows 本机连接缓冲压力测试;生产仍以 Nginx upstream keepalive、系统内存和 OTLP 指标一起判断。 + 线上回归辅助命令: ```bash systemctl show genarrative-api.service -p LimitNOFILE -p TasksMax cat /proc/$(pidof api-server)/limits +tr '\0' '\n' < /proc/$(pidof api-server)/environ | grep GENARRATIVE_API_MAX_CONCURRENT_REQUESTS ss -ltnp | grep 8082 curl -sS http://127.0.0.1:8082/healthz ``` diff --git a/scripts/loadtest/data/works-list.sample.from-migration-1.json b/scripts/loadtest/data/works-list.sample.from-migration-1.json new file mode 100644 index 00000000..0a8b9def --- /dev/null +++ b/scripts/loadtest/data/works-list.sample.from-migration-1.json @@ -0,0 +1,218 @@ +{ + "source": "spacetime-migration-1.json", + "generatedAt": "2026-05-16T13:35:40.282Z", + "counts": { + "puzzle_work_profile": 3, + "custom_world_profile": 1, + "match3d_work_profile": 0, + "square_hole_work_profile": 0, + "visual_novel_work_profile": 0 + }, + "tables": { + "puzzle_work_profile": [ + { + "profile_id": "profile-001", + "work_id": "work-001", + "owner_user_id": "user-001", + "author_display_name": "author-001", + "cover_asset_id": "asset-001", + "cover_image_src": "/generated-puzzle-assets/puzzle-session-f38101d7277040fcb6fbc41fea8b714a/puzzle-session-f38101d7277040fcb6fbc41fea8b714a-candidate-2/asset-1777649330373133/image.png", + "work_title": "化学家", + "level_name": "文学家", + "summary": "几个文学家正站在山上面对着瀑布侃侃而谈", + "work_description": "一个穿着白大褂的化学家正在做酷炫的化学实验,背景是化学实验室", + "levels_json": "[{\"level_id\":\"puzzle-level-1777649242577-7\",\"level_name\":\"文学家\",\"picture_description\":\"几个文学家正站在山上面对着瀑布侃侃而谈\",\"candidates\":[{\"candidate_id\":\"puzzle-session-f38101d7277040fcb6fbc41fea8b714a-candidate-2\",\"image_src\":\"/generated-puzzle-assets/puzzle-session-f38101d7277040fcb6fbc41fea8b714a/puzzle-session-f38101d7277040fcb6fbc41fea8b714a-candidate-2/asset-1777649330373133/image.png\",\"asset_id\":\"asset-1777649330373133\",\"prompt\":\"几个文学家正站在山上面对着瀑布侃侃而谈\",\"actual_prompt\":\"请生成一张高清插画。画面主体:几个文学家正站在山上面对着瀑布侃侃而谈。画面…", + "anchor_pack_json": "{\"theme_promise\":{\"key\":\"themePromise\",\"label\":\"题材承诺\",\"value\":\"化学家\",\"status\":\"Locked\"},\"visual_subject\":{\"key\":\"visualSubject\",\"label\":\"画面主体\",\"value\":\"一个穿着白大褂的化学家正在做酷炫的化学实验,背景是化学实验室\",\"status\":\"Locked\"},\"visual_mood\":{\"key\":\"visualMood\",\"label\":\"视觉气质\",\"value\":\"清晰、适合拼图切块\",\"status\":\"Inferred\"},\"composition_hooks\":{\"key\":\"compositionHooks\",\"label\":\"拼图记忆点\",\"value\":\"主体轮廓、色块分区、局部细节\",\"status\":\"Inferred\"},\"tags_and_forbidden\":{\"key\":\"tagsAndForbidden\",\"label\":\"标签与禁忌\",\"value\":\"化学家、拼图、插画;禁止标题字\",\"status\":\"I…", + "theme_tags_json": "[\"化学家\",\"拼图\",\"插画\",\"禁止标题字\"]", + "publication_status": { + "Published": [] + }, + "play_count": 1, + "like_count": 0, + "remix_count": 1, + "updated_at": { + "__timestamp_micros_since_unix_epoch__": 1777703338322544 + }, + "created_at": { + "__timestamp_micros_since_unix_epoch__": 1777648804043558 + }, + "published_at": { + "__timestamp_micros_since_unix_epoch__": 1777649364112270 + } + }, + { + "profile_id": "profile-002", + "work_id": "work-002", + "owner_user_id": "user-002", + "author_display_name": "author-002", + "work_title": "我不知道", + "level_name": "", + "summary": "你猜我是谁", + "work_description": "你猜我是谁", + "levels_json": "[{\"level_id\":\"puzzle-level-1\",\"level_name\":\"\",\"picture_description\":\"真不知道\",\"candidates\":[],\"selected_candidate_id\":null,\"cover_image_src\":null,\"cover_asset_id\":null,\"generation_status\":\"idle\"}]", + "anchor_pack_json": "{\"theme_promise\":{\"key\":\"themePromise\",\"label\":\"题材承诺\",\"value\":\"我不知道\",\"status\":\"Locked\"},\"visual_subject\":{\"key\":\"visualSubject\",\"label\":\"画面主体\",\"value\":\"真不知道\",\"status\":\"Locked\"},\"visual_mood\":{\"key\":\"visualMood\",\"label\":\"视觉气质\",\"value\":\"清晰、适合拼图切块\",\"status\":\"Inferred\"},\"composition_hooks\":{\"key\":\"compositionHooks\",\"label\":\"拼图记忆点\",\"value\":\"主体轮廓、色块分区、局部细节\",\"status\":\"Inferred\"},\"tags_and_forbidden\":{\"key\":\"tagsAndForbidden\",\"label\":\"标签与禁忌\",\"value\":\"我不知道、拼图、插画;禁止标题字\",\"status\":\"Inferred\"}}", + "theme_tags_json": "[\"我不知道\"]", + "publication_status": { + "Draft": [] + }, + "play_count": 0, + "like_count": 0, + "remix_count": 0, + "updated_at": { + "__timestamp_micros_since_unix_epoch__": 1777619351714201 + }, + "created_at": { + "__timestamp_micros_since_unix_epoch__": 1777619336673245 + } + }, + { + "profile_id": "profile-003", + "work_id": "work-003", + "owner_user_id": "user-003", + "author_display_name": "author-002", + "work_title": "", + "level_name": "", + "summary": "", + "work_description": "", + "levels_json": "[{\"level_id\":\"puzzle-level-1\",\"level_name\":\"\",\"picture_description\":\"\",\"candidates\":[],\"selected_candidate_id\":null,\"cover_image_src\":null,\"cover_asset_id\":null,\"generation_status\":\"idle\"}]", + "anchor_pack_json": "{\"theme_promise\":{\"key\":\"themePromise\",\"label\":\"题材承诺\",\"value\":\"\",\"status\":\"Missing\"},\"visual_subject\":{\"key\":\"visualSubject\",\"label\":\"画面主体\",\"value\":\"\",\"status\":\"Missing\"},\"visual_mood\":{\"key\":\"visualMood\",\"label\":\"视觉气质\",\"value\":\"\",\"status\":\"Missing\"},\"composition_hooks\":{\"key\":\"compositionHooks\",\"label\":\"拼图记忆点\",\"value\":\"\",\"status\":\"Missing\"},\"tags_and_forbidden\":{\"key\":\"tagsAndForbidden\",\"label\":\"标签与禁忌\",\"value\":\"\",\"status\":\"Missing\"}}", + "theme_tags_json": "[\"拼图\",\"插画\",\"清晰构图\"]", + "publication_status": { + "Draft": [] + }, + "play_count": 0, + "like_count": 0, + "remix_count": 0, + "updated_at": { + "__timestamp_micros_since_unix_epoch__": 1777622285252380 + }, + "created_at": { + "__timestamp_micros_since_unix_epoch__": 1777622285252380 + } + } + ], + "custom_world_profile": [ + { + "profile_id": "profile-081", + "owner_user_id": "user-002", + "author_display_name": "author-012", + "author_public_user_code": "author-code-001", + "world_name": "青春飞扬校园", + "summary_text": "在现代校园中,玩家摆脱内卷,追求真实成长", + "subtitle": "反内卷的自由学习之旅", + "profile_payload_json": "{\"anchorContent\":null,\"anchorPack\":null,\"attributeSchema\":{\"generatedFrom\":{\"conflictCore\":\"与传统教育模式的冲突\",\"settingSummary\":\"在现代校园中,玩家摆脱内卷,追求真实成长\",\"tone\":\"积极向上,充满活力与创新\",\"worldName\":\"青春飞扬校园\",\"worldType\":\"CUSTOM\"},\"id\":\"schema:rpg-agent:1e15b44d:v1\",\"schemaVersion\":1,\"slots\":[{\"name\":\"知识储备\",\"slotId\":\"axis_a\"},{\"name\":\"创新思维\",\"slotId\":\"axis_b\"},{\"name\":\"社交能力\",\"slotId\":\"axis_c\"},{\"name\":\"抗压能力\",\"slotId\":\"axis_d\"},{\"name\":\"自我认知\",\"slotId\":\"axis_e\"},{\"name\":\"团队协作\",\"slotId\":\"axis_f\"}],\"worldId\":\"custom:青春飞扬校…", + "publication_status": { + "Draft": [] + }, + "play_count": 0, + "like_count": 0, + "remix_count": 0, + "updated_at": { + "__timestamp_micros_since_unix_epoch__": 1777532006629209 + }, + "created_at": { + "__timestamp_micros_since_unix_epoch__": 1777531745887256 + } + } + ], + "match3d_work_profile": [], + "square_hole_work_profile": [], + "visual_novel_work_profile": [] + }, + "profileIds": { + "puzzle": [ + "profile-001", + "profile-002", + "profile-003" + ], + "customWorld": [ + "profile-081" + ], + "match3d": [], + "squareHole": [], + "bigFish": [], + "visualNovel": [] + }, + "workIds": { + "puzzle": [ + "work-001", + "work-002", + "work-003" + ], + "customWorld": [], + "match3d": [], + "squareHole": [], + "bigFish": [], + "visualNovel": [] + }, + "normalizedWorks": [ + { + "type": "puzzle", + "workId": "work-001", + "profileId": "profile-001", + "ownerUserId": "user-001", + "title": "化学家", + "subtitle": "几个文学家正站在山上面对着瀑布侃侃而谈", + "publicationStatus": { + "Published": [] + }, + "playCount": 1, + "likeCount": 0, + "remixCount": 1, + "coverImageSrc": "/generated-puzzle-assets/puzzle-session-f38101d7277040fcb6fbc41fea8b714a/puzzle-session-f38101d7277040fcb6fbc41fea8b714a-candidate-2/asset-1777649330373133/image.png", + "updatedAt": { + "__timestamp_micros_since_unix_epoch__": 1777703338322544 + } + }, + { + "type": "puzzle", + "workId": "work-002", + "profileId": "profile-002", + "ownerUserId": "user-002", + "title": "我不知道", + "subtitle": "你猜我是谁", + "publicationStatus": { + "Draft": [] + }, + "playCount": 0, + "likeCount": 0, + "remixCount": 0, + "updatedAt": { + "__timestamp_micros_since_unix_epoch__": 1777619351714201 + } + }, + { + "type": "puzzle", + "workId": "work-003", + "profileId": "profile-003", + "ownerUserId": "user-003", + "title": "", + "subtitle": "", + "publicationStatus": { + "Draft": [] + }, + "playCount": 0, + "likeCount": 0, + "remixCount": 0, + "updatedAt": { + "__timestamp_micros_since_unix_epoch__": 1777622285252380 + } + }, + { + "type": "customWorld", + "profileId": "profile-081", + "ownerUserId": "user-002", + "title": "青春飞扬校园", + "subtitle": "反内卷的自由学习之旅", + "publicationStatus": { + "Draft": [] + }, + "playCount": 0, + "likeCount": 0, + "remixCount": 0, + "updatedAt": { + "__timestamp_micros_since_unix_epoch__": 1777532006629209 + } + } + ] +} diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index ef78dd60..a74d29db 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -131,6 +131,7 @@ dependencies = [ "urlencoding", "uuid", "webp", + "windows-sys 0.61.2", "zip", ] diff --git a/server-rs/Cargo.toml b/server-rs/Cargo.toml index 6500ac2f..bddf6c17 100644 --- a/server-rs/Cargo.toml +++ b/server-rs/Cargo.toml @@ -117,6 +117,7 @@ opentelemetry-otlp = { version = "0.31", default-features = false, features = [" opentelemetry_sdk = { version = "0.31", default-features = false, features = ["trace", "metrics", "logs"] } tracing-opentelemetry = { version = "0.32", default-features = false } tracing-subscriber = "0.3" +windows-sys = "0.61" url = "2" urlencoding = "2" uuid = "1" diff --git a/server-rs/crates/api-server/Cargo.toml b/server-rs/crates/api-server/Cargo.toml index 92b07599..ce4ef1e6 100644 --- a/server-rs/crates/api-server/Cargo.toml +++ b/server-rs/crates/api-server/Cargo.toml @@ -11,6 +11,7 @@ base64 = { workspace = true } bytes = { workspace = true } dotenvy = { workspace = true } image = { workspace = true, features = ["jpeg", "png", "webp"] } +http-body-util = { workspace = true } reqwest = { workspace = true, features = ["json", "multipart", "rustls-tls"] } webp = { workspace = true } module-ai = { workspace = true } @@ -45,7 +46,7 @@ shared-kernel = { workspace = true } shared-logging = { workspace = true } socket2 = { workspace = true } spacetime-client = { workspace = true } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "time"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "net", "time", "sync"] } tokio-stream = { workspace = true } futures-util = { workspace = true } time = { workspace = true, features = ["formatting"] } @@ -57,6 +58,9 @@ urlencoding = { workspace = true } uuid = { workspace = true, features = ["v4"] } zip = { workspace = true, features = ["deflate"] } +[target.'cfg(windows)'.dependencies] +windows-sys = { workspace = true, features = ["Win32_Foundation", "Win32_System_Diagnostics_ToolHelp", "Win32_System_ProcessStatus", "Win32_System_Threading"] } + [dev-dependencies] base64 = { workspace = true } hmac = { workspace = true } diff --git a/server-rs/crates/api-server/src/api_response.rs b/server-rs/crates/api-server/src/api_response.rs index 35a8bc64..c9e7ffee 100644 --- a/server-rs/crates/api-server/src/api_response.rs +++ b/server-rs/crates/api-server/src/api_response.rs @@ -1,4 +1,13 @@ -use axum::Json; +use std::convert::Infallible; + +use axum::{ + Json, + body::Body, + http::{HeaderValue, header}, + response::{IntoResponse, Response}, +}; +use bytes::Bytes; +use futures_util::stream; use serde::Serialize; use serde_json::Value; #[cfg(test)] @@ -32,6 +41,30 @@ where Json(serde_json::to_value(data).unwrap_or(Value::Null)) } +pub fn json_success_data_bytes_response( + request_context: Option<&RequestContext>, + data_json: Bytes, +) -> Response { + if let Some(context) = request_context + && context.wants_envelope() + { + let meta = serde_json::to_vec(&build_api_response_meta(Some(context))) + .map(Bytes::from) + .unwrap_or_else(|_| Bytes::from_static(b"null")); + let chunks = [ + Bytes::from_static(b"{\"ok\":true,\"data\":"), + data_json, + Bytes::from_static(b",\"error\":null,\"meta\":"), + meta, + Bytes::from_static(b"}"), + ]; + let stream = stream::iter(chunks.into_iter().map(Ok::)); + return json_body_response(Body::from_stream(stream)); + } + + json_bytes_response(data_json) +} + pub fn json_error_body( request_context: Option<&RequestContext>, error: &ApiErrorPayload, @@ -65,6 +98,19 @@ fn build_api_response_meta(request_context: Option<&RequestContext>) -> ApiRespo ) } +fn json_bytes_response(bytes: Bytes) -> Response { + json_body_response(Body::from(bytes)) +} + +fn json_body_response(body: Body) -> Response { + let mut response = body.into_response(); + response.headers_mut().insert( + header::CONTENT_TYPE, + HeaderValue::from_static("application/json; charset=utf-8"), + ); + response +} + #[cfg(test)] mod tests { use super::*; @@ -106,6 +152,31 @@ mod tests { assert!(body.get("meta").is_none()); } + #[tokio::test] + async fn success_response_streams_cached_data_inside_standard_envelope() { + use http_body_util::BodyExt; + + let request_context = build_request_context(true); + let response = json_success_data_bytes_response( + Some(&request_context), + Bytes::from_static(br#"{"items":[]}"#), + ); + let body = response + .into_body() + .collect() + .await + .expect("response body should collect") + .to_bytes(); + let payload: Value = serde_json::from_slice(&body).expect("body should be json"); + + assert_eq!(payload["ok"], Value::Bool(true)); + assert_eq!(payload["data"]["items"], Value::Array(Vec::new())); + assert_eq!( + payload["meta"]["requestId"], + Value::String("req-test".to_string()) + ); + } + #[test] fn error_body_returns_legacy_shape_without_envelope_header() { let request_context = build_request_context(false); diff --git a/server-rs/crates/api-server/src/app.rs b/server-rs/crates/api-server/src/app.rs index 70cda406..ec886eb2 100644 --- a/server-rs/crates/api-server/src/app.rs +++ b/server-rs/crates/api-server/src/app.rs @@ -15,6 +15,7 @@ use tracing::{Level, Span, error, info_span}; use crate::{ auth::{AuthenticatedAccessToken, require_bearer_auth}, + backpressure::limit_concurrent_requests, creation_entry_config::require_creation_entry_route_enabled, error_middleware::normalize_error_response, modules, @@ -76,6 +77,11 @@ pub fn build_router(state: AppState) -> Router { state.clone(), require_creation_entry_route_enabled, )) + // HTTP 背压在业务路由外侧快拒绝,避免过载请求继续占用 SpacetimeDB facade 与业务执行资源。 + .layer(middleware::from_fn_with_state( + state.clone(), + limit_concurrent_requests, + )) // 错误归一化层放在 tracing 里侧,让 tracing 记录到最终对外返回的状态与错误体形态。 .layer(middleware::from_fn(normalize_error_response)) // 响应头回写放在错误归一化外侧,确保最终写回的是归一化后的最终响应。 diff --git a/server-rs/crates/api-server/src/backpressure.rs b/server-rs/crates/api-server/src/backpressure.rs new file mode 100644 index 00000000..6f9c5122 --- /dev/null +++ b/server-rs/crates/api-server/src/backpressure.rs @@ -0,0 +1,245 @@ +use std::sync::Arc; + +use axum::{ + body::Body, + extract::{Request, State}, + http::{HeaderValue, StatusCode, header::RETRY_AFTER}, + middleware::Next, + response::Response, +}; +use http_body_util::BodyExt; +use tokio::sync::{OwnedSemaphorePermit, TryAcquireError}; + +use crate::{ + http_error::AppError, + request_context::RequestContext, + state::{AppState, HttpRequestPermitPool}, +}; + +pub async fn limit_concurrent_requests( + State(state): State, + request: Request, + next: Next, +) -> Response { + if should_bypass_backpressure(&request) { + return next.run(request).await; + } + + let Some(permit_pool) = state.http_request_permit_pool() else { + return next.run(request).await; + }; + + match acquire_http_request_permit(permit_pool) { + Ok(permit) => hold_permit_until_response_body_dropped(next.run(request).await, permit), + Err(_) => reject_overloaded_request(&request), + } +} + +fn acquire_http_request_permit( + permit_pool: Arc, +) -> Result { + match permit_pool.clone().try_acquire_owned() { + Ok(permit) => { + crate::telemetry::update_http_request_permits_available(permit_pool.available_permits()); + Ok(HttpRequestPermitGuard { + permit: Some(permit), + permit_pool, + }) + } + Err(error) => { + crate::telemetry::update_http_request_permits_available(permit_pool.available_permits()); + Err(error) + } + } +} + +fn hold_permit_until_response_body_dropped( + response: Response, + permit: HttpRequestPermitGuard, +) -> Response { + response.map(|body| { + Body::new(body.map_frame(move |frame| { + let _permit_guard = &permit; + frame + })) + }) +} + +struct HttpRequestPermitGuard { + permit: Option, + permit_pool: Arc, +} + +impl Drop for HttpRequestPermitGuard { + fn drop(&mut self) { + drop(self.permit.take()); + crate::telemetry::update_http_request_permits_available(self.permit_pool.available_permits()); + } +} + +fn reject_overloaded_request(request: &Request) -> Response { + let request_context = request.extensions().get::().cloned(); + let mut response = AppError::from_status(StatusCode::TOO_MANY_REQUESTS) + .with_message("服务繁忙,请稍后重试") + .into_response_with_context(request_context.as_ref()); + response + .headers_mut() + .insert(RETRY_AFTER, HeaderValue::from_static("1")); + response +} + +fn should_bypass_backpressure(request: &Request) -> bool { + request.uri().path() == "/healthz" +} + +#[cfg(test)] +mod tests { + use std::sync::Arc; + + use axum::{ + Router, + body::Body, + extract::Extension, + http::{Request, StatusCode, header::RETRY_AFTER}, + middleware, + routing::get, + }; + use tokio::sync::Notify; + use tower::ServiceExt; + + use crate::{config::AppConfig, state::AppState}; + + use super::limit_concurrent_requests; + + #[derive(Clone)] + struct HeldRequestGate { + entered: Arc, + release: Arc, + } + + async fn held_request(Extension(gate): Extension) -> &'static str { + gate.entered.notify_one(); + gate.release.notified().await; + "ok" + } + + async fn fast_request() -> &'static str { + "ok" + } + + fn test_request(path: &str) -> Request { + Request::builder() + .uri(path) + .body(Body::empty()) + .expect("test request should build") + } + + fn build_test_app(max_concurrent_requests: usize, gate: HeldRequestGate) -> Router { + let mut config = AppConfig::default(); + config.max_concurrent_requests = Some(max_concurrent_requests); + let state = AppState::new(config).expect("state should build"); + + Router::new() + .route("/held", get(held_request)) + .route("/fast", get(fast_request)) + .route("/healthz", get(fast_request)) + .layer(middleware::from_fn_with_state( + state.clone(), + limit_concurrent_requests, + )) + .layer(Extension(gate)) + .with_state(state) + } + + #[tokio::test] + async fn returns_429_when_concurrency_permits_are_exhausted() { + let gate = HeldRequestGate { + entered: Arc::new(Notify::new()), + release: Arc::new(Notify::new()), + }; + let app = build_test_app(1, gate.clone()); + let entered = gate.entered.notified(); + + let held_response = tokio::spawn(app.clone().oneshot(test_request("/held"))); + entered.await; + + let rejected_response = app + .clone() + .oneshot(test_request("/fast")) + .await + .expect("rejected request should complete"); + assert_eq!(rejected_response.status(), StatusCode::TOO_MANY_REQUESTS); + assert_eq!( + rejected_response + .headers() + .get(RETRY_AFTER) + .and_then(|value| value.to_str().ok()), + Some("1") + ); + + gate.release.notify_one(); + let completed_response = held_response + .await + .expect("held request task should join") + .expect("held request should complete"); + assert_eq!(completed_response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn healthz_bypasses_concurrency_backpressure() { + let gate = HeldRequestGate { + entered: Arc::new(Notify::new()), + release: Arc::new(Notify::new()), + }; + let app = build_test_app(1, gate.clone()); + let entered = gate.entered.notified(); + + let held_response = tokio::spawn(app.clone().oneshot(test_request("/held"))); + entered.await; + + let health_response = app + .clone() + .oneshot(test_request("/healthz")) + .await + .expect("healthz request should complete"); + assert_eq!(health_response.status(), StatusCode::OK); + + gate.release.notify_one(); + let completed_response = held_response + .await + .expect("held request task should join") + .expect("held request should complete"); + assert_eq!(completed_response.status(), StatusCode::OK); + } + + #[tokio::test] + async fn permit_is_held_until_response_body_is_dropped() { + let gate = HeldRequestGate { + entered: Arc::new(Notify::new()), + release: Arc::new(Notify::new()), + }; + let app = build_test_app(1, gate); + + let first_response = app + .clone() + .oneshot(test_request("/fast")) + .await + .expect("first request should complete"); + assert_eq!(first_response.status(), StatusCode::OK); + + let rejected_response = app + .clone() + .oneshot(test_request("/fast")) + .await + .expect("second request should complete"); + assert_eq!(rejected_response.status(), StatusCode::TOO_MANY_REQUESTS); + + drop(first_response); + + let accepted_response = app + .oneshot(test_request("/fast")) + .await + .expect("third request should complete"); + assert_eq!(accepted_response.status(), StatusCode::OK); + } +} diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 00bc4fb4..13a62372 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -22,6 +22,7 @@ pub struct AppConfig { pub bind_port: u16, pub listen_backlog: i32, pub worker_threads: Option, + pub max_concurrent_requests: Option, pub log_filter: String, pub otel_enabled: bool, pub admin_username: Option, @@ -152,6 +153,7 @@ impl Default for AppConfig { bind_port: 3000, listen_backlog: 1024, worker_threads: None, + max_concurrent_requests: None, log_filter: "info,tower_http=info".to_string(), otel_enabled: false, admin_username: None, @@ -315,6 +317,11 @@ impl AppConfig { if let Some(worker_threads) = read_first_usize_env(&["GENARRATIVE_API_WORKER_THREADS"]) { config.worker_threads = Some(worker_threads); } + if let Some(max_concurrent_requests) = + read_first_usize_env(&["GENARRATIVE_API_MAX_CONCURRENT_REQUESTS"]) + { + config.max_concurrent_requests = Some(max_concurrent_requests); + } if let Some(otel_enabled) = read_first_bool_env(&["GENARRATIVE_OTEL_ENABLED"]) { config.otel_enabled = otel_enabled; } @@ -1198,20 +1205,24 @@ mod tests { unsafe { std::env::remove_var("GENARRATIVE_API_LISTEN_BACKLOG"); std::env::remove_var("GENARRATIVE_API_WORKER_THREADS"); + std::env::remove_var("GENARRATIVE_API_MAX_CONCURRENT_REQUESTS"); std::env::remove_var("GENARRATIVE_OTEL_ENABLED"); std::env::set_var("GENARRATIVE_API_LISTEN_BACKLOG", "2048"); std::env::set_var("GENARRATIVE_API_WORKER_THREADS", "6"); + std::env::set_var("GENARRATIVE_API_MAX_CONCURRENT_REQUESTS", "128"); std::env::set_var("GENARRATIVE_OTEL_ENABLED", "true"); } let config = AppConfig::from_env(); assert_eq!(config.listen_backlog, 2048); assert_eq!(config.worker_threads, Some(6)); + assert_eq!(config.max_concurrent_requests, Some(128)); assert!(config.otel_enabled); unsafe { std::env::remove_var("GENARRATIVE_API_LISTEN_BACKLOG"); std::env::remove_var("GENARRATIVE_API_WORKER_THREADS"); + std::env::remove_var("GENARRATIVE_API_MAX_CONCURRENT_REQUESTS"); std::env::remove_var("GENARRATIVE_OTEL_ENABLED"); } } diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index d1f15cd9..665f3526 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -13,6 +13,7 @@ mod auth_payload; mod auth_public_user; mod auth_session; mod auth_sessions; +mod backpressure; mod bark_battle; mod big_fish; mod big_fish_agent_turn; @@ -55,9 +56,11 @@ mod password_management; mod phone_auth; mod platform_errors; mod profile_identity; +mod process_metrics; mod prompt; mod puzzle; mod puzzle_agent_turn; +mod puzzle_gallery_cache; mod refresh_session; mod registration_reward; mod request_context; @@ -138,6 +141,8 @@ async fn run_server(config: AppConfig) -> Result<(), io::Error> { enabled: config.otel_enabled, }, )?; + process_metrics::register_process_metrics(); + telemetry::register_http_runtime_metrics(); let bind_address = config.bind_socket_addr(); let listen_backlog = config.listen_backlog; @@ -148,6 +153,7 @@ async fn run_server(config: AppConfig) -> Result<(), io::Error> { let state = restore_app_state_for_startup(config) .await .map_err(|error| std::io::Error::other(format!("初始化应用状态失败:{error}")))?; + state.puzzle_gallery_cache().spawn_cleanup_task(); let router = build_router(state); info!( diff --git a/server-rs/crates/api-server/src/process_metrics.rs b/server-rs/crates/api-server/src/process_metrics.rs new file mode 100644 index 00000000..5f27c8b8 --- /dev/null +++ b/server-rs/crates/api-server/src/process_metrics.rs @@ -0,0 +1,306 @@ +use std::sync::OnceLock; + +use opentelemetry::global; +use tracing::warn; + +// 进程指标只描述 api-server 自身,不携带请求、用户或作品维度,避免 OTLP 指标高基数膨胀。 +pub(crate) fn register_process_metrics() { + static REGISTERED: OnceLock<()> = OnceLock::new(); + REGISTERED.get_or_init(register_process_metrics_once); +} + +fn register_process_metrics_once() { + let meter = global::meter("genarrative-api"); + + meter + .i64_observable_up_down_counter("process.memory.usage") + .with_unit("By") + .with_description("api-server process physical memory usage") + .with_callback(|observer| { + let Some(snapshot) = ProcessMetricsSnapshot::collect() else { + return; + }; + observer.observe(to_i64(snapshot.rss_bytes), &[]); + }) + .build(); + + meter + .i64_observable_up_down_counter("process.memory.virtual") + .with_unit("By") + .with_description("api-server committed virtual memory") + .with_callback(|observer| { + let Some(snapshot) = ProcessMetricsSnapshot::collect() else { + return; + }; + if let Some(virtual_bytes) = snapshot.virtual_bytes { + observer.observe(to_i64(virtual_bytes), &[]); + } + }) + .build(); + + meter + .i64_observable_up_down_counter("genarrative.process.memory.private") + .with_unit("By") + .with_description("api-server private memory for local diagnostics") + .with_callback(|observer| { + let Some(snapshot) = ProcessMetricsSnapshot::collect() else { + return; + }; + if let Some(private_bytes) = snapshot.private_bytes { + observer.observe(to_i64(private_bytes), &[]); + } + }) + .build(); + + meter + .i64_observable_up_down_counter("process.thread.count") + .with_unit("{thread}") + .with_description("api-server process thread count") + .with_callback(|observer| { + let Some(snapshot) = ProcessMetricsSnapshot::collect() else { + return; + }; + observer.observe(to_i64(snapshot.thread_count), &[]); + }) + .build(); + + meter + .i64_observable_up_down_counter("process.windows.handle.count") + .with_unit("{handle}") + .with_description("api-server process handle count on Windows") + .with_callback(|observer| { + let Some(snapshot) = ProcessMetricsSnapshot::collect() else { + return; + }; + if let Some(handle_count) = snapshot.windows_handle_count { + observer.observe(to_i64(handle_count), &[]); + } + }) + .build(); + + meter + .i64_observable_up_down_counter("process.unix.file_descriptor.count") + .with_unit("{file_descriptor}") + .with_description("api-server process file descriptor count on Unix") + .with_callback(|observer| { + let Some(snapshot) = ProcessMetricsSnapshot::collect() else { + return; + }; + if let Some(fd_count) = snapshot.unix_fd_count { + observer.observe(to_i64(fd_count), &[]); + } + }) + .build(); +} + +fn to_i64(value: u64) -> i64 { + value.min(i64::MAX as u64) as i64 +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +struct ProcessMetricsSnapshot { + rss_bytes: u64, + private_bytes: Option, + virtual_bytes: Option, + thread_count: u64, + windows_handle_count: Option, + unix_fd_count: Option, +} + +impl ProcessMetricsSnapshot { + fn collect() -> Option { + collect_process_metrics() + .inspect_err(|error| { + warn!(%error, "采集 api-server 进程内存指标失败"); + }) + .ok() + } +} + +#[cfg(windows)] +fn collect_process_metrics() -> Result { + use windows_sys::Win32::{ + System::{ + ProcessStatus::{GetProcessMemoryInfo, PROCESS_MEMORY_COUNTERS_EX}, + Threading::{GetCurrentProcess, GetCurrentProcessId, GetProcessHandleCount}, + }, + }; + + let handle = unsafe { GetCurrentProcess() }; + let mut counters = PROCESS_MEMORY_COUNTERS_EX { + cb: std::mem::size_of::() as u32, + ..Default::default() + }; + let ok = unsafe { + GetProcessMemoryInfo( + handle, + std::ptr::addr_of_mut!(counters).cast(), + counters.cb, + ) + }; + if ok == 0 { + return Err("GetProcessMemoryInfo returned false".to_string()); + } + + let mut handle_count = 0_u32; + let handle_count = if unsafe { GetProcessHandleCount(handle, &mut handle_count) } == 0 { + None + } else { + Some(u64::from(handle_count)) + }; + + Ok(ProcessMetricsSnapshot { + rss_bytes: counters.WorkingSetSize as u64, + private_bytes: Some(counters.PrivateUsage as u64), + virtual_bytes: Some(counters.PrivateUsage as u64), + thread_count: u64::from(unsafe { GetCurrentProcessId() }.thread_count()?), + windows_handle_count: handle_count, + unix_fd_count: None, + }) +} + +#[cfg(windows)] +trait WindowsProcessThreadCount { + fn thread_count(self) -> Result; +} + +#[cfg(windows)] +impl WindowsProcessThreadCount for u32 { + fn thread_count(self) -> Result { + use windows_sys::Win32::{ + Foundation::{CloseHandle, INVALID_HANDLE_VALUE}, + System::Diagnostics::ToolHelp::{ + CreateToolhelp32Snapshot, PROCESSENTRY32, Process32First, Process32Next, + TH32CS_SNAPPROCESS, + }, + }; + + let snapshot = unsafe { CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0) }; + if snapshot == INVALID_HANDLE_VALUE { + return Err("CreateToolhelp32Snapshot returned INVALID_HANDLE_VALUE".to_string()); + } + + let mut entry = PROCESSENTRY32 { + dwSize: std::mem::size_of::() as u32, + ..Default::default() + }; + let mut found = None; + let mut ok = unsafe { Process32First(snapshot, &mut entry) }; + while ok != 0 { + if entry.th32ProcessID == self { + found = Some(entry.cntThreads); + break; + } + ok = unsafe { Process32Next(snapshot, &mut entry) }; + } + unsafe { + CloseHandle(snapshot); + } + + found.ok_or_else(|| format!("process {self} not found in ToolHelp snapshot")) + } +} + +#[cfg(target_os = "linux")] +fn collect_process_metrics() -> Result { + let status = std::fs::read_to_string("/proc/self/status") + .map_err(|error| format!("read /proc/self/status failed: {error}"))?; + let statm = std::fs::read_to_string("/proc/self/statm") + .map_err(|error| format!("read /proc/self/statm failed: {error}"))?; + let page_size = linux_page_size_bytes()?; + + let rss_bytes = parse_status_kb(&status, "VmRSS:") + .map(|value| value * 1024) + .or_else(|| parse_statm_pages(&statm, 1).map(|value| value * page_size)) + .ok_or_else(|| "missing VmRSS/statm resident field".to_string())?; + let virtual_bytes = parse_status_kb(&status, "VmSize:") + .map(|value| value * 1024) + .or_else(|| parse_statm_pages(&statm, 0).map(|value| value * page_size)) + .ok_or_else(|| "missing VmSize/statm size field".to_string())?; + let private_bytes = parse_status_kb(&status, "VmData:").map(|value| value * 1024); + let thread_count = parse_status_u64(&status, "Threads:") + .ok_or_else(|| "missing Threads field".to_string())?; + + Ok(ProcessMetricsSnapshot { + rss_bytes, + private_bytes, + virtual_bytes: Some(virtual_bytes), + thread_count, + windows_handle_count: None, + unix_fd_count: linux_fd_count(), + }) +} + +#[cfg(target_os = "linux")] +fn linux_page_size_bytes() -> Result { + let output = std::process::Command::new("getconf") + .arg("PAGESIZE") + .output() + .map_err(|error| format!("getconf PAGESIZE failed: {error}"))?; + if !output.status.success() { + return Err(format!("getconf PAGESIZE exited with {}", output.status)); + } + let text = String::from_utf8(output.stdout) + .map_err(|error| format!("getconf PAGESIZE output is not utf8: {error}"))?; + text.trim() + .parse::() + .map_err(|error| format!("parse PAGESIZE failed: {error}")) +} + +#[cfg(target_os = "linux")] +fn linux_fd_count() -> Option { + let entries = std::fs::read_dir("/proc/self/fd").ok()?; + Some(entries.filter_map(Result::ok).count() as u64) +} + +#[cfg(target_os = "linux")] +fn parse_status_kb(status: &str, key: &str) -> Option { + parse_status_u64(status, key) +} + +#[cfg(target_os = "linux")] +fn parse_status_u64(status: &str, key: &str) -> Option { + status.lines().find_map(|line| { + let rest = line.strip_prefix(key)?.trim(); + rest.split_whitespace().next()?.parse::().ok() + }) +} + +#[cfg(target_os = "linux")] +fn parse_statm_pages(statm: &str, index: usize) -> Option { + statm + .split_whitespace() + .nth(index)? + .parse::() + .ok() +} + +#[cfg(not(any(windows, target_os = "linux")))] +fn collect_process_metrics() -> Result { + Err("process metrics are only implemented for Windows and Linux".to_string()) +} + +#[cfg(test)] +mod tests { + #[cfg(target_os = "linux")] + use super::{parse_statm_pages, parse_status_kb, parse_status_u64}; + + #[cfg(target_os = "linux")] + #[test] + fn parses_linux_proc_status_memory_fields() { + let status = "Name:\tapi-server\nVmSize:\t 123456 kB\nVmRSS:\t 7890 kB\nVmData:\t 3456 kB\nThreads:\t37\n"; + + assert_eq!(parse_status_kb(status, "VmRSS:"), Some(7890)); + assert_eq!(parse_status_kb(status, "VmSize:"), Some(123456)); + assert_eq!(parse_status_kb(status, "VmData:"), Some(3456)); + assert_eq!(parse_status_u64(status, "Threads:"), Some(37)); + } + + #[cfg(target_os = "linux")] + #[test] + fn parses_linux_statm_pages() { + assert_eq!(parse_statm_pages("100 20 0 0 0 0 0", 0), Some(100)); + assert_eq!(parse_statm_pages("100 20 0 0 0 0 0", 1), Some(20)); + assert_eq!(parse_statm_pages("100 20", 7), None); + } +} diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index 24999dff..4619c613 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -38,7 +38,7 @@ use shared_contracts::{ PuzzleResultPreviewBlockerResponse, PuzzleResultPreviewEnvelopeResponse, PuzzleResultPreviewFindingResponse, SendPuzzleAgentMessageRequest, }, - puzzle_gallery::{PuzzleGalleryDetailResponse, PuzzleGalleryResponse}, + puzzle_gallery::PuzzleGalleryDetailResponse, puzzle_runtime::{ AdvancePuzzleNextLevelRequest, DragPuzzlePieceRequest, PuzzleBoardSnapshotResponse, PuzzleCellPositionResponse, PuzzleLeaderboardEntryResponse, PuzzleMergedGroupStateResponse, @@ -59,16 +59,16 @@ use spacetime_client::{ PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleAudioAssetRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord, - PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, PuzzleGeneratedImageCandidateRecord, - PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord, - PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, - PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, - PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunPauseRecordInput, - PuzzleRunPropRecordInput, PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, - PuzzleSelectCoverImageRecordInput, PuzzleUiBackgroundSaveRecordInput, - PuzzleWorkLikeReportRecordInput, PuzzleWorkPointIncentiveClaimRecordInput, - PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, PuzzleWorkUpsertRecordInput, - SpacetimeClientError, + PuzzleFormDraftRecord, PuzzleFormDraftSaveRecordInput, PuzzleGalleryCardRecord, + PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, + PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzlePublishRecordInput, + PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, + PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, + PuzzleRunPauseRecordInput, PuzzleRunPropRecordInput, PuzzleRunRecord, + PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleSelectCoverImageRecordInput, + PuzzleUiBackgroundSaveRecordInput, PuzzleWorkLikeReportRecordInput, + PuzzleWorkPointIncentiveClaimRecordInput, PuzzleWorkProfileRecord, PuzzleWorkRemixRecordInput, + PuzzleWorkUpsertRecordInput, SpacetimeClientError, }; use std::convert::Infallible; @@ -103,6 +103,7 @@ use crate::{ PuzzleAgentTurnRequest, build_failed_finalize_record_input, build_finalize_record_input, run_puzzle_agent_turn, }, + puzzle_gallery_cache::{build_puzzle_gallery_window_response, puzzle_gallery_cached_json}, request_context::RequestContext, state::AppState, vector_engine_audio_generation::{ @@ -1528,7 +1529,19 @@ pub async fn claim_puzzle_work_point_incentive( pub async fn list_puzzle_gallery( State(state): State, Extension(request_context): Extension, -) -> Result, Response> { +) -> Result { + if let Some(response) = state.puzzle_gallery_cache().read_fresh_response().await { + crate::telemetry::record_puzzle_gallery_cache_hit(); + return Ok(puzzle_gallery_cached_json(&request_context, response)); + } + crate::telemetry::record_puzzle_gallery_cache_miss(); + let _rebuild_guard = state.puzzle_gallery_cache().acquire_rebuild_guard().await; + if let Some(response) = state.puzzle_gallery_cache().read_fresh_response().await { + crate::telemetry::record_puzzle_gallery_cache_hit(); + return Ok(puzzle_gallery_cached_json(&request_context, response)); + } + + let rebuild_started_at = std::time::Instant::now(); let items = state .spacetime_client() .list_puzzle_gallery() @@ -1541,15 +1554,32 @@ pub async fn list_puzzle_gallery( ) })?; - Ok(json_success_body( - Some(&request_context), - PuzzleGalleryResponse { - items: items - .into_iter() - .map(|item| map_puzzle_work_summary_response(&state, item)) - .collect(), - }, - )) + let response = build_puzzle_gallery_window_response( + items + .into_iter() + .map(|item| map_puzzle_gallery_card_response(&state, item)) + .collect(), + ); + let cached_response = state + .puzzle_gallery_cache() + .store_response(response) + .await + .map_err(|error| { + puzzle_error_response( + &request_context, + PUZZLE_GALLERY_PROVIDER, + AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ + "provider": PUZZLE_GALLERY_PROVIDER, + "message": format!("拼图广场缓存序列化失败:{error}"), + })), + ) + })?; + crate::telemetry::record_puzzle_gallery_cache_rebuild( + rebuild_started_at.elapsed(), + cached_response.data_json_len(), + ); + + Ok(puzzle_gallery_cached_json(&request_context, cached_response)) } pub async fn get_puzzle_gallery_detail( diff --git a/server-rs/crates/api-server/src/puzzle/mappers.rs b/server-rs/crates/api-server/src/puzzle/mappers.rs index daefe7d3..e988809c 100644 --- a/server-rs/crates/api-server/src/puzzle/mappers.rs +++ b/server-rs/crates/api-server/src/puzzle/mappers.rs @@ -342,6 +342,49 @@ pub(super) fn map_puzzle_work_summary_response( } } +pub(super) fn map_puzzle_gallery_card_response( + state: &AppState, + item: PuzzleGalleryCardRecord, +) -> PuzzleWorkSummaryResponse { + let author = resolve_work_author_by_user_id( + state, + &item.owner_user_id, + Some(&item.author_display_name), + None, + ); + PuzzleWorkSummaryResponse { + work_id: item.work_id, + profile_id: item.profile_id, + owner_user_id: item.owner_user_id, + source_session_id: item.source_session_id, + author_display_name: author.display_name, + work_title: item.work_title, + work_description: item.work_description, + level_name: item.level_name, + summary: item.summary, + theme_tags: item.theme_tags, + cover_image_src: item.cover_image_src, + cover_asset_id: item.cover_asset_id, + publication_status: item.publication_status, + updated_at: item.updated_at, + published_at: item.published_at, + play_count: item.play_count, + remix_count: item.remix_count, + like_count: item.like_count, + recent_play_count_7d: item.recent_play_count_7d, + point_incentive_total_half_points: item.point_incentive_total_half_points, + point_incentive_claimed_points: item.point_incentive_claimed_points, + point_incentive_total_points: item.point_incentive_total_half_points as f64 / 2.0, + point_incentive_claimable_points: item + .point_incentive_total_half_points + .saturating_div(2) + .saturating_sub(item.point_incentive_claimed_points), + publish_ready: item.publish_ready, + generation_status: item.generation_status, + levels: Vec::new(), + } +} + pub(super) fn map_puzzle_work_profile_response( state: &AppState, item: PuzzleWorkProfileRecord, diff --git a/server-rs/crates/api-server/src/puzzle_gallery_cache.rs b/server-rs/crates/api-server/src/puzzle_gallery_cache.rs new file mode 100644 index 00000000..a6b9eb7d --- /dev/null +++ b/server-rs/crates/api-server/src/puzzle_gallery_cache.rs @@ -0,0 +1,208 @@ +use std::{ + sync::Arc, + time::{Duration, Instant}, +}; + +use axum::response::Response; +use bytes::Bytes; +use shared_contracts::{ + puzzle_gallery::{PuzzleGalleryResponse, PuzzleGalleryWorkRefResponse}, + puzzle_works::PuzzleWorkSummaryResponse, +}; +use tokio::{ + sync::{Mutex, MutexGuard, RwLock}, + time, +}; + +use crate::{api_response::json_success_data_bytes_response, request_context::RequestContext}; + +const PUZZLE_GALLERY_PRIMARY_ITEM_COUNT: usize = 10; +const PUZZLE_GALLERY_PREVIEW_REF_COUNT: usize = 10; +const PUZZLE_GALLERY_CACHE_TTL: Duration = Duration::from_secs(5); +const PUZZLE_GALLERY_CACHE_MAX_IDLE: Duration = Duration::from_secs(300); +const PUZZLE_GALLERY_CACHE_CLEANUP_INTERVAL: Duration = Duration::from_secs(60); + +#[derive(Clone, Debug)] +pub struct PuzzleGalleryCache { + inner: Arc>>, + rebuild_lock: Arc>, +} + +#[derive(Clone, Debug)] +struct PuzzleGalleryCacheEntry { + data_json: Bytes, + built_at: Instant, +} + +#[derive(Clone, Debug)] +pub struct PuzzleGalleryCachedResponse { + data_json: Bytes, +} + +impl PuzzleGalleryCachedResponse { + pub fn data_json_len(&self) -> usize { + self.data_json.len() + } +} + +impl PuzzleGalleryCache { + pub fn new() -> Self { + Self { + inner: Arc::new(RwLock::new(None)), + rebuild_lock: Arc::new(Mutex::new(())), + } + } + + pub async fn acquire_rebuild_guard(&self) -> MutexGuard<'_, ()> { + self.rebuild_lock.lock().await + } + + pub async fn read_fresh_response(&self) -> Option { + let guard = self.inner.read().await; + let entry = guard.as_ref()?; + let now = Instant::now(); + if now.duration_since(entry.built_at) > PUZZLE_GALLERY_CACHE_TTL { + return None; + } + Some(PuzzleGalleryCachedResponse { + data_json: entry.data_json.clone(), + }) + } + + pub async fn store_response( + &self, + response: PuzzleGalleryResponse, + ) -> Result { + let now = Instant::now(); + let cached = PuzzleGalleryCachedResponse { + data_json: Bytes::from(serde_json::to_vec(&response)?), + }; + *self.inner.write().await = Some(PuzzleGalleryCacheEntry { + data_json: cached.data_json.clone(), + built_at: now, + }); + Ok(cached) + } + + pub fn spawn_cleanup_task(&self) { + let cache = self.clone(); + tokio::spawn(async move { + let mut interval = time::interval(PUZZLE_GALLERY_CACHE_CLEANUP_INTERVAL); + loop { + interval.tick().await; + cache.cleanup_idle_entry().await; + } + }); + } + + async fn cleanup_idle_entry(&self) { + let mut guard = self.inner.write().await; + if let Some(entry) = guard.as_ref() + && Instant::now().duration_since(entry.built_at) > PUZZLE_GALLERY_CACHE_MAX_IDLE + { + *guard = None; + } + } +} + +pub fn build_puzzle_gallery_window_response( + items: Vec, +) -> PuzzleGalleryResponse { + let total_count = items.len().min(u32::MAX as usize) as u32; + let preview_refs = items + .iter() + .skip(PUZZLE_GALLERY_PRIMARY_ITEM_COUNT) + .take(PUZZLE_GALLERY_PREVIEW_REF_COUNT) + .map(|item| PuzzleGalleryWorkRefResponse { + work_id: item.work_id.clone(), + profile_id: item.profile_id.clone(), + }) + .collect::>(); + let next_cursor = items + .get(PUZZLE_GALLERY_PRIMARY_ITEM_COUNT + PUZZLE_GALLERY_PREVIEW_REF_COUNT) + .map(|item| item.profile_id.clone()); + let has_more = + items.len() > PUZZLE_GALLERY_PRIMARY_ITEM_COUNT + PUZZLE_GALLERY_PREVIEW_REF_COUNT; + + PuzzleGalleryResponse { + items: items + .into_iter() + .take(PUZZLE_GALLERY_PRIMARY_ITEM_COUNT) + .collect(), + preview_refs, + has_more, + next_cursor, + total_count, + } +} + +pub fn puzzle_gallery_cached_json( + request_context: &RequestContext, + response: PuzzleGalleryCachedResponse, +) -> Response { + json_success_data_bytes_response(Some(request_context), response.data_json) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn build_summary(index: usize) -> PuzzleWorkSummaryResponse { + PuzzleWorkSummaryResponse { + work_id: format!("work-{index}"), + profile_id: format!("profile-{index}"), + owner_user_id: "user-1".to_string(), + source_session_id: None, + author_display_name: "作者".to_string(), + work_title: format!("作品 {index}"), + work_description: "描述".to_string(), + level_name: "第一关".to_string(), + summary: "摘要".to_string(), + theme_tags: Vec::new(), + cover_image_src: None, + cover_asset_id: None, + publication_status: "published".to_string(), + updated_at: "2026-05-01T00:00:00Z".to_string(), + published_at: None, + play_count: 0, + remix_count: 0, + like_count: 0, + recent_play_count_7d: 0, + point_incentive_total_half_points: 0, + point_incentive_claimed_points: 0, + point_incentive_total_points: 0.0, + point_incentive_claimable_points: 0, + publish_ready: true, + generation_status: Some("ready".to_string()), + levels: Vec::new(), + } + } + + #[test] + fn build_window_returns_primary_cards_preview_refs_and_cursor() { + let response = + build_puzzle_gallery_window_response((0..25).map(build_summary).collect::>()); + + assert_eq!(response.total_count, 25); + assert_eq!(response.items.len(), 10); + assert_eq!(response.preview_refs.len(), 10); + assert_eq!(response.items[0].profile_id, "profile-0"); + assert_eq!(response.items[9].profile_id, "profile-9"); + assert_eq!(response.preview_refs[0].profile_id, "profile-10"); + assert_eq!(response.preview_refs[9].profile_id, "profile-19"); + assert!(response.has_more); + assert_eq!(response.next_cursor.as_deref(), Some("profile-20")); + } + + #[test] + fn build_window_handles_short_gallery_without_more_cursor() { + let response = + build_puzzle_gallery_window_response((0..8).map(build_summary).collect::>()); + + assert_eq!(response.total_count, 8); + assert_eq!(response.items.len(), 8); + assert!(response.preview_refs.is_empty()); + assert!(!response.has_more); + assert_eq!(response.next_cursor, None); + } +} diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index ee4b9a7e..2e2e690e 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -27,20 +27,25 @@ use shared_contracts::creation_entry_config::CreationEntryConfigResponse; use shared_contracts::creative_agent::CreativeAgentSessionSnapshot; use spacetime_client::{SpacetimeClient, SpacetimeClientConfig, SpacetimeClientError}; use time::OffsetDateTime; +use tokio::sync::Semaphore; use tracing::{info, warn}; use crate::config::AppConfig; +use crate::puzzle_gallery_cache::PuzzleGalleryCache; use crate::wechat_pay::{WechatPayClient, map_wechat_pay_init_error}; use crate::wechat_provider::build_wechat_provider; const ADMIN_ROLE: &str = "admin"; +pub type HttpRequestPermitPool = Semaphore; + // 当前阶段先保留最小共享状态壳,后续逐步接入配置、客户端与平台适配。 #[derive(Clone, Debug)] pub struct AppState { // 配置会在后续中间件、路由和平台适配接入时逐步消费。 #[allow(dead_code)] pub config: AppConfig, + http_request_permit_pool: Option>, auth_jwt_config: JwtConfig, admin_runtime: Option, refresh_cookie_config: RefreshCookieConfig, @@ -60,6 +65,7 @@ pub struct AppState { #[cfg_attr(not(test), allow(dead_code))] ai_task_service: AiTaskService, spacetime_client: SpacetimeClient, + puzzle_gallery_cache: PuzzleGalleryCache, llm_client: Option, creative_agent_gpt5_client: Option, creative_agent_executor: Arc, @@ -192,9 +198,14 @@ impl AppState { }); let llm_client = build_llm_client(&config)?; let creative_agent_gpt5_client = build_creative_agent_gpt5_client(&config)?; + let http_request_permit_pool = config + .max_concurrent_requests + .map(HttpRequestPermitPool::new) + .map(Arc::new); Ok(Self { config, + http_request_permit_pool, auth_jwt_config, admin_runtime, refresh_cookie_config, @@ -214,6 +225,7 @@ impl AppState { wechat_pay_client, ai_task_service, spacetime_client, + puzzle_gallery_cache: PuzzleGalleryCache::new(), llm_client, creative_agent_gpt5_client, creative_agent_executor: Arc::new(MockLangChainRustAgentExecutor), @@ -235,6 +247,10 @@ impl AppState { &self.refresh_cookie_config } + pub fn http_request_permit_pool(&self) -> Option> { + self.http_request_permit_pool.clone() + } + pub async fn upsert_creation_entry_type_config( &self, input: module_runtime::CreationEntryTypeAdminUpsertInput, @@ -464,6 +480,10 @@ impl AppState { &self.spacetime_client } + pub fn puzzle_gallery_cache(&self) -> &PuzzleGalleryCache { + &self.puzzle_gallery_cache + } + pub fn llm_client(&self) -> Option<&LlmClient> { self.llm_client.as_ref() } diff --git a/server-rs/crates/api-server/src/telemetry.rs b/server-rs/crates/api-server/src/telemetry.rs index 40347d8d..39643976 100644 --- a/server-rs/crates/api-server/src/telemetry.rs +++ b/server-rs/crates/api-server/src/telemetry.rs @@ -4,11 +4,19 @@ use axum::{ http::{HeaderMap, Request, Response}, middleware::Next, }; +use http_body_util::BodyExt; use opentelemetry::{KeyValue, global, metrics::Counter}; +use std::sync::{ + Arc, OnceLock, + atomic::{AtomicI64, Ordering}, +}; use tracing::{info, warn}; use crate::{request_context::resolve_request_id, state::AppState}; +static HTTP_RESPONSE_BODY_IN_FLIGHT: AtomicI64 = AtomicI64::new(0); +static HTTP_REQUEST_PERMITS_AVAILABLE: OnceLock> = OnceLock::new(); + // 集中维护 api-server HTTP 观测,避免在 handler 中散落高基数字段或重复创建 instrument。 pub async fn record_http_observability( State(state): State, @@ -67,7 +75,46 @@ pub async fn record_http_observability( ); } - response + track_response_body_in_flight(response) +} + +pub(crate) fn update_http_request_permits_available(available: usize) { + let gauge = HTTP_REQUEST_PERMITS_AVAILABLE.get_or_init(|| { + let gauge = Arc::new(AtomicI64::new(0)); + register_http_request_permits_available_metric(gauge.clone()); + gauge + }); + gauge.store(available.min(i64::MAX as usize) as i64, Ordering::Relaxed); +} + +pub(crate) fn record_puzzle_gallery_cache_hit() { + puzzle_gallery_cache_metrics().hits.add(1, &[]); +} + +pub(crate) fn record_puzzle_gallery_cache_miss() { + puzzle_gallery_cache_metrics().misses.add(1, &[]); +} + +pub(crate) fn record_puzzle_gallery_cache_rebuild(duration: std::time::Duration, data_bytes: usize) { + let metrics = puzzle_gallery_cache_metrics(); + metrics.rebuilds.add(1, &[]); + metrics + .rebuild_duration + .record(duration.as_secs_f64(), &[]); + metrics + .data_json_bytes + .record(data_bytes.min(u64::MAX as usize) as u64, &[]); +} + +fn track_response_body_in_flight(response: Response) -> Response { + response.map(|body| { + HTTP_RESPONSE_BODY_IN_FLIGHT.fetch_add(1, Ordering::Relaxed); + let guard = ResponseBodyInFlightGuard; + Body::new(body.map_frame(move |frame| { + let _guard = &guard; + frame + })) + }) } struct HttpMetrics { @@ -76,6 +123,22 @@ struct HttpMetrics { duration: opentelemetry::metrics::Histogram, } +struct PuzzleGalleryCacheMetrics { + hits: Counter, + misses: Counter, + rebuilds: Counter, + rebuild_duration: opentelemetry::metrics::Histogram, + data_json_bytes: opentelemetry::metrics::Histogram, +} + +struct ResponseBodyInFlightGuard; + +impl Drop for ResponseBodyInFlightGuard { + fn drop(&mut self) { + HTTP_RESPONSE_BODY_IN_FLIGHT.fetch_sub(1, Ordering::Relaxed); + } +} + fn http_metrics() -> &'static HttpMetrics { static METRICS: std::sync::OnceLock = std::sync::OnceLock::new(); METRICS.get_or_init(|| { @@ -99,6 +162,64 @@ fn http_metrics() -> &'static HttpMetrics { }) } +fn puzzle_gallery_cache_metrics() -> &'static PuzzleGalleryCacheMetrics { + static METRICS: std::sync::OnceLock = std::sync::OnceLock::new(); + METRICS.get_or_init(|| { + let meter = global::meter("genarrative-api"); + PuzzleGalleryCacheMetrics { + hits: meter + .u64_counter("genarrative.puzzle_gallery.cache.hits") + .with_description("Puzzle gallery response cache hits") + .build(), + misses: meter + .u64_counter("genarrative.puzzle_gallery.cache.misses") + .with_description("Puzzle gallery response cache misses") + .build(), + rebuilds: meter + .u64_counter("genarrative.puzzle_gallery.cache.rebuilds") + .with_description("Puzzle gallery response cache rebuild count") + .build(), + rebuild_duration: meter + .f64_histogram("genarrative.puzzle_gallery.cache.rebuild.duration") + .with_unit("s") + .with_description("Puzzle gallery response cache rebuild duration") + .build(), + data_json_bytes: meter + .u64_histogram("genarrative.puzzle_gallery.cache.data_json_bytes") + .with_unit("By") + .with_description("Serialized puzzle gallery data JSON size") + .build(), + } + }) +} + +fn register_http_request_permits_available_metric(gauge: Arc) { + let meter = global::meter("genarrative-api"); + meter + .i64_observable_up_down_counter("genarrative.http.server.request_permits.available") + .with_unit("{permit}") + .with_description("Available api-server HTTP backpressure permits") + .with_callback(move |observer| { + observer.observe(gauge.load(Ordering::Relaxed), &[]); + }) + .build(); +} + +pub(crate) fn register_http_runtime_metrics() { + static REGISTERED: OnceLock<()> = OnceLock::new(); + REGISTERED.get_or_init(|| { + let meter = global::meter("genarrative-api"); + meter + .i64_observable_up_down_counter("genarrative.http.server.response_bodies.in_flight") + .with_unit("{response}") + .with_description("HTTP response bodies still owned by Axum/Hyper") + .with_callback(|observer| { + observer.observe(HTTP_RESPONSE_BODY_IN_FLIGHT.load(Ordering::Relaxed), &[]); + }) + .build(); + }); +} + fn http_base_labels(method: String, route: String) -> Vec { vec![ KeyValue::new("http.request.method", method), diff --git a/server-rs/crates/module-bark-battle/src/application.rs b/server-rs/crates/module-bark-battle/src/application.rs new file mode 100644 index 00000000..840d5977 --- /dev/null +++ b/server-rs/crates/module-bark-battle/src/application.rs @@ -0,0 +1 @@ +//! 中文注释:汪汪声浪领域应用服务预留落位,当前规则仍集中在 domain/scoring。 diff --git a/server-rs/crates/module-bark-battle/src/commands.rs b/server-rs/crates/module-bark-battle/src/commands.rs new file mode 100644 index 00000000..c6be3434 --- /dev/null +++ b/server-rs/crates/module-bark-battle/src/commands.rs @@ -0,0 +1 @@ +//! 中文注释:汪汪声浪命令归一化预留落位,当前无独立命令构造。 diff --git a/server-rs/crates/module-bark-battle/src/errors.rs b/server-rs/crates/module-bark-battle/src/errors.rs new file mode 100644 index 00000000..06ea419b --- /dev/null +++ b/server-rs/crates/module-bark-battle/src/errors.rs @@ -0,0 +1 @@ +//! 中文注释:汪汪声浪领域错误预留落位,当前复用调用方错误文本。 diff --git a/server-rs/crates/module-bark-battle/src/events.rs b/server-rs/crates/module-bark-battle/src/events.rs new file mode 100644 index 00000000..fc838aae --- /dev/null +++ b/server-rs/crates/module-bark-battle/src/events.rs @@ -0,0 +1 @@ +//! 中文注释:汪汪声浪领域事件预留落位,当前不导出独立事件类型。 diff --git a/server-rs/crates/module-bark-battle/src/lib.rs b/server-rs/crates/module-bark-battle/src/lib.rs index b587645a..64a3b54d 100644 --- a/server-rs/crates/module-bark-battle/src/lib.rs +++ b/server-rs/crates/module-bark-battle/src/lib.rs @@ -1,4 +1,8 @@ +mod application; +mod commands; pub mod domain; +mod errors; +mod events; pub mod scoring; pub use domain::*; diff --git a/server-rs/crates/module-big-fish/src/commands.rs b/server-rs/crates/module-big-fish/src/commands.rs index 72d67bdd..0b186ac7 100644 --- a/server-rs/crates/module-big-fish/src/commands.rs +++ b/server-rs/crates/module-big-fish/src/commands.rs @@ -68,7 +68,7 @@ pub struct BigFishWorkRemixInput { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BigFishWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } @@ -188,9 +188,9 @@ pub struct BigFishInputSubmitInput { } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct BigFishRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/module-creative-agent/src/events.rs b/server-rs/crates/module-creative-agent/src/events.rs new file mode 100644 index 00000000..669dec26 --- /dev/null +++ b/server-rs/crates/module-creative-agent/src/events.rs @@ -0,0 +1 @@ +//! 中文注释:创意 Agent 领域事件预留落位,当前流程不导出独立事件类型。 diff --git a/server-rs/crates/module-creative-agent/src/lib.rs b/server-rs/crates/module-creative-agent/src/lib.rs index b68fa524..b700e48b 100644 --- a/server-rs/crates/module-creative-agent/src/lib.rs +++ b/server-rs/crates/module-creative-agent/src/lib.rs @@ -2,6 +2,7 @@ mod application; mod commands; mod domain; mod errors; +mod events; pub use application::*; pub use commands::*; diff --git a/server-rs/crates/module-puzzle/src/application.rs b/server-rs/crates/module-puzzle/src/application.rs index dfa24169..eed25933 100644 --- a/server-rs/crates/module-puzzle/src/application.rs +++ b/server-rs/crates/module-puzzle/src/application.rs @@ -16,7 +16,7 @@ use crate::{domain::*, errors::PuzzleFieldError}; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PuzzleAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } @@ -24,7 +24,7 @@ pub struct PuzzleAgentSessionProcedureResult { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PuzzleWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } @@ -32,15 +32,15 @@ pub struct PuzzleWorksProcedureResult { #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct PuzzleWorkProcedureResult { pub ok: bool, - pub item_json: Option, + pub item: Option, pub error_message: Option, } #[cfg_attr(feature = "spacetime-types", derive(SpacetimeType))] -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] pub struct PuzzleRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/module-visual-novel/src/commands.rs b/server-rs/crates/module-visual-novel/src/commands.rs new file mode 100644 index 00000000..975799fa --- /dev/null +++ b/server-rs/crates/module-visual-novel/src/commands.rs @@ -0,0 +1 @@ +//! 中文注释:视觉小说命令归一化预留落位,当前命令校验仍由 application 承接。 diff --git a/server-rs/crates/module-visual-novel/src/events.rs b/server-rs/crates/module-visual-novel/src/events.rs new file mode 100644 index 00000000..9f691de8 --- /dev/null +++ b/server-rs/crates/module-visual-novel/src/events.rs @@ -0,0 +1 @@ +//! 中文注释:视觉小说领域事件预留落位,当前不导出独立事件类型。 diff --git a/server-rs/crates/module-visual-novel/src/lib.rs b/server-rs/crates/module-visual-novel/src/lib.rs index 290d744e..0b5c82a5 100644 --- a/server-rs/crates/module-visual-novel/src/lib.rs +++ b/server-rs/crates/module-visual-novel/src/lib.rs @@ -1,6 +1,8 @@ mod application; +mod commands; mod domain; mod errors; +mod events; pub use application::*; pub use domain::*; diff --git a/server-rs/crates/shared-contracts/src/puzzle_gallery.rs b/server-rs/crates/shared-contracts/src/puzzle_gallery.rs index daed2603..ecf89149 100644 --- a/server-rs/crates/shared-contracts/src/puzzle_gallery.rs +++ b/server-rs/crates/shared-contracts/src/puzzle_gallery.rs @@ -6,6 +6,21 @@ use crate::puzzle_works::{PuzzleWorkProfileResponse, PuzzleWorkSummaryResponse}; #[serde(rename_all = "camelCase")] pub struct PuzzleGalleryResponse { pub items: Vec, + #[serde(default)] + pub preview_refs: Vec, + #[serde(default)] + pub has_more: bool, + #[serde(default)] + pub next_cursor: Option, + #[serde(default)] + pub total_count: u32, +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct PuzzleGalleryWorkRefResponse { + pub work_id: String, + pub profile_id: String, } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] diff --git a/server-rs/crates/spacetime-client/src/big_fish.rs b/server-rs/crates/spacetime-client/src/big_fish.rs index 38c976b4..63325f3c 100644 --- a/server-rs/crates/spacetime-client/src/big_fish.rs +++ b/server-rs/crates/spacetime-client/src/big_fish.rs @@ -7,7 +7,6 @@ use crate::module_bindings::record_big_fish_play_procedure::record_big_fish_play use crate::module_bindings::remix_big_fish_work_procedure::remix_big_fish_work; use crate::module_bindings::start_big_fish_run_procedure::start_big_fish_run; use crate::module_bindings::submit_big_fish_input_procedure::submit_big_fish_input; -use module_big_fish::PUBLIC_BIG_FISH_GALLERY_OWNER_USER_ID; impl SpacetimeClient { pub async fn create_big_fish_session( @@ -75,10 +74,29 @@ impl SpacetimeClient { pub async fn list_big_fish_gallery( &self, ) -> Result, SpacetimeClientError> { - self.list_big_fish_works_with_input(BigFishWorksListInput { - // 中文注释:公开广场读取只依赖 published_only,但旧部署模块会先校验 owner_user_id 非空。 - owner_user_id: PUBLIC_BIG_FISH_GALLERY_OWNER_USER_ID.to_string(), - published_only: true, + self.read_after_connect("list_big_fish_gallery", move |connection| { + let recent_play_counts = public_work_recent_play_counts(connection, "big-fish"); + let mut items = connection + .db() + .big_fish_gallery_view() + .iter() + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.source_session_id.cmp(&right.source_session_id)) + }); + Ok(items + .into_iter() + .map(|item| { + let recent_play_count_7d = recent_play_counts + .get(&item.source_session_id) + .copied() + .unwrap_or(0); + map_big_fish_gallery_view_row(item, recent_play_count_7d) + }) + .collect()) }) .await } diff --git a/server-rs/crates/spacetime-client/src/custom_world.rs b/server-rs/crates/spacetime-client/src/custom_world.rs index 62b7be19..9d5f5285 100644 --- a/server-rs/crates/spacetime-client/src/custom_world.rs +++ b/server-rs/crates/spacetime-client/src/custom_world.rs @@ -181,6 +181,55 @@ impl SpacetimeClient { pub async fn list_custom_world_gallery_entries( &self, ) -> Result, SpacetimeClientError> { + let records = self.read_custom_world_gallery_entries_from_cache().await?; + if !records.is_empty() + || self + .custom_world_gallery_legacy_sync_attempted + .swap(true, std::sync::atomic::Ordering::SeqCst) + { + return Ok(records); + } + + let _ = self + .sync_custom_world_gallery_entries_via_legacy_procedure() + .await; + self.read_custom_world_gallery_entries_from_cache().await + } + + async fn read_custom_world_gallery_entries_from_cache( + &self, + ) -> Result, SpacetimeClientError> { + self.read_after_connect("list_custom_world_gallery", move |connection| { + let recent_play_counts = public_work_recent_play_counts(connection, "custom-world"); + let mut entries = connection + .db() + .custom_world_gallery_entry() + .iter() + .collect::>(); + entries.sort_by(|left, right| { + right + .published_at + .cmp(&left.published_at) + .then(right.updated_at.cmp(&left.updated_at)) + }); + + Ok(entries + .into_iter() + .map(|entry| { + let recent_play_count_7d = recent_play_counts + .get(&entry.profile_id) + .copied() + .unwrap_or(0); + map_custom_world_gallery_entry_row(entry, recent_play_count_7d) + }) + .collect()) + }) + .await + } + + async fn sync_custom_world_gallery_entries_via_legacy_procedure( + &self, + ) -> Result<(), SpacetimeClientError> { self.call_after_connect( "list_custom_world_gallery_entries", move |connection, sender| { @@ -188,8 +237,8 @@ impl SpacetimeClient { .procedures() .list_custom_world_gallery_entries_then(move |_, result| { let mapped = result - .map_err(SpacetimeClientError::from_sdk_error) - .and_then(map_custom_world_gallery_list_result); + .map(|_| ()) + .map_err(SpacetimeClientError::from_sdk_error); send_once(&sender, mapped); }); }, diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index cd09a684..b3b33e7d 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -44,7 +44,7 @@ pub use mapper::{ PuzzleAgentSessionRecord, PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleAudioAssetRecord, PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord, PuzzleDraftLevelRecord, PuzzleFormDraftRecord, - PuzzleFormDraftSaveRecordInput, PuzzleGeneratedImageCandidateRecord, + PuzzleFormDraftSaveRecordInput, PuzzleGalleryCardRecord, PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, PuzzleLeaderboardEntryRecord, PuzzleLeaderboardSubmitRecordInput, PuzzleMergedGroupRecord, PuzzlePieceStateRecord, PuzzlePublishRecordInput, PuzzleRecommendedNextWorkRecord, PuzzleResultDraftRecord, @@ -96,6 +96,7 @@ pub mod story_runtime; pub mod visual_novel; use std::{ + collections::HashMap, error::Error, fmt, sync::atomic::{AtomicBool, Ordering}, @@ -225,7 +226,7 @@ use module_story::{ use shared_kernel::format_timestamp_micros; use spacetimedb_sdk::{DbContext, Table}; use tokio::{ - sync::{OwnedSemaphorePermit, Semaphore, oneshot}, + sync::{OwnedSemaphorePermit, RwLock, Semaphore, oneshot}, time::timeout, }; @@ -257,6 +258,8 @@ pub struct AuthStoreSnapshotImportRecord { pub struct SpacetimeClient { config: SpacetimeClientConfig, pool: Arc, + creation_entry_config_cache: Arc>>, + custom_world_gallery_legacy_sync_attempted: Arc, } #[derive(Debug)] @@ -269,6 +272,8 @@ pub enum SpacetimeClientError { } const DEFAULT_PROCEDURE_TIMEOUT: Duration = Duration::from_secs(30); +const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000; +const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7; type ProcedureResultSender = Arc>>>>; @@ -286,7 +291,7 @@ struct PooledConnectionSlot { struct PooledConnection { connection: DbConnection, - _gallery_subscription: Vec, + _read_model_subscriptions: Vec, runner: Option>, broken: Arc, } @@ -321,7 +326,12 @@ impl SpacetimeClient { permits: Arc::new(Semaphore::new(pool_size)), }); - Self { config, pool } + Self { + config, + pool, + creation_entry_config_cache: Arc::new(RwLock::new(None)), + custom_world_gallery_legacy_sync_attempted: Arc::new(AtomicBool::new(false)), + } } async fn call_after_connect( @@ -397,12 +407,21 @@ impl SpacetimeClient { async fn read_after_connect( &self, + read_name: &'static str, read: impl FnOnce(&DbConnection) -> Result + Send + 'static, ) -> Result where T: Send + 'static, { - let lease = self.acquire_connection().await?; + let metrics_guard = telemetry::begin_read(read_name); + let lease = match self.acquire_connection().await { + Ok(lease) => lease, + Err(error) => { + let final_result = Err(error); + metrics_guard.finish(&final_result); + return final_result; + } + }; let final_result = if let Some(connection) = lease.connection.as_ref() { read(&connection.connection) } else { @@ -412,9 +431,18 @@ impl SpacetimeClient { }; self.release_connection(lease).await; + metrics_guard.finish(&final_result); final_result } + async fn cache_creation_entry_config(&self, config: CreationEntryConfigRecord) { + *self.creation_entry_config_cache.write().await = Some(config); + } + + async fn read_cached_creation_entry_config(&self) -> Option { + self.creation_entry_config_cache.read().await.clone() + } + async fn acquire_connection(&self) -> Result { let permit = timeout( self.config.procedure_timeout, @@ -503,57 +531,95 @@ impl SpacetimeClient { .map_err(|_| SpacetimeClientError::Timeout)? .map_err(|_| SpacetimeClientError::ConnectDropped)??; - let gallery_subscription = self - .subscribe_puzzle_gallery_views(&connection, broken.clone()) + let read_model_subscriptions = self + .subscribe_cached_read_models(&connection, broken.clone()) .await?; Ok(PooledConnection { connection, - _gallery_subscription: gallery_subscription, + _read_model_subscriptions: read_model_subscriptions, runner: Some(runner), broken, }) } - async fn subscribe_puzzle_gallery_views( + async fn subscribe_cached_read_models( &self, connection: &DbConnection, broken: Arc, ) -> Result, SpacetimeClientError> { let mut subscriptions = Vec::new(); for query in [ - "SELECT * FROM puzzle_gallery_view", + "SELECT * FROM puzzle_gallery_card_view", + "SELECT * FROM custom_world_gallery_entry", + "SELECT * FROM match_3_d_gallery_view", + "SELECT * FROM square_hole_gallery_view", + "SELECT * FROM visual_novel_gallery_view", + "SELECT * FROM big_fish_gallery_view", ] { - let (sender, receiver) = oneshot::channel::>(); - let applied_sender = Arc::new(Mutex::new(Some(sender))); - let on_applied_sender = applied_sender.clone(); - let on_error_sender = applied_sender.clone(); - let broken_flag = broken.clone(); - let subscription = connection - .subscription_builder() - .on_applied(move |_| { - send_connect_once(&on_applied_sender, Ok(())); - }) - .on_error(move |_, error| { - broken_flag.store(true, Ordering::SeqCst); - send_connect_once( - &on_error_sender, - Err(SpacetimeClientError::Procedure(error.to_string())), - ); - }) - .subscribe(query); - - timeout(self.config.procedure_timeout, receiver) - .await - .map_err(|_| SpacetimeClientError::Timeout)? - .map_err(|_| SpacetimeClientError::ConnectDropped)??; - + let subscription = self + .subscribe_cached_read_model_query(connection, broken.clone(), query, true) + .await?; subscriptions.push(subscription); } + for query in [ + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'puzzle'", + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'custom-world'", + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'match3d'", + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'square-hole'", + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'visual-novel'", + "SELECT * FROM public_work_play_daily_stat WHERE source_type = 'big-fish'", + "SELECT * FROM creation_entry_config", + "SELECT * FROM creation_entry_type_config", + ] { + if let Ok(subscription) = self + .subscribe_cached_read_model_query(connection, broken.clone(), query, false) + .await + { + subscriptions.push(subscription); + } + } + Ok(subscriptions) } + async fn subscribe_cached_read_model_query( + &self, + connection: &DbConnection, + broken: Arc, + query: &'static str, + mark_broken_on_error: bool, + ) -> Result { + let (sender, receiver) = oneshot::channel::>(); + let applied_sender = Arc::new(Mutex::new(Some(sender))); + let on_applied_sender = applied_sender.clone(); + let on_error_sender = applied_sender.clone(); + let broken_flag = broken.clone(); + let subscription = connection + .subscription_builder() + .on_applied(move |_| { + send_connect_once(&on_applied_sender, Ok(())); + }) + .on_error(move |_, error| { + if mark_broken_on_error { + broken_flag.store(true, Ordering::SeqCst); + } + send_connect_once( + &on_error_sender, + Err(SpacetimeClientError::Procedure(error.to_string())), + ); + }) + .subscribe(query); + + timeout(self.config.procedure_timeout, receiver) + .await + .map_err(|_| SpacetimeClientError::Timeout)? + .map_err(|_| SpacetimeClientError::ConnectDropped)??; + + Ok(subscription) + } + async fn release_connection(&self, mut lease: PooledConnectionLease) { let mut slot_guard = self.pool.slots[lease.slot_index].lock().await; slot_guard.in_use = false; @@ -581,6 +647,39 @@ impl SpacetimeClient { } } +fn current_unix_micros() -> i64 { + std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|duration| duration.as_micros() as i64) + .unwrap_or(0) +} + +fn current_public_work_day() -> i64 { + current_unix_micros().div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS) +} + +fn public_work_recent_play_counts( + connection: &DbConnection, + source_type: &str, +) -> HashMap { + let current_day = current_public_work_day(); + let first_day = current_day - (PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1); + let mut counts = HashMap::new(); + + for row in connection.db().public_work_play_daily_stat().iter() { + if row.source_type != source_type + || row.played_day < first_day + || row.played_day > current_day + { + continue; + } + let entry: &mut u32 = counts.entry(row.profile_id).or_insert(0); + *entry = (*entry).saturating_add(row.play_count); + } + + counts +} + impl SpacetimeClientError { pub(crate) fn from_sdk_error(error: impl fmt::Display) -> Self { Self::Procedure(error.to_string()) diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index cef4511b..6ee3b1ca 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -782,33 +782,94 @@ pub type BarkBattleRunRecord = serde_json::Value; pub(crate) fn map_bark_battle_draft_config_procedure_result( result: BarkBattleProcedureResult, ) -> Result { - parse_bark_battle_row_json(result, "Bark Battle draft config") + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .draft_config + .ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle draft config")) + .map(bark_battle_draft_config_to_value) } pub(crate) fn map_bark_battle_runtime_config_procedure_result( result: BarkBattleProcedureResult, ) -> Result { - parse_bark_battle_row_json(result, "Bark Battle runtime config") + if !result.ok { + return Err(SpacetimeClientError::procedure_failed(result.error_message)); + } + result + .runtime_config + .ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle runtime config")) + .map(bark_battle_runtime_config_to_value) } pub(crate) fn map_bark_battle_run_procedure_result( result: BarkBattleProcedureResult, ) -> Result { - parse_bark_battle_row_json(result, "Bark Battle run") -} - -fn parse_bark_battle_row_json( - result: BarkBattleProcedureResult, - label: &'static str, -) -> Result { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let row_json = result - .row_json - .ok_or_else(|| SpacetimeClientError::missing_snapshot(label))?; - serde_json::from_str(&row_json) - .map_err(|error| SpacetimeClientError::Runtime(format!("{label} JSON 解析失败: {error}"))) + result + .run + .ok_or_else(|| SpacetimeClientError::missing_snapshot("Bark Battle run")) + .map(bark_battle_run_to_value) +} + +fn bark_battle_draft_config_to_value(snapshot: BarkBattleDraftConfigSnapshot) -> serde_json::Value { + serde_json::json!({ + "draftId": snapshot.draft_id, + "ownerUserId": snapshot.owner_user_id, + "workId": snapshot.work_id, + "configVersion": snapshot.config_version, + "rulesetVersion": snapshot.ruleset_version, + "difficultyPreset": snapshot.difficulty_preset, + "leaderboardEnabled": snapshot.leaderboard_enabled, + "configJson": snapshot.config_json, + "editorStateJson": snapshot.editor_state_json, + "createdAtMicros": snapshot.created_at_micros, + "updatedAtMicros": snapshot.updated_at_micros, + }) +} + +fn bark_battle_runtime_config_to_value( + snapshot: BarkBattleRuntimeConfigSnapshot, +) -> serde_json::Value { + serde_json::json!({ + "workId": snapshot.work_id, + "ownerUserId": snapshot.owner_user_id, + "sourceDraftId": snapshot.source_draft_id, + "configVersion": snapshot.config_version, + "rulesetVersion": snapshot.ruleset_version, + "difficultyPreset": snapshot.difficulty_preset, + "leaderboardEnabled": snapshot.leaderboard_enabled, + "configJson": snapshot.config_json, + "publishedSnapshotJson": snapshot.published_snapshot_json, + "publishedAtMicros": snapshot.published_at_micros, + "updatedAtMicros": snapshot.updated_at_micros, + }) +} + +fn bark_battle_run_to_value(snapshot: BarkBattleRunSnapshot) -> serde_json::Value { + serde_json::json!({ + "runId": snapshot.run_id, + "ownerUserId": snapshot.owner_user_id, + "workId": snapshot.work_id, + "configVersion": snapshot.config_version, + "rulesetVersion": snapshot.ruleset_version, + "difficultyPreset": snapshot.difficulty_preset, + "leaderboardEnabled": snapshot.leaderboard_enabled, + "status": snapshot.status, + "clientStartedAtMicros": snapshot.client_started_at_micros, + "serverStartedAtMicros": snapshot.server_started_at_micros, + "clientFinishedAtMicros": snapshot.client_finished_at_micros, + "serverFinishedAtMicros": snapshot.server_finished_at_micros, + "metricsJson": snapshot.metrics_json, + "serverResult": snapshot.server_result, + "validationStatus": snapshot.validation_status, + "antiCheatFlagsJson": snapshot.anti_cheat_flags_json, + "leaderboardScore": snapshot.leaderboard_score, + "scoreId": snapshot.score_id, + }) } pub type CreationEntryConfigRecord = @@ -830,6 +891,48 @@ pub(crate) fn map_creation_entry_config_procedure_result( )) } +pub(crate) fn build_creation_entry_config_record_from_rows( + header: CreationEntryConfig, + mut creation_types: Vec, +) -> CreationEntryConfigRecord { + creation_types.sort_by(|left, right| { + left.sort_order + .cmp(&right.sort_order) + .then_with(|| left.id.cmp(&right.id)) + }); + + module_runtime::build_creation_entry_config_response( + module_runtime::CreationEntryConfigSnapshot { + config_id: header.config_id, + start_card: module_runtime::CreationEntryStartCardSnapshot { + title: header.start_title, + description: header.start_description, + idle_badge: header.start_idle_badge, + busy_badge: header.start_busy_badge, + }, + type_modal: module_runtime::CreationEntryTypeModalSnapshot { + title: header.modal_title, + description: header.modal_description, + }, + creation_types: creation_types + .into_iter() + .map(|item| module_runtime::CreationEntryTypeSnapshot { + id: item.id, + title: item.title, + subtitle: item.subtitle, + badge: item.badge, + image_src: item.image_src, + visible: item.visible, + open: item.open, + sort_order: item.sort_order, + updated_at_micros: item.updated_at.to_micros_since_unix_epoch(), + }) + .collect(), + updated_at_micros: header.updated_at.to_micros_since_unix_epoch(), + }, + ) +} + fn map_creation_entry_config_snapshot( snapshot: CreationEntryConfigSnapshot, ) -> module_runtime::CreationEntryConfigSnapshot { @@ -1584,13 +1687,9 @@ pub(crate) fn map_puzzle_agent_session_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let session_json = result - .session_json + let session = result + .session .ok_or_else(|| SpacetimeClientError::missing_snapshot("puzzle agent session 快照"))?; - let session: DomainPuzzleAgentSessionSnapshot = - serde_json::from_str(&session_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("puzzle agent session_json 非法: {error}")) - })?; Ok(map_puzzle_agent_session_snapshot(session)) } @@ -1601,12 +1700,9 @@ pub(crate) fn map_puzzle_work_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let item_json = result - .item_json + let item = result + .item .ok_or_else(|| SpacetimeClientError::missing_snapshot("puzzle work 快照"))?; - let item: DomainPuzzleWorkProfile = serde_json::from_str(&item_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("puzzle work item_json 非法: {error}")) - })?; Ok(map_puzzle_work_profile(item)) } @@ -1617,14 +1713,11 @@ pub(crate) fn map_puzzle_works_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let items_json = result - .items_json - .ok_or_else(|| SpacetimeClientError::missing_snapshot("puzzle works 快照"))?; - let items: Vec = - serde_json::from_str(&items_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("puzzle works items_json 非法: {error}")) - })?; - Ok(items.into_iter().map(map_puzzle_work_profile).collect()) + Ok(result + .items + .into_iter() + .map(map_puzzle_work_profile) + .collect()) } pub(crate) fn map_puzzle_run_procedure_result( @@ -1634,12 +1727,9 @@ pub(crate) fn map_puzzle_run_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let run_json = result - .run_json + let run = result + .run .ok_or_else(|| SpacetimeClientError::missing_snapshot("puzzle run 快照"))?; - let run: DomainPuzzleRunSnapshot = serde_json::from_str(&run_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("puzzle run run_json 非法: {error}")) - })?; Ok(map_puzzle_run_snapshot(run)) } @@ -1659,25 +1749,17 @@ pub(crate) fn map_big_fish_session_procedure_result( pub(crate) fn map_big_fish_works_procedure_result( result: BigFishWorksProcedureResult, - fallback_owner_user_id: Option<&str>, + _fallback_owner_user_id: Option<&str>, ) -> Result, SpacetimeClientError> { if !result.ok { return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let items_json = result - .items_json - .ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish works 快照"))?; - serde_json::from_str::>(&items_json) - .map(|items| { - items - .into_iter() - .map(|item| item.into_record(fallback_owner_user_id)) - .collect() - }) - .map_err(|error| { - SpacetimeClientError::Runtime(format!("big fish works items_json 非法: {error}")) - }) + Ok(result + .items + .into_iter() + .map(map_big_fish_work_summary_snapshot) + .collect()) } pub(crate) fn map_big_fish_run_procedure_result( @@ -1687,13 +1769,9 @@ pub(crate) fn map_big_fish_run_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let run_json = result - .run_json + let run = result + .run .ok_or_else(|| SpacetimeClientError::missing_snapshot("big fish run 快照"))?; - let run: module_big_fish::BigFishRuntimeSnapshot = - serde_json::from_str(&run_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("big fish run run_json 非法: {error}")) - })?; Ok(map_big_fish_runtime_snapshot(run)) } @@ -1708,15 +1786,11 @@ pub(crate) fn map_match3d_agent_session_procedure_result( )); } - let session_json = result.session_json.ok_or_else(|| { + let session = result.session.ok_or_else(|| { SpacetimeClientError::Procedure( "SpacetimeDB procedure 未返回 match3d agent session 快照".to_string(), ) })?; - let session = - serde_json::from_str::(&session_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("match3d session_json 非法: {error}")) - })?; Ok(map_match3d_agent_session_snapshot(session)) } @@ -1732,14 +1806,11 @@ pub(crate) fn map_match3d_work_procedure_result( )); } - let work_json = result.work_json.ok_or_else(|| { + let work = result.work.ok_or_else(|| { SpacetimeClientError::Procedure( "SpacetimeDB procedure 未返回 match3d work 快照".to_string(), ) })?; - let work = serde_json::from_str::(&work_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("match3d work_json 非法: {error}")) - })?; Ok(map_match3d_work_snapshot(work)) } @@ -1755,17 +1826,11 @@ pub(crate) fn map_match3d_works_procedure_result( )); } - let items_json = result.items_json.ok_or_else(|| { - SpacetimeClientError::Procedure( - "SpacetimeDB procedure 未返回 match3d works 快照".to_string(), - ) - })?; - let items = - serde_json::from_str::>(&items_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("match3d works items_json 非法: {error}")) - })?; - - Ok(items.into_iter().map(map_match3d_work_snapshot).collect()) + Ok(result + .items + .into_iter() + .map(map_match3d_work_snapshot) + .collect()) } pub(crate) fn map_match3d_run_procedure_result( @@ -1779,10 +1844,10 @@ pub(crate) fn map_match3d_run_procedure_result( )); } - let run_json = result.run_json.ok_or_else(|| { + let run = result.run.ok_or_else(|| { SpacetimeClientError::Procedure("SpacetimeDB procedure 未返回 match3d run 快照".to_string()) })?; - map_match3d_run_json(run_json) + Ok(map_match3d_run_snapshot(run)) } pub(crate) fn map_match3d_click_item_procedure_result( @@ -1796,12 +1861,12 @@ pub(crate) fn map_match3d_click_item_procedure_result( )); } - let run_json = result.run_json.ok_or_else(|| { + let run = result.run.ok_or_else(|| { SpacetimeClientError::Procedure( "SpacetimeDB procedure 未返回 match3d click run 快照".to_string(), ) })?; - let run = map_match3d_run_json(run_json)?; + let run = map_match3d_run_snapshot(run); let accepted = result.status == "Accepted"; let accepted_item_instance_id = result.accepted_item_instance_id.clone(); let entered_slot_index = accepted_item_instance_id.as_deref().and_then(|item_id| { @@ -1830,12 +1895,9 @@ pub(crate) fn map_square_hole_agent_session_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let session_json = result - .session_json + let session = result + .session .ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole agent session 快照"))?; - let session = serde_json::from_str::(&session_json).map_err( - |error| SpacetimeClientError::Runtime(format!("square hole session_json 非法: {error}")), - )?; Ok(map_square_hole_agent_session_snapshot(session)) } @@ -1847,12 +1909,9 @@ pub(crate) fn map_square_hole_work_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let work_json = result - .work_json + let work = result + .work .ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole work 快照"))?; - let work = serde_json::from_str::(&work_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("square hole work_json 非法: {error}")) - })?; Ok(map_square_hole_work_snapshot(work)) } @@ -1864,15 +1923,8 @@ pub(crate) fn map_square_hole_works_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let items_json = result - .items_json - .ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole works 快照"))?; - let items = - serde_json::from_str::>(&items_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("square hole works items_json 非法: {error}")) - })?; - - Ok(items + Ok(result + .items .into_iter() .map(map_square_hole_work_snapshot) .collect()) @@ -1885,10 +1937,10 @@ pub(crate) fn map_square_hole_run_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let run_json = result - .run_json + let run = result + .run .ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole run 快照"))?; - map_square_hole_run_json(run_json) + Ok(map_square_hole_run_snapshot(run)) } pub(crate) fn map_square_hole_drop_shape_procedure_result( @@ -1898,17 +1950,13 @@ pub(crate) fn map_square_hole_drop_shape_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let run_json = result - .run_json + let run = result + .run .ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop run 快照"))?; - let feedback_json = result - .feedback_json + let feedback = result + .feedback .ok_or_else(|| SpacetimeClientError::missing_snapshot("square hole drop feedback 快照"))?; - let run = map_square_hole_run_json(run_json)?; - let feedback = serde_json::from_str::(&feedback_json) - .map_err(|error| { - SpacetimeClientError::Runtime(format!("square hole feedback_json 非法: {error}")) - })?; + let run = map_square_hole_run_snapshot(run); Ok(SquareHoleDropConfirmationRecord { status: result.status, @@ -1927,13 +1975,9 @@ pub(crate) fn map_visual_novel_agent_session_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let session_json = result - .session_json + let session = result + .session .ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel agent session 快照"))?; - let session = serde_json::from_str::(&session_json) - .map_err(|error| { - SpacetimeClientError::Runtime(format!("visual novel session_json 非法: {error}")) - })?; Ok(map_visual_novel_agent_session_snapshot(session)) } @@ -1945,12 +1989,9 @@ pub(crate) fn map_visual_novel_work_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let work_json = result - .work_json + let work = result + .work .ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel work 快照"))?; - let work = serde_json::from_str::(&work_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("visual novel work_json 非法: {error}")) - })?; Ok(map_visual_novel_work_snapshot(work)) } @@ -1962,15 +2003,8 @@ pub(crate) fn map_visual_novel_works_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let items_json = result - .items_json - .ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel works 快照"))?; - let items = - serde_json::from_str::>(&items_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("visual novel works items_json 非法: {error}")) - })?; - - Ok(items + Ok(result + .items .into_iter() .map(map_visual_novel_work_snapshot) .collect()) @@ -1983,12 +2017,9 @@ pub(crate) fn map_visual_novel_run_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let run_json = result - .run_json + let run = result + .run .ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel run 快照"))?; - let run = serde_json::from_str::(&run_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("visual novel run_json 非法: {error}")) - })?; Ok(map_visual_novel_run_snapshot(run)) } @@ -2000,15 +2031,8 @@ pub(crate) fn map_visual_novel_history_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let items_json = result - .items_json - .ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel history 快照"))?; - let items = serde_json::from_str::>(&items_json) - .map_err(|error| { - SpacetimeClientError::Runtime(format!("visual novel history items_json 非法: {error}")) - })?; - - Ok(items + Ok(result + .items .into_iter() .map(map_visual_novel_history_entry) .collect()) @@ -2021,12 +2045,9 @@ pub(crate) fn map_visual_novel_runtime_event_procedure_result( return Err(SpacetimeClientError::procedure_failed(result.error_message)); } - let event_json = result - .event_json + let event = result + .event .ok_or_else(|| SpacetimeClientError::missing_snapshot("visual novel runtime event 快照"))?; - let event = serde_json::from_str::(&event_json).map_err( - |error| SpacetimeClientError::Runtime(format!("visual novel event_json 非法: {error}")), - )?; Ok(map_visual_novel_runtime_event(event)) } @@ -2676,6 +2697,38 @@ pub(crate) fn map_custom_world_gallery_entry_snapshot( }) } +pub(crate) fn map_custom_world_gallery_entry_row( + row: CustomWorldGalleryEntry, + recent_play_count_7d: u32, +) -> CustomWorldGalleryEntryRecord { + CustomWorldGalleryEntryRecord { + owner_user_id: row.owner_user_id, + profile_id: row.profile_id, + public_work_code: row.public_work_code, + author_public_user_code: row.author_public_user_code, + visibility: "published".to_string(), + published_at: Some(format_timestamp_micros( + row.published_at.to_micros_since_unix_epoch(), + )), + updated_at: format_timestamp_micros(row.updated_at.to_micros_since_unix_epoch()), + author_display_name: row.author_display_name, + world_name: row.world_name, + subtitle: row.subtitle, + summary_text: row.summary_text, + cover_image_src: row.cover_image_src, + theme_mode: format_custom_world_theme_mode(map_custom_world_theme_mode_back( + row.theme_mode, + )) + .to_string(), + playable_npc_count: row.playable_npc_count, + landmark_count: row.landmark_count, + play_count: row.play_count, + remix_count: row.remix_count, + like_count: row.like_count, + recent_play_count_7d, + } +} + pub(crate) fn map_custom_world_published_profile_compile_snapshot( snapshot: CustomWorldPublishedProfileCompileSnapshot, ) -> Result { @@ -2974,14 +3027,14 @@ pub(crate) fn map_big_fish_session_snapshot( } pub(crate) fn map_puzzle_agent_session_snapshot( - snapshot: DomainPuzzleAgentSessionSnapshot, + snapshot: PuzzleAgentSessionSnapshot, ) -> PuzzleAgentSessionRecord { PuzzleAgentSessionRecord { session_id: snapshot.session_id, seed_text: snapshot.seed_text, current_turn: snapshot.current_turn, progress_percent: snapshot.progress_percent, - stage: snapshot.stage.as_str().to_string(), + stage: format_puzzle_agent_stage(snapshot.stage).to_string(), anchor_pack: map_puzzle_anchor_pack(snapshot.anchor_pack), draft: snapshot.draft.map(map_puzzle_result_draft), messages: snapshot @@ -3001,7 +3054,7 @@ pub(crate) fn map_puzzle_agent_session_snapshot( } } -pub(crate) fn map_puzzle_anchor_pack(snapshot: DomainPuzzleAnchorPack) -> PuzzleAnchorPackRecord { +pub(crate) fn map_puzzle_anchor_pack(snapshot: PuzzleAnchorPack) -> PuzzleAnchorPackRecord { PuzzleAnchorPackRecord { theme_promise: map_puzzle_anchor_item(snapshot.theme_promise), visual_subject: map_puzzle_anchor_item(snapshot.visual_subject), @@ -3011,18 +3064,16 @@ pub(crate) fn map_puzzle_anchor_pack(snapshot: DomainPuzzleAnchorPack) -> Puzzle } } -pub(crate) fn map_puzzle_anchor_item(snapshot: DomainPuzzleAnchorItem) -> PuzzleAnchorItemRecord { +pub(crate) fn map_puzzle_anchor_item(snapshot: PuzzleAnchorItem) -> PuzzleAnchorItemRecord { PuzzleAnchorItemRecord { key: snapshot.key, label: snapshot.label, value: snapshot.value, - status: snapshot.status.as_str().to_string(), + status: format_puzzle_anchor_status(snapshot.status).to_string(), } } -pub(crate) fn map_puzzle_result_draft( - snapshot: DomainPuzzleResultDraft, -) -> PuzzleResultDraftRecord { +pub(crate) fn map_puzzle_result_draft(snapshot: PuzzleResultDraft) -> PuzzleResultDraftRecord { PuzzleResultDraftRecord { work_title: snapshot.work_title, work_description: snapshot.work_description, @@ -3050,9 +3101,7 @@ pub(crate) fn map_puzzle_result_draft( } } -pub(crate) fn map_puzzle_form_draft( - snapshot: module_puzzle::PuzzleFormDraft, -) -> PuzzleFormDraftRecord { +pub(crate) fn map_puzzle_form_draft(snapshot: PuzzleFormDraft) -> PuzzleFormDraftRecord { PuzzleFormDraftRecord { work_title: snapshot.work_title, work_description: snapshot.work_description, @@ -3060,7 +3109,7 @@ pub(crate) fn map_puzzle_form_draft( } } -pub(crate) fn map_puzzle_draft_level(snapshot: DomainPuzzleDraftLevel) -> PuzzleDraftLevelRecord { +pub(crate) fn map_puzzle_draft_level(snapshot: PuzzleDraftLevel) -> PuzzleDraftLevelRecord { PuzzleDraftLevelRecord { level_id: snapshot.level_id, level_name: snapshot.level_name, @@ -3082,9 +3131,7 @@ pub(crate) fn map_puzzle_draft_level(snapshot: DomainPuzzleDraftLevel) -> Puzzle } } -pub(crate) fn map_puzzle_audio_asset( - asset: module_puzzle::PuzzleAudioAsset, -) -> PuzzleAudioAssetRecord { +pub(crate) fn map_puzzle_audio_asset(asset: PuzzleAudioAsset) -> PuzzleAudioAssetRecord { PuzzleAudioAssetRecord { task_id: asset.task_id, provider: asset.provider, @@ -3098,7 +3145,7 @@ pub(crate) fn map_puzzle_audio_asset( } pub(crate) fn map_puzzle_creator_intent( - snapshot: DomainPuzzleCreatorIntent, + snapshot: PuzzleCreatorIntent, ) -> PuzzleCreatorIntentRecord { PuzzleCreatorIntentRecord { source_mode: snapshot.source_mode, @@ -3113,7 +3160,7 @@ pub(crate) fn map_puzzle_creator_intent( } pub(crate) fn map_puzzle_generated_image_candidate( - snapshot: DomainPuzzleGeneratedImageCandidate, + snapshot: PuzzleGeneratedImageCandidate, ) -> PuzzleGeneratedImageCandidateRecord { PuzzleGeneratedImageCandidateRecord { candidate_id: snapshot.candidate_id, @@ -3127,19 +3174,19 @@ pub(crate) fn map_puzzle_generated_image_candidate( } pub(crate) fn map_puzzle_agent_message_snapshot( - snapshot: DomainPuzzleAgentMessageSnapshot, + snapshot: PuzzleAgentMessageSnapshot, ) -> PuzzleAgentMessageRecord { PuzzleAgentMessageRecord { message_id: snapshot.message_id, - role: snapshot.role.as_str().to_string(), - kind: snapshot.kind.as_str().to_string(), + role: format_puzzle_agent_message_role(snapshot.role).to_string(), + kind: format_puzzle_agent_message_kind(snapshot.kind).to_string(), text: snapshot.text, created_at: format_timestamp_micros(snapshot.created_at_micros), } } fn map_match3d_agent_session_snapshot( - snapshot: Match3DAgentSessionJsonRecord, + snapshot: Match3DAgentSessionSnapshot, ) -> Match3DAgentSessionRecord { let config = map_match3d_creator_config(snapshot.config); Match3DAgentSessionRecord { @@ -3164,7 +3211,7 @@ fn map_match3d_agent_session_snapshot( } fn map_match3d_creator_config( - snapshot: Match3DCreatorConfigJsonRecord, + snapshot: Match3DCreatorConfigSnapshot, ) -> Match3DCreatorConfigRecord { Match3DCreatorConfigRecord { theme_text: snapshot.theme_text, @@ -3179,7 +3226,7 @@ fn map_match3d_creator_config( } fn map_match3d_result_draft( - snapshot: Match3DDraftJsonRecord, + snapshot: Match3DDraftSnapshot, reference_image_src: Option, ) -> Match3DResultDraftRecord { Match3DResultDraftRecord { @@ -3192,15 +3239,15 @@ fn map_match3d_result_draft( reference_image_src, clear_count: snapshot.clear_count, difficulty: snapshot.difficulty, + generated_item_assets_json: snapshot.generated_item_assets_json, total_item_count: snapshot.clear_count.saturating_mul(3), publish_ready: false, blockers: Vec::new(), - generated_item_assets_json: snapshot.generated_item_assets_json, } } fn map_match3d_agent_message_snapshot( - snapshot: Match3DAgentMessageJsonRecord, + snapshot: Match3DAgentMessageSnapshot, ) -> Match3DAgentMessageRecord { Match3DAgentMessageRecord { message_id: snapshot.message_id, @@ -3211,7 +3258,7 @@ fn map_match3d_agent_message_snapshot( } } -fn map_match3d_work_snapshot(snapshot: Match3DWorkJsonRecord) -> Match3DWorkProfileRecord { +fn map_match3d_work_snapshot(snapshot: Match3DWorkSnapshot) -> Match3DWorkProfileRecord { let config = map_match3d_creator_config(snapshot.config); Match3DWorkProfileRecord { work_id: snapshot.profile_id.clone(), @@ -3238,14 +3285,33 @@ fn map_match3d_work_snapshot(snapshot: Match3DWorkJsonRecord) -> Match3DWorkProf } } -fn map_match3d_run_json(run_json: String) -> Result { - let run = serde_json::from_str::(&run_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("match3d run_json 非法: {error}")) - })?; - Ok(map_match3d_run_snapshot(run)) +pub(crate) fn map_match3d_gallery_view_row(row: Match3DGalleryViewRow) -> Match3DWorkProfileRecord { + Match3DWorkProfileRecord { + work_id: row.profile_id.clone(), + profile_id: row.profile_id, + owner_user_id: row.owner_user_id, + source_session_id: empty_string_to_none(row.source_session_id), + author_display_name: row.author_display_name, + game_name: row.game_name, + theme_text: row.theme_text, + summary: row.summary_text, + tags: row.tags, + cover_image_src: empty_string_to_none(row.cover_image_src), + cover_asset_id: empty_string_to_none(row.cover_asset_id), + reference_image_src: row.reference_image_src, + clear_count: row.clear_count, + difficulty: row.difficulty, + publication_status: normalize_match3d_publication_status(&row.publication_status) + .to_string(), + play_count: row.play_count, + updated_at: format_timestamp_micros(row.updated_at_micros), + published_at: row.published_at_micros.map(format_timestamp_micros), + publish_ready: row.publish_ready, + generated_item_assets_json: row.generated_item_assets_json, + } } -fn map_match3d_run_snapshot(snapshot: Match3DRunJsonRecord) -> Match3DRunRecord { +fn map_match3d_run_snapshot(snapshot: Match3DRunSnapshot) -> Match3DRunRecord { let tray_slots = snapshot .tray_slots .into_iter() @@ -3286,7 +3352,7 @@ fn map_match3d_run_snapshot(snapshot: Match3DRunJsonRecord) -> Match3DRunRecord } fn map_match3d_item_snapshot( - snapshot: Match3DItemJsonRecord, + snapshot: Match3DItemSnapshot, tray_slot_index: Option, ) -> Match3DItemSnapshotRecord { Match3DItemSnapshotRecord { @@ -3303,7 +3369,7 @@ fn map_match3d_item_snapshot( } } -fn map_match3d_tray_slot_snapshot(snapshot: Match3DTraySlotJsonRecord) -> Match3DTraySlotRecord { +fn map_match3d_tray_slot_snapshot(snapshot: Match3DTraySlotSnapshot) -> Match3DTraySlotRecord { Match3DTraySlotRecord { slot_index: snapshot.slot_index, item_instance_id: snapshot.item_instance_id, @@ -3337,7 +3403,7 @@ fn build_match3d_anchor_item(key: &str, label: &str, value: &str) -> Match3DAnch } fn map_square_hole_agent_session_snapshot( - snapshot: SquareHoleAgentSessionJsonRecord, + snapshot: SquareHoleAgentSessionSnapshot, ) -> SquareHoleAgentSessionRecord { let config = map_square_hole_creator_config(snapshot.config); SquareHoleAgentSessionRecord { @@ -3360,7 +3426,7 @@ fn map_square_hole_agent_session_snapshot( } fn map_square_hole_creator_config( - snapshot: SquareHoleCreatorConfigJsonRecord, + snapshot: SquareHoleCreatorConfigSnapshot, ) -> SquareHoleCreatorConfigRecord { SquareHoleCreatorConfigRecord { theme_text: snapshot.theme_text, @@ -3383,9 +3449,7 @@ fn map_square_hole_creator_config( } } -fn map_square_hole_result_draft( - snapshot: SquareHoleDraftJsonRecord, -) -> SquareHoleResultDraftRecord { +fn map_square_hole_result_draft(snapshot: SquareHoleDraftSnapshot) -> SquareHoleResultDraftRecord { SquareHoleResultDraftRecord { profile_id: snapshot.profile_id, game_name: snapshot.game_name, @@ -3414,7 +3478,7 @@ fn map_square_hole_result_draft( } fn map_square_hole_agent_message_snapshot( - snapshot: SquareHoleAgentMessageJsonRecord, + snapshot: SquareHoleAgentMessageSnapshot, ) -> SquareHoleAgentMessageRecord { SquareHoleAgentMessageRecord { id: snapshot.message_id, @@ -3425,9 +3489,7 @@ fn map_square_hole_agent_message_snapshot( } } -fn map_square_hole_work_snapshot( - snapshot: SquareHoleWorkJsonRecord, -) -> SquareHoleWorkProfileRecord { +fn map_square_hole_work_snapshot(snapshot: SquareHoleWorkSnapshot) -> SquareHoleWorkProfileRecord { SquareHoleWorkProfileRecord { work_id: snapshot.work_id, profile_id: snapshot.profile_id, @@ -3463,14 +3525,45 @@ fn map_square_hole_work_snapshot( } } -fn map_square_hole_run_json(run_json: String) -> Result { - let run = serde_json::from_str::(&run_json).map_err(|error| { - SpacetimeClientError::Runtime(format!("square hole run_json 非法: {error}")) - })?; - Ok(map_square_hole_run_snapshot(run)) +pub(crate) fn map_square_hole_gallery_view_row( + row: SquareHoleGalleryViewRow, +) -> SquareHoleWorkProfileRecord { + SquareHoleWorkProfileRecord { + work_id: row.work_id, + profile_id: row.profile_id, + owner_user_id: row.owner_user_id, + source_session_id: empty_string_to_none(row.source_session_id), + author_display_name: row.author_display_name, + game_name: row.game_name, + theme_text: row.theme_text, + twist_rule: row.twist_rule, + summary: row.summary_text, + tags: row.tags, + cover_image_src: empty_string_to_none(row.cover_image_src), + background_prompt: row.background_prompt, + background_image_src: empty_string_to_none(row.background_image_src), + shape_options: row + .shape_options + .into_iter() + .map(map_square_hole_shape_option) + .collect(), + hole_options: row + .hole_options + .into_iter() + .map(map_square_hole_hole_option) + .collect(), + shape_count: row.shape_count, + difficulty: row.difficulty, + publication_status: normalize_square_hole_publication_status(&row.publication_status) + .to_string(), + play_count: row.play_count, + updated_at: format_timestamp_micros(row.updated_at_micros), + published_at: row.published_at_micros.map(format_timestamp_micros), + publish_ready: row.publish_ready, + } } -fn map_square_hole_run_snapshot(snapshot: SquareHoleRunJsonRecord) -> SquareHoleRunRecord { +fn map_square_hole_run_snapshot(snapshot: SquareHoleRunSnapshot) -> SquareHoleRunRecord { SquareHoleRunRecord { run_id: snapshot.run_id, profile_id: snapshot.profile_id, @@ -3502,7 +3595,7 @@ fn map_square_hole_run_snapshot(snapshot: SquareHoleRunJsonRecord) -> SquareHole } fn map_square_hole_shape_snapshot( - snapshot: SquareHoleShapeJsonRecord, + snapshot: SquareHoleShapeSnapshot, ) -> SquareHoleShapeSnapshotRecord { SquareHoleShapeSnapshotRecord { shape_id: snapshot.shape_id, @@ -3514,9 +3607,7 @@ fn map_square_hole_shape_snapshot( } } -fn map_square_hole_hole_snapshot( - snapshot: SquareHoleHoleJsonRecord, -) -> SquareHoleHoleSnapshotRecord { +fn map_square_hole_hole_snapshot(snapshot: SquareHoleHoleSnapshot) -> SquareHoleHoleSnapshotRecord { SquareHoleHoleSnapshotRecord { hole_id: snapshot.hole_id, hole_kind: snapshot.hole_kind, @@ -3528,7 +3619,7 @@ fn map_square_hole_hole_snapshot( } fn map_square_hole_shape_option( - snapshot: SquareHoleShapeOptionJsonRecord, + snapshot: SquareHoleShapeOptionSnapshot, ) -> SquareHoleShapeOptionRecord { SquareHoleShapeOptionRecord { option_id: snapshot.option_id, @@ -3541,7 +3632,7 @@ fn map_square_hole_shape_option( } fn map_square_hole_hole_option( - snapshot: SquareHoleHoleOptionJsonRecord, + snapshot: SquareHoleHoleOptionSnapshot, ) -> SquareHoleHoleOptionRecord { SquareHoleHoleOptionRecord { hole_id: snapshot.hole_id, @@ -3553,7 +3644,7 @@ fn map_square_hole_hole_option( } fn map_square_hole_feedback_snapshot( - snapshot: SquareHoleDropFeedbackJsonRecord, + snapshot: SquareHoleDropFeedbackSnapshot, ) -> SquareHoleDropFeedbackRecord { SquareHoleDropFeedbackRecord { accepted: snapshot.accepted, @@ -3600,7 +3691,7 @@ fn build_square_hole_anchor_item( } fn map_visual_novel_agent_session_snapshot( - snapshot: VisualNovelAgentSessionJsonRecord, + snapshot: VisualNovelAgentSessionSnapshot, ) -> VisualNovelAgentSessionRecord { VisualNovelAgentSessionRecord { session_id: snapshot.session_id, @@ -3616,8 +3707,8 @@ fn map_visual_novel_agent_session_snapshot( .into_iter() .map(map_visual_novel_agent_message) .collect(), - draft: snapshot.draft, - pending_action: snapshot.pending_action, + draft: snapshot.draft.map(visual_novel_json_to_value), + pending_action: snapshot.pending_action.map(visual_novel_json_to_value), last_assistant_reply: snapshot.last_assistant_reply, published_profile_id: snapshot.published_profile_id, created_at: format_timestamp_micros(snapshot.created_at_micros), @@ -3626,7 +3717,7 @@ fn map_visual_novel_agent_session_snapshot( } fn map_visual_novel_agent_message( - snapshot: VisualNovelAgentMessageJsonRecord, + snapshot: VisualNovelAgentMessageSnapshot, ) -> VisualNovelAgentMessageRecord { VisualNovelAgentMessageRecord { message_id: snapshot.message_id, @@ -3639,7 +3730,7 @@ fn map_visual_novel_agent_message( } fn map_visual_novel_work_snapshot( - snapshot: VisualNovelWorkJsonRecord, + snapshot: VisualNovelWorkSnapshot, ) -> VisualNovelWorkProfileRecord { VisualNovelWorkProfileRecord { work_id: snapshot.work_id, @@ -3652,7 +3743,7 @@ fn map_visual_novel_work_snapshot( tags: snapshot.tags, cover_image_src: snapshot.cover_image_src, source_asset_ids: snapshot.source_asset_ids, - draft: snapshot.draft, + draft: visual_novel_json_to_value(snapshot.draft), publication_status: snapshot.publication_status, publish_ready: snapshot.publish_ready, play_count: snapshot.play_count, @@ -3662,7 +3753,32 @@ fn map_visual_novel_work_snapshot( } } -fn map_visual_novel_run_snapshot(snapshot: VisualNovelRunJsonRecord) -> VisualNovelRunRecord { +pub(crate) fn map_visual_novel_gallery_view_row( + row: VisualNovelGalleryViewRow, +) -> VisualNovelWorkProfileRecord { + VisualNovelWorkProfileRecord { + work_id: row.work_id, + profile_id: row.profile_id, + owner_user_id: row.owner_user_id, + source_session_id: row.source_session_id, + author_display_name: row.author_display_name, + work_title: row.work_title, + work_description: row.work_description, + tags: row.tags, + cover_image_src: row.cover_image_src, + source_asset_ids: row.source_asset_ids, + // 中文注释:公开列表 view 不暴露完整 draft,详情页仍通过 detail procedure 读取。 + draft: serde_json::Value::Null, + publication_status: row.publication_status, + publish_ready: row.publish_ready, + play_count: row.play_count, + created_at: format_timestamp_micros(row.created_at_micros), + updated_at: format_timestamp_micros(row.updated_at_micros), + published_at: row.published_at_micros.map(format_timestamp_micros), + } +} + +fn map_visual_novel_run_snapshot(snapshot: VisualNovelRunSnapshot) -> VisualNovelRunRecord { VisualNovelRunRecord { run_id: snapshot.run_id, owner_user_id: snapshot.owner_user_id, @@ -3672,14 +3788,14 @@ fn map_visual_novel_run_snapshot(snapshot: VisualNovelRunJsonRecord) -> VisualNo current_scene_id: snapshot.current_scene_id, current_phase_id: snapshot.current_phase_id, visible_character_ids: snapshot.visible_character_ids, - flags: snapshot.flags, - metrics: snapshot.metrics, + flags: visual_novel_json_to_value(snapshot.flags), + metrics: visual_novel_json_to_value(snapshot.metrics), history: snapshot .history .into_iter() .map(map_visual_novel_history_entry) .collect(), - available_choices: snapshot.available_choices, + available_choices: visual_novel_json_to_value(snapshot.available_choices), text_mode_enabled: snapshot.text_mode_enabled, created_at: format_timestamp_micros(snapshot.created_at_micros), updated_at: format_timestamp_micros(snapshot.updated_at_micros), @@ -3687,7 +3803,7 @@ fn map_visual_novel_run_snapshot(snapshot: VisualNovelRunJsonRecord) -> VisualNo } fn map_visual_novel_history_entry( - snapshot: VisualNovelHistoryEntryJsonRecord, + snapshot: VisualNovelRuntimeHistoryEntrySnapshot, ) -> VisualNovelHistoryEntryRecord { VisualNovelHistoryEntryRecord { entry_id: snapshot.entry_id, @@ -3697,7 +3813,7 @@ fn map_visual_novel_history_entry( turn_index: snapshot.turn_index, source: snapshot.source, action_text: snapshot.action_text, - steps: snapshot.steps, + steps: visual_novel_json_to_value(snapshot.steps), snapshot_before_hash: snapshot.snapshot_before_hash, snapshot_after_hash: snapshot.snapshot_after_hash, created_at: format_timestamp_micros(snapshot.created_at_micros), @@ -3705,7 +3821,7 @@ fn map_visual_novel_history_entry( } fn map_visual_novel_runtime_event( - snapshot: VisualNovelRuntimeEventJsonRecord, + snapshot: VisualNovelRuntimeEventSnapshot, ) -> VisualNovelRuntimeEventRecord { VisualNovelRuntimeEventRecord { event_id: snapshot.event_id, @@ -3715,11 +3831,32 @@ fn map_visual_novel_runtime_event( event_kind: snapshot.event_kind, client_event_id: snapshot.client_event_id, history_entry_id: snapshot.history_entry_id, - payload: snapshot.payload, + payload: visual_novel_json_to_value(snapshot.payload), occurred_at: format_timestamp_micros(snapshot.occurred_at_micros), } } +fn visual_novel_json_to_value(value: VisualNovelJsonValue) -> serde_json::Value { + match value { + VisualNovelJsonValue::Null => serde_json::Value::Null, + VisualNovelJsonValue::Bool(value) => serde_json::Value::Bool(value), + VisualNovelJsonValue::Number(value) => serde_json::Number::from_f64(value) + .map(serde_json::Value::Number) + .unwrap_or(serde_json::Value::Null), + VisualNovelJsonValue::String(value) => serde_json::Value::String(value), + VisualNovelJsonValue::Array(items) => { + serde_json::Value::Array(items.into_iter().map(visual_novel_json_to_value).collect()) + } + VisualNovelJsonValue::Object(fields) => { + let object = fields + .into_iter() + .map(|field| (field.key, visual_novel_json_to_value(field.value))) + .collect(); + serde_json::Value::Object(object) + } + } +} + fn normalize_match3d_stage(value: &str) -> &str { match value { "Collecting" | "collecting" | "collecting_config" => "collecting_config", @@ -3807,7 +3944,7 @@ fn i64_to_u64_ms(value: i64) -> u64 { } pub(crate) fn map_puzzle_suggested_action( - snapshot: DomainPuzzleAgentSuggestedAction, + snapshot: PuzzleAgentSuggestedAction, ) -> PuzzleAgentSuggestedActionRecord { PuzzleAgentSuggestedActionRecord { action_id: snapshot.id, @@ -3817,7 +3954,7 @@ pub(crate) fn map_puzzle_suggested_action( } pub(crate) fn map_puzzle_result_preview( - snapshot: DomainPuzzleResultPreviewEnvelope, + snapshot: PuzzleResultPreviewEnvelope, ) -> PuzzleResultPreviewRecord { PuzzleResultPreviewRecord { draft: map_puzzle_result_draft(snapshot.draft), @@ -3836,7 +3973,7 @@ pub(crate) fn map_puzzle_result_preview( } pub(crate) fn map_puzzle_result_preview_blocker( - snapshot: DomainPuzzleResultPreviewBlocker, + snapshot: PuzzleResultPreviewBlocker, ) -> PuzzleResultPreviewBlockerRecord { PuzzleResultPreviewBlockerRecord { blocker_id: snapshot.id, @@ -3846,7 +3983,7 @@ pub(crate) fn map_puzzle_result_preview_blocker( } pub(crate) fn map_puzzle_result_preview_finding( - snapshot: DomainPuzzleResultPreviewFinding, + snapshot: PuzzleResultPreviewFinding, ) -> PuzzleResultPreviewFindingRecord { PuzzleResultPreviewFindingRecord { finding_id: snapshot.id, @@ -3856,9 +3993,7 @@ pub(crate) fn map_puzzle_result_preview_finding( } } -pub(crate) fn map_puzzle_work_profile( - snapshot: DomainPuzzleWorkProfile, -) -> PuzzleWorkProfileRecord { +pub(crate) fn map_puzzle_work_profile(snapshot: PuzzleWorkProfile) -> PuzzleWorkProfileRecord { PuzzleWorkProfileRecord { work_id: snapshot.work_id, profile_id: snapshot.profile_id, @@ -3872,13 +4007,14 @@ pub(crate) fn map_puzzle_work_profile( theme_tags: snapshot.theme_tags, cover_image_src: snapshot.cover_image_src, cover_asset_id: snapshot.cover_asset_id, - publication_status: snapshot.publication_status.as_str().to_string(), + publication_status: format_puzzle_publication_status(snapshot.publication_status) + .to_string(), updated_at: format_timestamp_micros(snapshot.updated_at_micros), published_at: snapshot.published_at_micros.map(format_timestamp_micros), play_count: snapshot.play_count, remix_count: snapshot.remix_count, like_count: snapshot.like_count, - recent_play_count_7d: snapshot.recent_play_count_7d, + recent_play_count_7d: snapshot.recent_play_count_7_d, point_incentive_total_half_points: snapshot.point_incentive_total_half_points, point_incentive_claimed_points: snapshot.point_incentive_claimed_points, publish_ready: snapshot.publish_ready, @@ -3891,7 +4027,39 @@ pub(crate) fn map_puzzle_work_profile( } } -pub(crate) fn map_puzzle_run_snapshot(snapshot: DomainPuzzleRunSnapshot) -> PuzzleRunRecord { +pub(crate) fn map_puzzle_gallery_card_view_row( + snapshot: PuzzleGalleryCardViewRow, + recent_play_count_7d: u32, +) -> PuzzleGalleryCardRecord { + PuzzleGalleryCardRecord { + work_id: snapshot.work_id, + profile_id: snapshot.profile_id, + owner_user_id: snapshot.owner_user_id, + source_session_id: snapshot.source_session_id, + author_display_name: snapshot.author_display_name, + work_title: snapshot.work_title, + work_description: snapshot.work_description, + level_name: snapshot.level_name, + summary: snapshot.summary, + theme_tags: snapshot.theme_tags, + cover_image_src: snapshot.cover_image_src, + cover_asset_id: snapshot.cover_asset_id, + publication_status: format_puzzle_publication_status(snapshot.publication_status) + .to_string(), + updated_at: format_timestamp_micros(snapshot.updated_at_micros), + published_at: snapshot.published_at_micros.map(format_timestamp_micros), + play_count: snapshot.play_count, + remix_count: snapshot.remix_count, + like_count: snapshot.like_count, + recent_play_count_7d, + point_incentive_total_half_points: snapshot.point_incentive_total_half_points, + point_incentive_claimed_points: snapshot.point_incentive_claimed_points, + publish_ready: snapshot.publish_ready, + generation_status: snapshot.generation_status, + } +} + +pub(crate) fn map_puzzle_run_snapshot(snapshot: PuzzleRunSnapshot) -> PuzzleRunRecord { PuzzleRunRecord { run_id: snapshot.run_id, entry_profile_id: snapshot.entry_profile_id, @@ -3921,7 +4089,7 @@ pub(crate) fn map_puzzle_run_snapshot(snapshot: DomainPuzzleRunSnapshot) -> Puzz } fn map_puzzle_recommended_next_work( - snapshot: module_puzzle::PuzzleRecommendedNextWork, + snapshot: PuzzleRecommendedNextWork, ) -> PuzzleRecommendedNextWorkRecord { PuzzleRecommendedNextWorkRecord { profile_id: snapshot.profile_id, @@ -3934,10 +4102,10 @@ fn map_puzzle_recommended_next_work( } pub(crate) fn map_puzzle_runtime_level_snapshot( - snapshot: DomainPuzzleRuntimeLevelSnapshot, + snapshot: PuzzleRuntimeLevelSnapshot, ) -> PuzzleRuntimeLevelRecord { let started_at_ms = if snapshot.started_at_ms == 0 { - // 中文注释:旧 run_json 没有计时字段时只补一个可用开始时间,其余限时字段保持旧默认值。 + // 中文注释:运行态快照缺少可用开始时间时只补一个可用值,其余限时字段保持快照原值。 current_unix_millis_for_legacy_puzzle_snapshot() } else { snapshot.started_at_ms @@ -3957,7 +4125,7 @@ pub(crate) fn map_puzzle_runtime_level_snapshot( ui_background_image_object_key: snapshot.ui_background_image_object_key, background_music: snapshot.background_music.map(map_puzzle_audio_asset), board: map_puzzle_board_snapshot(snapshot.board), - status: snapshot.status.as_str().to_string(), + status: format_puzzle_runtime_level_status(snapshot.status).to_string(), started_at_ms, cleared_at_ms: snapshot.cleared_at_ms, elapsed_ms: snapshot.elapsed_ms, @@ -3984,7 +4152,7 @@ fn current_unix_millis_for_legacy_puzzle_snapshot() -> u64 { } pub(crate) fn map_puzzle_leaderboard_entry( - snapshot: module_puzzle::PuzzleLeaderboardEntry, + snapshot: PuzzleLeaderboardEntry, ) -> PuzzleLeaderboardEntryRecord { PuzzleLeaderboardEntryRecord { rank: snapshot.rank, @@ -3995,7 +4163,7 @@ pub(crate) fn map_puzzle_leaderboard_entry( } } -pub(crate) fn map_puzzle_board_snapshot(snapshot: DomainPuzzleBoardSnapshot) -> PuzzleBoardRecord { +pub(crate) fn map_puzzle_board_snapshot(snapshot: PuzzleBoardSnapshot) -> PuzzleBoardRecord { PuzzleBoardRecord { rows: snapshot.rows, cols: snapshot.cols, @@ -4014,7 +4182,7 @@ pub(crate) fn map_puzzle_board_snapshot(snapshot: DomainPuzzleBoardSnapshot) -> } } -pub(crate) fn map_puzzle_piece_state(snapshot: DomainPuzzlePieceState) -> PuzzlePieceStateRecord { +pub(crate) fn map_puzzle_piece_state(snapshot: PuzzlePieceState) -> PuzzlePieceStateRecord { PuzzlePieceStateRecord { piece_id: snapshot.piece_id, correct_row: snapshot.correct_row, @@ -4026,7 +4194,7 @@ pub(crate) fn map_puzzle_piece_state(snapshot: DomainPuzzlePieceState) -> Puzzle } pub(crate) fn map_puzzle_merged_group_state( - snapshot: DomainPuzzleMergedGroupState, + snapshot: PuzzleMergedGroupState, ) -> PuzzleMergedGroupRecord { PuzzleMergedGroupRecord { group_id: snapshot.group_id, @@ -4039,9 +4207,7 @@ pub(crate) fn map_puzzle_merged_group_state( } } -pub(crate) fn map_puzzle_cell_position( - snapshot: DomainPuzzleCellPosition, -) -> PuzzleCellPositionRecord { +pub(crate) fn map_puzzle_cell_position(snapshot: PuzzleCellPosition) -> PuzzleCellPositionRecord { PuzzleCellPositionRecord { row: snapshot.row, col: snapshot.col, @@ -4175,13 +4341,48 @@ pub(crate) fn map_big_fish_agent_message_snapshot( } } +pub(crate) fn map_big_fish_work_summary_snapshot( + snapshot: BigFishWorkSummarySnapshot, +) -> BigFishWorkSummaryRecord { + BigFishWorkSummaryRecord { + work_id: snapshot.work_id, + source_session_id: snapshot.source_session_id, + owner_user_id: snapshot.owner_user_id, + title: snapshot.title, + subtitle: snapshot.subtitle, + summary: snapshot.summary, + cover_image_src: snapshot.cover_image_src, + status: snapshot.status, + updated_at_micros: snapshot.updated_at_micros, + published_at_micros: snapshot.published_at_micros, + publish_ready: snapshot.publish_ready, + level_count: snapshot.level_count, + level_main_image_ready_count: snapshot.level_main_image_ready_count, + level_motion_ready_count: snapshot.level_motion_ready_count, + background_ready: snapshot.background_ready, + play_count: snapshot.play_count, + remix_count: snapshot.remix_count, + like_count: snapshot.like_count, + recent_play_count_7d: snapshot.recent_play_count_7_d, + } +} + +pub(crate) fn map_big_fish_gallery_view_row( + row: BigFishWorkSummarySnapshot, + recent_play_count_7d: u32, +) -> BigFishWorkSummaryRecord { + let mut record = map_big_fish_work_summary_snapshot(row); + record.recent_play_count_7d = recent_play_count_7d; + record +} + pub(crate) fn map_big_fish_runtime_snapshot( - snapshot: module_big_fish::BigFishRuntimeSnapshot, + snapshot: BigFishRuntimeSnapshot, ) -> BigFishRuntimeRunRecord { BigFishRuntimeRunRecord { run_id: snapshot.run_id, session_id: snapshot.session_id, - status: snapshot.status.as_str().to_string(), + status: format_big_fish_run_status(snapshot.status).to_string(), tick: snapshot.tick, player_level: snapshot.player_level, win_level: snapshot.win_level, @@ -4204,7 +4405,7 @@ pub(crate) fn map_big_fish_runtime_snapshot( } fn map_big_fish_runtime_entity_snapshot( - snapshot: module_big_fish::BigFishRuntimeEntitySnapshot, + snapshot: BigFishRuntimeEntitySnapshot, ) -> BigFishRuntimeEntityRecord { BigFishRuntimeEntityRecord { entity_id: snapshot.entity_id, @@ -4215,7 +4416,7 @@ fn map_big_fish_runtime_entity_snapshot( } } -fn map_big_fish_vector2(snapshot: module_big_fish::BigFishVector2) -> BigFishVector2Record { +fn map_big_fish_vector2(snapshot: BigFishVector2) -> BigFishVector2Record { BigFishVector2Record { x: snapshot.x, y: snapshot.y, @@ -4680,6 +4881,57 @@ pub(crate) fn parse_puzzle_agent_stage_record( } } +pub(crate) fn format_puzzle_agent_stage(value: PuzzleAgentStage) -> &'static str { + match value { + PuzzleAgentStage::CollectingAnchors => "collecting_anchors", + PuzzleAgentStage::DraftReady => "draft_ready", + PuzzleAgentStage::ImageRefining => "image_refining", + PuzzleAgentStage::ReadyToPublish => "ready_to_publish", + PuzzleAgentStage::Published => "published", + } +} + +pub(crate) fn format_puzzle_anchor_status(value: PuzzleAnchorStatus) -> &'static str { + match value { + PuzzleAnchorStatus::Missing => "missing", + PuzzleAnchorStatus::Inferred => "inferred", + PuzzleAnchorStatus::Confirmed => "confirmed", + PuzzleAnchorStatus::Locked => "locked", + } +} + +pub(crate) fn format_puzzle_agent_message_role(value: PuzzleAgentMessageRole) -> &'static str { + match value { + PuzzleAgentMessageRole::User => "user", + PuzzleAgentMessageRole::Assistant => "assistant", + PuzzleAgentMessageRole::System => "system", + } +} + +pub(crate) fn format_puzzle_agent_message_kind(value: PuzzleAgentMessageKind) -> &'static str { + match value { + PuzzleAgentMessageKind::Chat => "chat", + PuzzleAgentMessageKind::Summary => "summary", + PuzzleAgentMessageKind::ActionResult => "action_result", + PuzzleAgentMessageKind::Warning => "warning", + } +} + +pub(crate) fn format_puzzle_publication_status(value: PuzzlePublicationStatus) -> &'static str { + match value { + PuzzlePublicationStatus::Draft => "draft", + PuzzlePublicationStatus::Published => "published", + } +} + +pub(crate) fn format_puzzle_runtime_level_status(value: PuzzleRuntimeLevelStatus) -> &'static str { + match value { + PuzzleRuntimeLevelStatus::Playing => "playing", + PuzzleRuntimeLevelStatus::Cleared => "cleared", + PuzzleRuntimeLevelStatus::Failed => "failed", + } +} + pub(crate) fn parse_rpg_agent_stage_record( value: &str, ) -> Result { @@ -4927,6 +5179,14 @@ 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", @@ -6396,10 +6656,10 @@ pub struct Match3DResultDraftRecord { pub reference_image_src: Option, pub clear_count: u32, pub difficulty: u32, + pub generated_item_assets_json: Option, pub total_item_count: u32, pub publish_ready: bool, pub blockers: Vec, - pub generated_item_assets_json: Option, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -6504,137 +6764,6 @@ pub struct Match3DClickConfirmationRecord { pub run: Match3DRunRecord, } -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DCreatorConfigJsonRecord { - theme_text: String, - reference_image_src: Option, - clear_count: u32, - difficulty: u32, - #[serde(default)] - asset_style_id: Option, - #[serde(default)] - asset_style_label: Option, - #[serde(default)] - asset_style_prompt: Option, - #[serde(default)] - generate_click_sound: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DAgentMessageJsonRecord { - message_id: String, - #[allow(dead_code)] - session_id: String, - role: String, - kind: String, - text: String, - created_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DDraftJsonRecord { - profile_id: String, - game_name: String, - theme_text: String, - summary_text: String, - tags: Vec, - clear_count: u32, - difficulty: u32, - #[serde(default)] - generated_item_assets_json: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DAgentSessionJsonRecord { - session_id: String, - #[allow(dead_code)] - owner_user_id: String, - #[allow(dead_code)] - seed_text: String, - current_turn: u32, - progress_percent: u32, - stage: String, - config: Match3DCreatorConfigJsonRecord, - draft: Option, - messages: Vec, - last_assistant_reply: String, - published_profile_id: Option, - #[allow(dead_code)] - created_at_micros: i64, - updated_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DWorkJsonRecord { - profile_id: String, - owner_user_id: String, - source_session_id: String, - author_display_name: String, - game_name: String, - theme_text: String, - summary_text: String, - tags: Vec, - cover_image_src: String, - cover_asset_id: String, - clear_count: u32, - difficulty: u32, - config: Match3DCreatorConfigJsonRecord, - publication_status: String, - publish_ready: bool, - play_count: u32, - updated_at_micros: i64, - published_at_micros: Option, - #[serde(default)] - generated_item_assets_json: Option, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DItemJsonRecord { - item_instance_id: String, - item_type_id: String, - visual_key: String, - x: f32, - y: f32, - radius: f32, - layer: u32, - state: String, - clickable: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DTraySlotJsonRecord { - slot_index: u32, - item_instance_id: Option, - item_type_id: Option, - visual_key: Option, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct Match3DRunJsonRecord { - run_id: String, - profile_id: String, - status: String, - snapshot_version: u32, - started_at_ms: i64, - duration_limit_ms: i64, - server_now_ms: i64, - remaining_ms: i64, - clear_count: u32, - total_item_count: u32, - cleared_item_count: u32, - tray_slots: Vec, - items: Vec, - failure_reason: Option, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct SquareHoleAgentSessionCreateRecordInput { pub session_id: String, @@ -6954,109 +7083,6 @@ pub struct VisualNovelRuntimeEventRecord { pub occurred_at: String, } -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct VisualNovelAgentMessageJsonRecord { - message_id: String, - session_id: String, - role: String, - kind: String, - text: String, - created_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct VisualNovelAgentSessionJsonRecord { - session_id: String, - owner_user_id: String, - source_mode: String, - status: String, - seed_text: String, - source_asset_ids: Vec, - current_turn: u32, - progress_percent: u32, - messages: Vec, - draft: Option, - pending_action: Option, - last_assistant_reply: Option, - published_profile_id: Option, - created_at_micros: i64, - updated_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct VisualNovelWorkJsonRecord { - work_id: String, - profile_id: String, - owner_user_id: String, - source_session_id: Option, - author_display_name: String, - work_title: String, - work_description: String, - tags: Vec, - cover_image_src: Option, - source_asset_ids: Vec, - draft: serde_json::Value, - publication_status: String, - publish_ready: bool, - play_count: u32, - created_at_micros: i64, - updated_at_micros: i64, - published_at_micros: Option, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct VisualNovelHistoryEntryJsonRecord { - entry_id: String, - run_id: String, - owner_user_id: String, - profile_id: String, - turn_index: u32, - source: String, - action_text: Option, - steps: serde_json::Value, - snapshot_before_hash: Option, - snapshot_after_hash: Option, - created_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct VisualNovelRunJsonRecord { - run_id: String, - owner_user_id: String, - profile_id: String, - mode: String, - status: String, - current_scene_id: Option, - current_phase_id: Option, - visible_character_ids: Vec, - flags: serde_json::Value, - metrics: serde_json::Value, - history: Vec, - available_choices: serde_json::Value, - text_mode_enabled: bool, - created_at_micros: i64, - updated_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct VisualNovelRuntimeEventJsonRecord { - event_id: String, - run_id: Option, - owner_user_id: String, - profile_id: Option, - event_kind: String, - client_event_id: Option, - history_entry_id: Option, - payload: serde_json::Value, - occurred_at_micros: i64, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct SquareHoleAnchorItemRecord { pub key: String, @@ -7235,204 +7261,6 @@ pub struct SquareHoleDropConfirmationRecord { pub run: SquareHoleRunRecord, } -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleCreatorConfigJsonRecord { - theme_text: String, - twist_rule: String, - shape_count: u32, - difficulty: u32, - #[serde(default)] - shape_options: Vec, - #[serde(default)] - hole_options: Vec, - #[serde(default)] - background_prompt: String, - #[serde(default)] - cover_image_src: String, - #[serde(default)] - background_image_src: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleShapeOptionJsonRecord { - option_id: String, - shape_kind: String, - label: String, - #[serde(default)] - target_hole_id: String, - image_prompt: String, - #[serde(default)] - image_src: String, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleHoleOptionJsonRecord { - hole_id: String, - hole_kind: String, - label: String, - #[serde(default)] - image_prompt: String, - #[serde(default)] - image_src: String, - #[serde(default)] - bonus: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleAgentMessageJsonRecord { - message_id: String, - #[allow(dead_code)] - session_id: String, - role: String, - kind: String, - text: String, - created_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleDraftJsonRecord { - profile_id: String, - game_name: String, - theme_text: String, - twist_rule: String, - summary_text: String, - tags: Vec, - #[serde(default)] - cover_image_src: String, - #[serde(default)] - background_prompt: String, - #[serde(default)] - background_image_src: String, - #[serde(default)] - shape_options: Vec, - #[serde(default)] - hole_options: Vec, - shape_count: u32, - difficulty: u32, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleAgentSessionJsonRecord { - session_id: String, - #[allow(dead_code)] - owner_user_id: String, - #[allow(dead_code)] - seed_text: String, - current_turn: u32, - progress_percent: u32, - stage: String, - config: SquareHoleCreatorConfigJsonRecord, - draft: Option, - messages: Vec, - last_assistant_reply: String, - published_profile_id: Option, - #[allow(dead_code)] - created_at_micros: i64, - updated_at_micros: i64, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleWorkJsonRecord { - work_id: String, - profile_id: String, - owner_user_id: String, - source_session_id: String, - author_display_name: String, - game_name: String, - theme_text: String, - twist_rule: String, - summary_text: String, - tags: Vec, - cover_image_src: String, - #[serde(default)] - background_prompt: String, - #[serde(default)] - background_image_src: String, - #[serde(default)] - shape_options: Vec, - #[serde(default)] - hole_options: Vec, - shape_count: u32, - difficulty: u32, - #[allow(dead_code)] - config: SquareHoleCreatorConfigJsonRecord, - publication_status: String, - publish_ready: bool, - play_count: u32, - updated_at_micros: i64, - published_at_micros: Option, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleShapeJsonRecord { - shape_id: String, - shape_kind: String, - label: String, - #[serde(default)] - target_hole_id: String, - color: String, - #[serde(default)] - image_src: String, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleHoleJsonRecord { - hole_id: String, - hole_kind: String, - label: String, - x: f32, - y: f32, - #[serde(default)] - image_src: String, - #[serde(default)] - bonus: bool, -} - -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleDropFeedbackJsonRecord { - accepted: bool, - reject_reason: Option, - message: String, -} - -#[derive(Clone, Debug, PartialEq, serde::Deserialize)] -#[serde(rename_all = "camelCase")] -struct SquareHoleRunJsonRecord { - run_id: String, - profile_id: String, - owner_user_id: String, - status: String, - snapshot_version: u64, - started_at_ms: i64, - duration_limit_ms: i64, - server_now_ms: i64, - remaining_ms: i64, - total_shape_count: u32, - completed_shape_count: u32, - combo: u32, - best_combo: u32, - score: u32, - rule_label: String, - #[serde(default)] - background_image_src: String, - #[serde(default)] - #[allow(dead_code)] - shape_options: Vec, - current_shape: Option, - holes: Vec, - last_feedback: Option, -} - #[derive(Clone, Debug, PartialEq, Eq)] pub struct PuzzleAnchorItemRecord { pub key: String, @@ -7612,6 +7440,33 @@ pub struct PuzzleWorkProfileRecord { pub levels: Vec, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PuzzleGalleryCardRecord { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub publication_status: String, + pub updated_at: String, + pub published_at: Option, + pub play_count: u32, + pub remix_count: u32, + pub like_count: u32, + pub recent_play_count_7d: u32, + pub point_incentive_total_half_points: u64, + pub point_incentive_claimed_points: u64, + pub publish_ready: bool, + pub generation_status: Option, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct PuzzleWorkPointIncentiveClaimRecordInput { pub profile_id: String, @@ -7954,184 +7809,122 @@ pub struct BigFishWorkSummaryRecord { pub recent_play_count_7d: u32, } -#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize)] -struct CompatibleBigFishWorkSummaryRecord { - work_id: String, - source_session_id: String, - #[serde(default)] - owner_user_id: Option, - title: String, - subtitle: String, - summary: String, - cover_image_src: Option, - status: String, - updated_at_micros: i64, - #[serde(default)] - published_at_micros: Option, - publish_ready: bool, - level_count: u32, - level_main_image_ready_count: u32, - level_motion_ready_count: u32, - background_ready: bool, - #[serde(default)] - play_count: u32, - #[serde(default)] - remix_count: u32, - #[serde(default)] - like_count: u32, - #[serde(default)] - recent_play_count_7d: u32, -} - -impl CompatibleBigFishWorkSummaryRecord { - fn into_record(self, fallback_owner_user_id: Option<&str>) -> BigFishWorkSummaryRecord { - BigFishWorkSummaryRecord { - work_id: self.work_id, - source_session_id: self.source_session_id, - // 中文注释:兼容旧 works JSON 没有 owner_user_id 的历史数据,避免一次字段升级把整个作品列表打崩。 - owner_user_id: self.owner_user_id.unwrap_or_else(|| { - fallback_owner_user_id - .map(str::to_string) - .unwrap_or_default() - }), - title: self.title, - subtitle: self.subtitle, - summary: self.summary, - cover_image_src: self.cover_image_src, - status: self.status, - updated_at_micros: self.updated_at_micros, - published_at_micros: self.published_at_micros, - publish_ready: self.publish_ready, - level_count: self.level_count, - level_main_image_ready_count: self.level_main_image_ready_count, - level_motion_ready_count: self.level_motion_ready_count, - background_ready: self.background_ready, - play_count: self.play_count, - remix_count: self.remix_count, - like_count: self.like_count, - recent_play_count_7d: self.recent_play_count_7d, - } - } -} - #[cfg(test)] mod tests { use super::*; #[test] - fn puzzle_works_mapper_backfills_missing_public_stat_fields() { + fn puzzle_works_mapper_keeps_typed_public_stat_fields() { let result = PuzzleWorksProcedureResult { ok: true, - items_json: Some( - r#"[{ - "work_id":"puzzle-work-1", - "profile_id":"puzzle-profile-1", - "owner_user_id":"user-1", - "source_session_id":null, - "author_display_name":"测试作者", - "level_name":"雨夜拼图", - "summary":"旧公开作品摘要", - "theme_tags":["雨夜","猫咪","神庙"], - "cover_image_src":null, - "cover_asset_id":null, - "publication_status":"Published", - "updated_at_micros":123000000, - "published_at_micros":123000000, - "publish_ready":true, - "anchor_pack":{ - "theme_promise":{ - "key":"themePromise", - "label":"题材承诺", - "value":"雨夜冒险", - "status":"Inferred" - }, - "visual_subject":{ - "key":"visualSubject", - "label":"画面主体", - "value":"猫咪神庙", - "status":"Inferred" - }, - "visual_mood":{ - "key":"visualMood", - "label":"视觉气质", - "value":"温暖", - "status":"Inferred" - }, - "composition_hooks":{ - "key":"compositionHooks", - "label":"拼图记忆点", - "value":"灯光", - "status":"Inferred" - }, - "tags_and_forbidden":{ - "key":"tagsAndForbidden", - "label":"标签与禁忌", - "value":"雨夜, 猫咪, 神庙", - "status":"Inferred" - } - } - }]"# - .to_string(), - ), + items: vec![PuzzleWorkProfile { + work_id: "puzzle-work-1".to_string(), + profile_id: "puzzle-profile-1".to_string(), + owner_user_id: "user-1".to_string(), + source_session_id: None, + author_display_name: "测试作者".to_string(), + work_title: "雨夜拼图作品".to_string(), + work_description: "拼图作品说明".to_string(), + level_name: "雨夜拼图".to_string(), + summary: "公开作品摘要".to_string(), + theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()], + cover_image_src: None, + cover_asset_id: None, + levels: Vec::new(), + publication_status: PuzzlePublicationStatus::Published, + updated_at_micros: 123000000, + published_at_micros: Some(123000000), + play_count: 11, + remix_count: 7, + like_count: 5, + recent_play_count_7_d: 3, + point_incentive_total_half_points: 4, + point_incentive_claimed_points: 2, + publish_ready: true, + anchor_pack: test_puzzle_anchor_pack(), + }], error_message: None, }; let items = map_puzzle_works_procedure_result(result) - .expect("旧 puzzle works JSON 缺统计字段时应按 0 兼容"); + .expect("typed puzzle works result 应能映射统计字段"); assert_eq!(items.len(), 1); - assert_eq!(items[0].play_count, 0); - assert_eq!(items[0].remix_count, 0); - assert_eq!(items[0].like_count, 0); + assert_eq!(items[0].play_count, 11); + assert_eq!(items[0].remix_count, 7); + assert_eq!(items[0].like_count, 5); + assert_eq!(items[0].recent_play_count_7d, 3); } #[test] - fn puzzle_run_mapper_backfills_missing_timer_fields() { + fn puzzle_run_mapper_maps_typed_timer_fields() { let result = PuzzleRunProcedureResult { ok: true, - run_json: Some( - r#"{ - "run_id":"puzzle-run-1", - "entry_profile_id":"puzzle-profile-1", - "cleared_level_count":0, - "current_level_index":1, - "current_grid_size":3, - "played_profile_ids":["puzzle-profile-1"], - "previous_level_tags":["雨夜","猫咪","神庙"], - "current_level":{ - "run_id":"puzzle-run-1", - "level_index":1, - "grid_size":3, - "profile_id":"puzzle-profile-1", - "level_name":"雨夜拼图", - "author_display_name":"测试作者", - "theme_tags":["雨夜","猫咪","神庙"], - "cover_image_src":null, - "board":{ - "rows":3, - "cols":3, - "pieces":[{ - "piece_id":"piece-1", - "correct_row":0, - "correct_col":0, - "current_row":0, - "current_col":0, - "merged_group_id":null - }], - "merged_groups":[], - "selected_piece_id":null - }, - "status":"Playing" + run: Some(PuzzleRunSnapshot { + run_id: "puzzle-run-1".to_string(), + entry_profile_id: "puzzle-profile-1".to_string(), + cleared_level_count: 0, + current_level_index: 1, + current_grid_size: 3, + played_profile_ids: vec!["puzzle-profile-1".to_string()], + previous_level_tags: vec![ + "雨夜".to_string(), + "猫咪".to_string(), + "神庙".to_string(), + ], + current_level: Some(PuzzleRuntimeLevelSnapshot { + run_id: "puzzle-run-1".to_string(), + level_index: 1, + level_id: None, + grid_size: 3, + profile_id: "puzzle-profile-1".to_string(), + level_name: "雨夜拼图".to_string(), + author_display_name: "测试作者".to_string(), + theme_tags: vec!["雨夜".to_string(), "猫咪".to_string(), "神庙".to_string()], + cover_image_src: None, + ui_background_image_src: None, + ui_background_image_object_key: None, + background_music: None, + board: PuzzleBoardSnapshot { + rows: 3, + cols: 3, + pieces: vec![PuzzlePieceState { + piece_id: "piece-1".to_string(), + correct_row: 0, + correct_col: 0, + current_row: 0, + current_col: 0, + merged_group_id: None, + }], + merged_groups: Vec::new(), + selected_piece_id: None, + all_tiles_resolved: false, }, - "recommended_next_profile_id":null - }"# - .to_string(), - ), + status: PuzzleRuntimeLevelStatus::Playing, + started_at_ms: 0, + cleared_at_ms: None, + elapsed_ms: None, + time_limit_ms: 0, + remaining_ms: 0, + paused_accumulated_ms: 0, + pause_started_at_ms: None, + freeze_accumulated_ms: 0, + freeze_started_at_ms: None, + freeze_until_ms: None, + leaderboard_entries: Vec::new(), + }), + recommended_next_profile_id: None, + next_level_mode: "none".to_string(), + next_level_profile_id: None, + next_level_id: None, + recommended_next_works: Vec::new(), + leaderboard_entries: Vec::new(), + }), error_message: None, }; let run = map_puzzle_run_procedure_result(result) - .expect("旧 puzzle run JSON 缺计时字段时应按默认值兼容"); + .expect("typed puzzle run result 应能映射计时字段"); let level = run.current_level.expect("兼容后仍应保留当前关卡"); assert_eq!(run.run_id, "puzzle-run-1"); @@ -8142,115 +7935,87 @@ mod tests { } #[test] - fn big_fish_works_mapper_backfills_missing_owner_user_id_for_private_lists() { + fn big_fish_works_mapper_uses_typed_owner_and_public_stats() { let result = BigFishWorksProcedureResult { ok: true, - items_json: Some( - r#"[{ - "work_id":"big-fish-work-session-1", - "source_session_id":"session-1", - "title":"深海草稿", - "subtitle":"副标题", - "summary":"摘要", - "cover_image_src":null, - "status":"draft", - "updated_at_micros":123, - "publish_ready":false, - "level_count":8, - "level_main_image_ready_count":0, - "level_motion_ready_count":0, - "background_ready":false - }]"# - .to_string(), - ), + items: vec![BigFishWorkSummarySnapshot { + work_id: "big-fish-work-session-1".to_string(), + source_session_id: "session-1".to_string(), + owner_user_id: "user-1".to_string(), + title: "深海草稿".to_string(), + subtitle: "副标题".to_string(), + summary: "摘要".to_string(), + cover_image_src: None, + status: "draft".to_string(), + updated_at_micros: 123, + publish_ready: false, + level_count: 8, + level_main_image_ready_count: 0, + level_motion_ready_count: 0, + background_ready: false, + play_count: 9, + remix_count: 4, + like_count: 2, + recent_play_count_7_d: 6, + published_at_micros: None, + }], error_message: None, }; let items = map_big_fish_works_procedure_result(result, Some("user-1")) - .expect("旧 works JSON 应能被兼容解析"); + .expect("typed big fish works result 应能映射 owner 和统计字段"); assert_eq!(items.len(), 1); assert_eq!(items[0].owner_user_id, "user-1"); assert_eq!(items[0].published_at_micros, None); - assert_eq!(items[0].play_count, 0); - assert_eq!(items[0].remix_count, 0); - assert_eq!(items[0].like_count, 0); - } - - #[test] - fn big_fish_works_mapper_keeps_empty_owner_when_gallery_legacy_json_lacks_field() { - let result = BigFishWorksProcedureResult { - ok: true, - items_json: Some( - r#"[{ - "work_id":"big-fish-work-session-2", - "source_session_id":"session-2", - "title":"公开作品", - "subtitle":"副标题", - "summary":"摘要", - "cover_image_src":null, - "status":"published", - "updated_at_micros":456, - "publish_ready":true, - "level_count":8, - "level_main_image_ready_count":8, - "level_motion_ready_count":16, - "background_ready":true - }]"# - .to_string(), - ), - error_message: None, - }; - - let items = map_big_fish_works_procedure_result(result, None) - .expect("公开 works 旧 JSON 也不应因缺字段报错"); - - assert_eq!(items.len(), 1); - assert!(items[0].owner_user_id.is_empty()); - assert_eq!(items[0].published_at_micros, None); - assert_eq!(items[0].play_count, 0); - assert_eq!(items[0].remix_count, 0); - assert_eq!(items[0].like_count, 0); + assert_eq!(items[0].play_count, 9); + assert_eq!(items[0].remix_count, 4); + assert_eq!(items[0].like_count, 2); + assert_eq!(items[0].recent_play_count_7d, 6); } #[test] fn match3d_work_mapper_keeps_generated_item_assets_json() { let result = Match3DWorkProcedureResult { ok: true, - work_json: Some( - r#"{ - "profileId":"match3d-profile-1", - "ownerUserId":"user-1", - "sourceSessionId":"match3d-session-1", - "authorDisplayName":"测试作者", - "gameName":"水果抓大鹅", - "themeText":"水果", - "summaryText":"水果主题", - "tags":["水果"], - "coverImageSrc":"", - "coverAssetId":"", - "clearCount":3, - "difficulty":3, - "config":{ - "themeText":"水果", - "referenceImageSrc":null, - "clearCount":3, - "difficulty":3 - }, - "publicationStatus":"Draft", - "publishReady":false, - "playCount":0, - "updatedAtMicros":123000000, - "publishedAtMicros":null, - "generatedItemAssetsJson":"[{\"itemId\":\"match3d-item-1\",\"itemName\":\"草莓\",\"imageSrc\":\"/generated-match3d-assets/session/profile/items/item/image.png\",\"status\":\"image_ready\"}]" - }"# - .to_string(), - ), + work: Some(Match3DWorkSnapshot { + profile_id: "match3d-profile-1".to_string(), + owner_user_id: "user-1".to_string(), + source_session_id: "match3d-session-1".to_string(), + author_display_name: "测试作者".to_string(), + game_name: "水果抓大鹅".to_string(), + theme_text: "水果".to_string(), + summary_text: "水果主题".to_string(), + tags: vec!["水果".to_string()], + cover_image_src: String::new(), + cover_asset_id: String::new(), + clear_count: 3, + difficulty: 3, + config: Match3DCreatorConfigSnapshot { + theme_text: "水果".to_string(), + reference_image_src: None, + clear_count: 3, + difficulty: 3, + asset_style_id: None, + asset_style_label: None, + asset_style_prompt: None, + generate_click_sound: false, + }, + publication_status: "Draft".to_string(), + publish_ready: false, + play_count: 0, + updated_at_micros: 123000000, + published_at_micros: None, + generated_item_assets_json: Some( + r#"[{"itemId":"match3d-item-1","itemName":"草莓","imageSrc":"/generated-match3d-assets/session/profile/items/item/image.png","status":"image_ready"}]"# + .to_string(), + ), + }), error_message: None, }; let item = map_match3d_work_procedure_result(result) - .expect("match3d work JSON 应保留生成素材 JSON"); + .expect("typed match3d work result 应保留生成素材 JSON"); assert_eq!( item.generated_item_assets_json.as_deref(), @@ -8259,6 +8024,29 @@ mod tests { ) ); } + + fn test_puzzle_anchor_pack() -> PuzzleAnchorPack { + PuzzleAnchorPack { + theme_promise: test_puzzle_anchor_item("themePromise", "题材承诺", "雨夜冒险"), + visual_subject: test_puzzle_anchor_item("visualSubject", "画面主体", "猫咪神庙"), + visual_mood: test_puzzle_anchor_item("visualMood", "视觉气质", "温暖"), + composition_hooks: test_puzzle_anchor_item("compositionHooks", "拼图记忆点", "灯光"), + tags_and_forbidden: test_puzzle_anchor_item( + "tagsAndForbidden", + "标签与禁忌", + "雨夜, 猫咪, 神庙", + ), + } + } + + fn test_puzzle_anchor_item(key: &str, label: &str, value: &str) -> PuzzleAnchorItem { + PuzzleAnchorItem { + key: key.to_string(), + label: label.to_string(), + value: value.to_string(), + status: PuzzleAnchorStatus::Inferred, + } + } } #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/server-rs/crates/spacetime-client/src/match3d.rs b/server-rs/crates/spacetime-client/src/match3d.rs index baaf7bb9..df7fb762 100644 --- a/server-rs/crates/spacetime-client/src/match3d.rs +++ b/server-rs/crates/spacetime-client/src/match3d.rs @@ -225,10 +225,22 @@ impl SpacetimeClient { pub async fn list_match3d_gallery( &self, ) -> Result, SpacetimeClientError> { - self.list_match3d_works_with_input(Match3DWorksListInput { - // 中文注释:公开广场读取只依赖 published_only,owner_user_id 保持非空便于兼容校验。 - owner_user_id: "match3d-public-gallery".to_string(), - published_only: true, + self.read_after_connect("list_match3d_gallery", move |connection| { + let mut items = connection + .db() + .match_3_d_gallery_view() + .iter() + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + Ok(items + .into_iter() + .map(map_match3d_gallery_view_row) + .collect()) }) .await } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_snapshot_type.rs new file mode 100644 index 00000000..1271082f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_draft_config_snapshot_type.rs @@ -0,0 +1,25 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BarkBattleDraftConfigSnapshot { + pub draft_id: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub editor_state_json: String, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for BarkBattleDraftConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs index 6fe7a3ee..03e6fe2c 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_procedure_result_type.rs @@ -4,11 +4,17 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::bark_battle_draft_config_snapshot_type::BarkBattleDraftConfigSnapshot; +use super::bark_battle_run_snapshot_type::BarkBattleRunSnapshot; +use super::bark_battle_runtime_config_snapshot_type::BarkBattleRuntimeConfigSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct BarkBattleProcedureResult { pub ok: bool, - pub row_json: Option, + pub draft_config: Option, + pub runtime_config: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_snapshot_type.rs new file mode 100644 index 00000000..474af775 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_run_snapshot_type.rs @@ -0,0 +1,32 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BarkBattleRunSnapshot { + pub run_id: String, + pub owner_user_id: String, + pub work_id: String, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub status: String, + pub client_started_at_micros: i64, + pub server_started_at_micros: i64, + pub client_finished_at_micros: Option, + pub server_finished_at_micros: Option, + pub metrics_json: String, + pub server_result: Option, + pub validation_status: String, + pub anti_cheat_flags_json: String, + pub leaderboard_score: Option, + pub score_id: Option, +} + +impl __sdk::InModule for BarkBattleRunSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_config_snapshot_type.rs new file mode 100644 index 00000000..e176ca63 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/bark_battle_runtime_config_snapshot_type.rs @@ -0,0 +1,25 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BarkBattleRuntimeConfigSnapshot { + pub work_id: String, + pub owner_user_id: String, + pub source_draft_id: Option, + pub config_version: u64, + pub ruleset_version: String, + pub difficulty_preset: String, + pub leaderboard_enabled: bool, + pub config_json: String, + pub published_snapshot_json: String, + pub published_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for BarkBattleRuntimeConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs index 572889b8..d87690de 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_creation_session_type.rs @@ -92,6 +92,7 @@ impl __sdk::__query_builder::HasCols for BigFishCreationSession { pub struct BigFishCreationSessionIxCols { pub owner_user_id: __sdk::__query_builder::IxCol, pub session_id: __sdk::__query_builder::IxCol, + pub stage: __sdk::__query_builder::IxCol, } impl __sdk::__query_builder::HasIxCols for BigFishCreationSession { @@ -100,6 +101,7 @@ impl __sdk::__query_builder::HasIxCols for BigFishCreationSession { BigFishCreationSessionIxCols { owner_user_id: __sdk::__query_builder::IxCol::new(table_name, "owner_user_id"), session_id: __sdk::__query_builder::IxCol::new(table_name, "session_id"), + stage: __sdk::__query_builder::IxCol::new(table_name, "stage"), } } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_gallery_view_table.rs new file mode 100644 index 00000000..2d9419c4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_gallery_view_table.rs @@ -0,0 +1,114 @@ +// 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 super::big_fish_work_summary_snapshot_type::BigFishWorkSummarySnapshot; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `big_fish_gallery_view`. +/// +/// Obtain a handle from the [`BigFishGalleryViewTableAccess::big_fish_gallery_view`] method on [`super::RemoteTables`], +/// like `ctx.db.big_fish_gallery_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.big_fish_gallery_view().on_insert(...)`. +pub struct BigFishGalleryViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `big_fish_gallery_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait BigFishGalleryViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`BigFishGalleryViewTableHandle`], which mediates access to the table `big_fish_gallery_view`. + fn big_fish_gallery_view(&self) -> BigFishGalleryViewTableHandle<'_>; +} + +impl BigFishGalleryViewTableAccess for super::RemoteTables { + fn big_fish_gallery_view(&self) -> BigFishGalleryViewTableHandle<'_> { + BigFishGalleryViewTableHandle { + imp: self + .imp + .get_table::("big_fish_gallery_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct BigFishGalleryViewInsertCallbackId(__sdk::CallbackId); +pub struct BigFishGalleryViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for BigFishGalleryViewTableHandle<'ctx> { + type Row = BigFishWorkSummarySnapshot; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = BigFishGalleryViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishGalleryViewInsertCallbackId { + BigFishGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: BigFishGalleryViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = BigFishGalleryViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> BigFishGalleryViewDeleteCallbackId { + BigFishGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: BigFishGalleryViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("big_fish_gallery_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `BigFishWorkSummarySnapshot`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait big_fish_gallery_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `BigFishWorkSummarySnapshot`. + fn big_fish_gallery_view(&self) -> __sdk::__query_builder::Table; +} + +impl big_fish_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn big_fish_gallery_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("big_fish_gallery_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_type.rs index 2dc3db1d..86d73fc2 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_run_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::big_fish_runtime_snapshot_type::BigFishRuntimeSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct BigFishRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_entity_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_entity_snapshot_type.rs new file mode 100644 index 00000000..ce829b70 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_entity_snapshot_type.rs @@ -0,0 +1,21 @@ +// 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::big_fish_vector_2_type::BigFishVector2; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishRuntimeEntitySnapshot { + pub entity_id: String, + pub level: u32, + pub position: BigFishVector2, + pub radius: f32, + pub offscreen_seconds: f32, +} + +impl __sdk::InModule for BigFishRuntimeEntitySnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_snapshot_type.rs new file mode 100644 index 00000000..48f71186 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_runtime_snapshot_type.rs @@ -0,0 +1,31 @@ +// 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::big_fish_run_status_type::BigFishRunStatus; +use super::big_fish_runtime_entity_snapshot_type::BigFishRuntimeEntitySnapshot; +use super::big_fish_vector_2_type::BigFishVector2; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishRuntimeSnapshot { + pub run_id: String, + pub session_id: String, + pub status: BigFishRunStatus, + pub tick: u64, + pub player_level: u32, + pub win_level: u32, + pub leader_entity_id: Option, + pub owned_entities: Vec, + pub wild_entities: Vec, + pub camera_center: BigFishVector2, + pub last_input: BigFishVector2, + pub event_log: Vec, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for BigFishRuntimeSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_vector_2_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_vector_2_type.rs new file mode 100644 index 00000000..745063ad --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_vector_2_type.rs @@ -0,0 +1,16 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishVector2 { + pub x: f32, + pub y: f32, +} + +impl __sdk::InModule for BigFishVector2 { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_work_summary_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_work_summary_snapshot_type.rs new file mode 100644 index 00000000..9bdeeedb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_work_summary_snapshot_type.rs @@ -0,0 +1,97 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct BigFishWorkSummarySnapshot { + pub work_id: String, + pub source_session_id: String, + pub owner_user_id: String, + pub title: String, + pub subtitle: String, + pub summary: String, + pub cover_image_src: Option, + pub status: String, + pub updated_at_micros: i64, + pub publish_ready: bool, + pub level_count: u32, + pub level_main_image_ready_count: u32, + pub level_motion_ready_count: u32, + pub background_ready: bool, + pub play_count: u32, + pub remix_count: u32, + pub like_count: u32, + pub recent_play_count_7_d: u32, + pub published_at_micros: Option, +} + +impl __sdk::InModule for BigFishWorkSummarySnapshot { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `BigFishWorkSummarySnapshot`. +/// +/// Provides typed access to columns for query building. +pub struct BigFishWorkSummarySnapshotCols { + pub work_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub title: __sdk::__query_builder::Col, + pub subtitle: __sdk::__query_builder::Col, + pub summary: __sdk::__query_builder::Col, + pub cover_image_src: __sdk::__query_builder::Col>, + pub status: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub level_count: __sdk::__query_builder::Col, + pub level_main_image_ready_count: __sdk::__query_builder::Col, + pub level_motion_ready_count: __sdk::__query_builder::Col, + pub background_ready: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub remix_count: __sdk::__query_builder::Col, + pub like_count: __sdk::__query_builder::Col, + pub recent_play_count_7_d: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for BigFishWorkSummarySnapshot { + type Cols = BigFishWorkSummarySnapshotCols; + fn cols(table_name: &'static str) -> Self::Cols { + BigFishWorkSummarySnapshotCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + title: __sdk::__query_builder::Col::new(table_name, "title"), + subtitle: __sdk::__query_builder::Col::new(table_name, "subtitle"), + summary: __sdk::__query_builder::Col::new(table_name, "summary"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + status: __sdk::__query_builder::Col::new(table_name, "status"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + level_count: __sdk::__query_builder::Col::new(table_name, "level_count"), + level_main_image_ready_count: __sdk::__query_builder::Col::new( + table_name, + "level_main_image_ready_count", + ), + level_motion_ready_count: __sdk::__query_builder::Col::new( + table_name, + "level_motion_ready_count", + ), + background_ready: __sdk::__query_builder::Col::new(table_name, "background_ready"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"), + like_count: __sdk::__query_builder::Col::new(table_name, "like_count"), + recent_play_count_7_d: __sdk::__query_builder::Col::new( + table_name, + "recent_play_count_7_d", + ), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_works_procedure_result_type.rs index 37d7c7b6..ea3cac68 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/big_fish_works_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/big_fish_works_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::big_fish_work_summary_snapshot_type::BigFishWorkSummarySnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct BigFishWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_snapshot_type.rs new file mode 100644 index 00000000..b157584f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_message_snapshot_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentMessageSnapshot { + pub message_id: String, + pub session_id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at_micros: i64, +} + +impl __sdk::InModule for Match3DAgentMessageSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs index 45f54f93..ca860890 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::match_3_d_agent_session_snapshot_type::Match3DAgentSessionSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct Match3DAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_snapshot_type.rs new file mode 100644 index 00000000..f0ea685a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_agent_session_snapshot_type.rs @@ -0,0 +1,31 @@ +// 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::match_3_d_agent_message_snapshot_type::Match3DAgentMessageSnapshot; +use super::match_3_d_creator_config_snapshot_type::Match3DCreatorConfigSnapshot; +use super::match_3_d_draft_snapshot_type::Match3DDraftSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config: Match3DCreatorConfigSnapshot, + pub draft: Option, + pub messages: Vec, + pub last_assistant_reply: String, + pub published_profile_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for Match3DAgentSessionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs index 80f32a59..c8a58510 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_click_item_procedure_result_type.rs @@ -4,12 +4,14 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::match_3_d_run_snapshot_type::Match3DRunSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct Match3DClickItemProcedureResult { pub ok: bool, pub status: String, - pub run_json: Option, + pub run: Option, pub accepted_item_instance_id: Option, pub cleared_item_instance_ids: Vec, pub failure_reason: Option, diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_creator_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_creator_config_snapshot_type.rs new file mode 100644 index 00000000..a0fd2d61 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_creator_config_snapshot_type.rs @@ -0,0 +1,22 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DCreatorConfigSnapshot { + pub theme_text: String, + pub reference_image_src: Option, + pub clear_count: u32, + pub difficulty: u32, + pub asset_style_id: Option, + pub asset_style_label: Option, + pub asset_style_prompt: Option, + pub generate_click_sound: bool, +} + +impl __sdk::InModule for Match3DCreatorConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_draft_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_draft_snapshot_type.rs new file mode 100644 index 00000000..32a67c83 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_draft_snapshot_type.rs @@ -0,0 +1,22 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DDraftSnapshot { + pub profile_id: String, + pub game_name: String, + pub theme_text: String, + pub summary_text: String, + pub tags: Vec, + pub clear_count: u32, + pub difficulty: u32, + pub generated_item_assets_json: Option, +} + +impl __sdk::InModule for Match3DDraftSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_gallery_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_gallery_view_row_type.rs new file mode 100644 index 00000000..03b768d3 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_gallery_view_row_type.rs @@ -0,0 +1,98 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DGalleryViewRow { + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub game_name: String, + pub theme_text: String, + pub summary_text: String, + pub tags: Vec, + pub cover_image_src: String, + pub cover_asset_id: String, + pub reference_image_src: Option, + pub clear_count: u32, + pub difficulty: u32, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub generated_item_assets_json: Option, +} + +impl __sdk::InModule for Match3DGalleryViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `Match3DGalleryViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct Match3DGalleryViewRowCols { + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub game_name: __sdk::__query_builder::Col, + pub theme_text: __sdk::__query_builder::Col, + pub summary_text: __sdk::__query_builder::Col, + pub tags: __sdk::__query_builder::Col>, + pub cover_image_src: __sdk::__query_builder::Col, + pub cover_asset_id: __sdk::__query_builder::Col, + pub reference_image_src: __sdk::__query_builder::Col>, + pub clear_count: __sdk::__query_builder::Col, + pub difficulty: __sdk::__query_builder::Col, + pub publication_status: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, + pub generated_item_assets_json: + __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for Match3DGalleryViewRow { + type Cols = Match3DGalleryViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + Match3DGalleryViewRowCols { + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + game_name: __sdk::__query_builder::Col::new(table_name, "game_name"), + theme_text: __sdk::__query_builder::Col::new(table_name, "theme_text"), + summary_text: __sdk::__query_builder::Col::new(table_name, "summary_text"), + tags: __sdk::__query_builder::Col::new(table_name, "tags"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + cover_asset_id: __sdk::__query_builder::Col::new(table_name, "cover_asset_id"), + reference_image_src: __sdk::__query_builder::Col::new( + table_name, + "reference_image_src", + ), + clear_count: __sdk::__query_builder::Col::new(table_name, "clear_count"), + difficulty: __sdk::__query_builder::Col::new(table_name, "difficulty"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + generated_item_assets_json: __sdk::__query_builder::Col::new( + table_name, + "generated_item_assets_json", + ), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_gallery_view_table.rs new file mode 100644 index 00000000..b47d62a3 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_gallery_view_table.rs @@ -0,0 +1,113 @@ +// 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 super::match_3_d_gallery_view_row_type::Match3DGalleryViewRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `match_3_d_gallery_view`. +/// +/// Obtain a handle from the [`Match3DGalleryViewTableAccess::match_3_d_gallery_view`] method on [`super::RemoteTables`], +/// like `ctx.db.match_3_d_gallery_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.match_3_d_gallery_view().on_insert(...)`. +pub struct Match3DGalleryViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `match_3_d_gallery_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait Match3DGalleryViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`Match3DGalleryViewTableHandle`], which mediates access to the table `match_3_d_gallery_view`. + fn match_3_d_gallery_view(&self) -> Match3DGalleryViewTableHandle<'_>; +} + +impl Match3DGalleryViewTableAccess for super::RemoteTables { + fn match_3_d_gallery_view(&self) -> Match3DGalleryViewTableHandle<'_> { + Match3DGalleryViewTableHandle { + imp: self + .imp + .get_table::("match_3_d_gallery_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct Match3DGalleryViewInsertCallbackId(__sdk::CallbackId); +pub struct Match3DGalleryViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for Match3DGalleryViewTableHandle<'ctx> { + type Row = Match3DGalleryViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = Match3DGalleryViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> Match3DGalleryViewInsertCallbackId { + Match3DGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: Match3DGalleryViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = Match3DGalleryViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> Match3DGalleryViewDeleteCallbackId { + Match3DGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: Match3DGalleryViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = client_cache.get_or_make_table::("match_3_d_gallery_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `Match3DGalleryViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait match_3_d_gallery_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `Match3DGalleryViewRow`. + fn match_3_d_gallery_view(&self) -> __sdk::__query_builder::Table; +} + +impl match_3_d_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn match_3_d_gallery_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("match_3_d_gallery_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_item_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_item_snapshot_type.rs new file mode 100644 index 00000000..fefdd184 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_item_snapshot_type.rs @@ -0,0 +1,23 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DItemSnapshot { + pub item_instance_id: String, + pub item_type_id: String, + pub visual_key: String, + pub x: f32, + pub y: f32, + pub radius: f32, + pub layer: u32, + pub state: String, + pub clickable: bool, +} + +impl __sdk::InModule for Match3DItemSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs index f3c4ceec..56da83e6 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::match_3_d_run_snapshot_type::Match3DRunSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct Match3DRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_snapshot_type.rs new file mode 100644 index 00000000..3165a471 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_run_snapshot_type.rs @@ -0,0 +1,31 @@ +// 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::match_3_d_item_snapshot_type::Match3DItemSnapshot; +use super::match_3_d_tray_slot_snapshot_type::Match3DTraySlotSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DRunSnapshot { + pub run_id: String, + pub profile_id: String, + pub status: String, + pub snapshot_version: u32, + pub started_at_ms: i64, + pub duration_limit_ms: i64, + pub server_now_ms: i64, + pub remaining_ms: i64, + pub clear_count: u32, + pub total_item_count: u32, + pub cleared_item_count: u32, + pub tray_slots: Vec, + pub items: Vec, + pub failure_reason: Option, +} + +impl __sdk::InModule for Match3DRunSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_tray_slot_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_tray_slot_snapshot_type.rs new file mode 100644 index 00000000..823cff0c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_tray_slot_snapshot_type.rs @@ -0,0 +1,18 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DTraySlotSnapshot { + pub slot_index: u32, + pub item_instance_id: Option, + pub item_type_id: Option, + pub visual_key: Option, +} + +impl __sdk::InModule for Match3DTraySlotSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs index 9cb5d518..d4d589f1 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::match_3_d_work_snapshot_type::Match3DWorkSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct Match3DWorkProcedureResult { pub ok: bool, - pub work_json: Option, + pub work: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_snapshot_type.rs new file mode 100644 index 00000000..fc1a862f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_work_snapshot_type.rs @@ -0,0 +1,35 @@ +// 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::match_3_d_creator_config_snapshot_type::Match3DCreatorConfigSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct Match3DWorkSnapshot { + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub game_name: String, + pub theme_text: String, + pub summary_text: String, + pub tags: Vec, + pub cover_image_src: String, + pub cover_asset_id: String, + pub clear_count: u32, + pub difficulty: u32, + pub config: Match3DCreatorConfigSnapshot, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub generated_item_assets_json: Option, +} + +impl __sdk::InModule for Match3DWorkSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs index f1cfd0be..0bc07ad4 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/match_3_d_works_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::match_3_d_work_snapshot_type::Match3DWorkSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct Match3DWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs index 8e38203c..984ccd36 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -95,6 +95,7 @@ pub mod auth_store_snapshot_type; pub mod auth_store_snapshot_upsert_input_type; pub mod authorize_database_migration_operator_procedure; pub mod bark_battle_draft_config_row_type; +pub mod bark_battle_draft_config_snapshot_type; pub mod bark_battle_draft_config_table; pub mod bark_battle_draft_config_upsert_input_type; pub mod bark_battle_draft_create_input_type; @@ -107,8 +108,10 @@ pub mod bark_battle_published_config_row_type; pub mod bark_battle_published_config_table; pub mod bark_battle_run_finish_input_type; pub mod bark_battle_run_get_input_type; +pub mod bark_battle_run_snapshot_type; pub mod bark_battle_run_start_input_type; pub mod bark_battle_runtime_config_get_input_type; +pub mod bark_battle_runtime_config_snapshot_type; pub mod bark_battle_runtime_run_row_type; pub mod bark_battle_runtime_run_table; pub mod bark_battle_score_record_row_type; @@ -149,6 +152,7 @@ pub mod big_fish_draft_compile_input_type; pub mod big_fish_event_kind_type; pub mod big_fish_event_table; pub mod big_fish_event_type; +pub mod big_fish_gallery_view_table; pub mod big_fish_game_draft_type; pub mod big_fish_input_submit_input_type; pub mod big_fish_level_blueprint_type; @@ -160,16 +164,20 @@ pub mod big_fish_run_get_input_type; pub mod big_fish_run_procedure_result_type; pub mod big_fish_run_start_input_type; pub mod big_fish_run_status_type; +pub mod big_fish_runtime_entity_snapshot_type; pub mod big_fish_runtime_params_type; pub mod big_fish_runtime_run_table; pub mod big_fish_runtime_run_type; +pub mod big_fish_runtime_snapshot_type; pub mod big_fish_session_create_input_type; pub mod big_fish_session_get_input_type; pub mod big_fish_session_procedure_result_type; pub mod big_fish_session_snapshot_type; +pub mod big_fish_vector_2_type; pub mod big_fish_work_delete_input_type; pub mod big_fish_work_like_record_input_type; pub mod big_fish_work_remix_input_type; +pub mod big_fish_work_summary_snapshot_type; pub mod big_fish_works_list_input_type; pub mod big_fish_works_procedure_result_type; pub mod bind_asset_object_to_entity_and_return_procedure; @@ -402,30 +410,40 @@ pub mod list_visual_novel_works_procedure; pub mod mark_profile_recharge_order_paid_and_return_procedure; pub mod match_3_d_agent_message_finalize_input_type; pub mod match_3_d_agent_message_row_type; +pub mod match_3_d_agent_message_snapshot_type; pub mod match_3_d_agent_message_submit_input_type; pub mod match_3_d_agent_message_table; pub mod match_3_d_agent_session_create_input_type; pub mod match_3_d_agent_session_get_input_type; pub mod match_3_d_agent_session_procedure_result_type; pub mod match_3_d_agent_session_row_type; +pub mod match_3_d_agent_session_snapshot_type; pub mod match_3_d_agent_session_table; pub mod match_3_d_click_item_procedure_result_type; +pub mod match_3_d_creator_config_snapshot_type; pub mod match_3_d_draft_compile_input_type; +pub mod match_3_d_draft_snapshot_type; +pub mod match_3_d_gallery_view_row_type; +pub mod match_3_d_gallery_view_table; +pub mod match_3_d_item_snapshot_type; pub mod match_3_d_run_click_input_type; pub mod match_3_d_run_get_input_type; pub mod match_3_d_run_procedure_result_type; pub mod match_3_d_run_restart_input_type; +pub mod match_3_d_run_snapshot_type; pub mod match_3_d_run_start_input_type; pub mod match_3_d_run_stop_input_type; pub mod match_3_d_run_time_up_input_type; pub mod match_3_d_runtime_run_row_type; pub mod match_3_d_runtime_run_table; +pub mod match_3_d_tray_slot_snapshot_type; pub mod match_3_d_work_delete_input_type; pub mod match_3_d_work_get_input_type; pub mod match_3_d_work_procedure_result_type; pub mod match_3_d_work_profile_row_type; pub mod match_3_d_work_profile_table; pub mod match_3_d_work_publish_input_type; +pub mod match_3_d_work_snapshot_type; pub mod match_3_d_work_update_input_type; pub mod match_3_d_works_list_input_type; pub mod match_3_d_works_procedure_result_type; @@ -499,34 +517,60 @@ pub mod puzzle_agent_message_finalize_input_type; pub mod puzzle_agent_message_kind_type; pub mod puzzle_agent_message_role_type; pub mod puzzle_agent_message_row_type; +pub mod puzzle_agent_message_snapshot_type; pub mod puzzle_agent_message_submit_input_type; pub mod puzzle_agent_message_table; pub mod puzzle_agent_session_create_input_type; pub mod puzzle_agent_session_get_input_type; pub mod puzzle_agent_session_procedure_result_type; pub mod puzzle_agent_session_row_type; +pub mod puzzle_agent_session_snapshot_type; pub mod puzzle_agent_session_table; pub mod puzzle_agent_stage_type; +pub mod puzzle_agent_suggested_action_type; +pub mod puzzle_anchor_item_type; +pub mod puzzle_anchor_pack_type; +pub mod puzzle_anchor_status_type; +pub mod puzzle_audio_asset_type; +pub mod puzzle_board_snapshot_type; +pub mod puzzle_cell_position_type; +pub mod puzzle_creator_intent_type; pub mod puzzle_draft_compile_input_type; +pub mod puzzle_draft_level_type; pub mod puzzle_event_kind_type; pub mod puzzle_event_table; pub mod puzzle_event_type; pub mod puzzle_form_draft_save_input_type; +pub mod puzzle_form_draft_type; +pub mod puzzle_gallery_card_view_row_type; +pub mod puzzle_gallery_card_view_table; pub mod puzzle_gallery_view_table; +pub mod puzzle_generated_image_candidate_type; pub mod puzzle_generated_images_save_input_type; pub mod puzzle_leaderboard_entry_row_type; pub mod puzzle_leaderboard_entry_table; +pub mod puzzle_leaderboard_entry_type; pub mod puzzle_leaderboard_submit_input_type; +pub mod puzzle_merged_group_state_type; +pub mod puzzle_piece_state_type; pub mod puzzle_publication_status_type; pub mod puzzle_publish_input_type; +pub mod puzzle_recommended_next_work_type; +pub mod puzzle_result_draft_type; +pub mod puzzle_result_preview_blocker_type; +pub mod puzzle_result_preview_envelope_type; +pub mod puzzle_result_preview_finding_type; pub mod puzzle_run_drag_input_type; pub mod puzzle_run_get_input_type; pub mod puzzle_run_next_level_input_type; pub mod puzzle_run_pause_input_type; pub mod puzzle_run_procedure_result_type; pub mod puzzle_run_prop_input_type; +pub mod puzzle_run_snapshot_type; pub mod puzzle_run_start_input_type; pub mod puzzle_run_swap_input_type; +pub mod puzzle_runtime_level_snapshot_type; +pub mod puzzle_runtime_level_status_type; pub mod puzzle_runtime_run_row_type; pub mod puzzle_runtime_run_table; pub mod puzzle_select_cover_image_input_type; @@ -538,6 +582,7 @@ pub mod puzzle_work_point_incentive_claim_input_type; pub mod puzzle_work_procedure_result_type; pub mod puzzle_work_profile_row_type; pub mod puzzle_work_profile_table; +pub mod puzzle_work_profile_type; pub mod puzzle_work_remix_input_type; pub mod puzzle_work_upsert_input_type; pub mod puzzle_works_list_input_type; @@ -729,30 +774,43 @@ pub mod seed_analytics_date_dimensions_reducer; pub mod select_puzzle_cover_image_procedure; pub mod square_hole_agent_message_finalize_input_type; pub mod square_hole_agent_message_row_type; +pub mod square_hole_agent_message_snapshot_type; pub mod square_hole_agent_message_submit_input_type; pub mod square_hole_agent_message_table; pub mod square_hole_agent_session_create_input_type; pub mod square_hole_agent_session_get_input_type; pub mod square_hole_agent_session_procedure_result_type; pub mod square_hole_agent_session_row_type; +pub mod square_hole_agent_session_snapshot_type; pub mod square_hole_agent_session_table; +pub mod square_hole_creator_config_snapshot_type; pub mod square_hole_draft_compile_input_type; +pub mod square_hole_draft_snapshot_type; +pub mod square_hole_drop_feedback_snapshot_type; pub mod square_hole_drop_shape_procedure_result_type; +pub mod square_hole_gallery_view_row_type; +pub mod square_hole_gallery_view_table; +pub mod square_hole_hole_option_snapshot_type; +pub mod square_hole_hole_snapshot_type; pub mod square_hole_run_drop_input_type; pub mod square_hole_run_get_input_type; pub mod square_hole_run_procedure_result_type; pub mod square_hole_run_restart_input_type; +pub mod square_hole_run_snapshot_type; pub mod square_hole_run_start_input_type; pub mod square_hole_run_stop_input_type; pub mod square_hole_run_time_up_input_type; pub mod square_hole_runtime_run_row_type; pub mod square_hole_runtime_run_table; +pub mod square_hole_shape_option_snapshot_type; +pub mod square_hole_shape_snapshot_type; pub mod square_hole_work_delete_input_type; pub mod square_hole_work_get_input_type; pub mod square_hole_work_procedure_result_type; pub mod square_hole_work_profile_row_type; pub mod square_hole_work_profile_table; pub mod square_hole_work_publish_input_type; +pub mod square_hole_work_snapshot_type; pub mod square_hole_work_update_input_type; pub mod square_hole_works_list_input_type; pub mod square_hole_works_procedure_result_type; @@ -829,24 +887,33 @@ pub mod user_browse_history_table; pub mod user_browse_history_type; pub mod visual_novel_agent_message_finalize_input_type; pub mod visual_novel_agent_message_row_type; +pub mod visual_novel_agent_message_snapshot_type; pub mod visual_novel_agent_message_submit_input_type; pub mod visual_novel_agent_message_table; pub mod visual_novel_agent_session_create_input_type; pub mod visual_novel_agent_session_get_input_type; pub mod visual_novel_agent_session_procedure_result_type; pub mod visual_novel_agent_session_row_type; +pub mod visual_novel_agent_session_snapshot_type; pub mod visual_novel_agent_session_table; +pub mod visual_novel_gallery_view_row_type; +pub mod visual_novel_gallery_view_table; pub mod visual_novel_history_procedure_result_type; +pub mod visual_novel_json_field_type; +pub mod visual_novel_json_value_type; pub mod visual_novel_run_get_input_type; pub mod visual_novel_run_procedure_result_type; +pub mod visual_novel_run_snapshot_type; pub mod visual_novel_run_snapshot_upsert_input_type; pub mod visual_novel_run_start_input_type; pub mod visual_novel_runtime_event_procedure_result_type; pub mod visual_novel_runtime_event_record_input_type; +pub mod visual_novel_runtime_event_snapshot_type; pub mod visual_novel_runtime_event_table; pub mod visual_novel_runtime_event_type; pub mod visual_novel_runtime_history_append_input_type; pub mod visual_novel_runtime_history_entry_row_type; +pub mod visual_novel_runtime_history_entry_snapshot_type; pub mod visual_novel_runtime_history_entry_table; pub mod visual_novel_runtime_history_list_input_type; pub mod visual_novel_runtime_run_row_type; @@ -858,6 +925,7 @@ pub mod visual_novel_work_procedure_result_type; pub mod visual_novel_work_profile_row_type; pub mod visual_novel_work_profile_table; pub mod visual_novel_work_publish_input_type; +pub mod visual_novel_work_snapshot_type; pub mod visual_novel_work_update_input_type; pub mod visual_novel_works_list_input_type; pub mod visual_novel_works_procedure_result_type; @@ -951,6 +1019,7 @@ pub use auth_store_snapshot_type::AuthStoreSnapshot; pub use auth_store_snapshot_upsert_input_type::AuthStoreSnapshotUpsertInput; pub use authorize_database_migration_operator_procedure::authorize_database_migration_operator; pub use bark_battle_draft_config_row_type::BarkBattleDraftConfigRow; +pub use bark_battle_draft_config_snapshot_type::BarkBattleDraftConfigSnapshot; pub use bark_battle_draft_config_table::*; pub use bark_battle_draft_config_upsert_input_type::BarkBattleDraftConfigUpsertInput; pub use bark_battle_draft_create_input_type::BarkBattleDraftCreateInput; @@ -963,8 +1032,10 @@ pub use bark_battle_published_config_row_type::BarkBattlePublishedConfigRow; pub use bark_battle_published_config_table::*; pub use bark_battle_run_finish_input_type::BarkBattleRunFinishInput; pub use bark_battle_run_get_input_type::BarkBattleRunGetInput; +pub use bark_battle_run_snapshot_type::BarkBattleRunSnapshot; pub use bark_battle_run_start_input_type::BarkBattleRunStartInput; pub use bark_battle_runtime_config_get_input_type::BarkBattleRuntimeConfigGetInput; +pub use bark_battle_runtime_config_snapshot_type::BarkBattleRuntimeConfigSnapshot; pub use bark_battle_runtime_run_row_type::BarkBattleRuntimeRunRow; pub use bark_battle_runtime_run_table::*; pub use bark_battle_score_record_row_type::BarkBattleScoreRecordRow; @@ -1005,6 +1076,7 @@ pub use big_fish_draft_compile_input_type::BigFishDraftCompileInput; pub use big_fish_event_kind_type::BigFishEventKind; pub use big_fish_event_table::*; pub use big_fish_event_type::BigFishEvent; +pub use big_fish_gallery_view_table::*; pub use big_fish_game_draft_type::BigFishGameDraft; pub use big_fish_input_submit_input_type::BigFishInputSubmitInput; pub use big_fish_level_blueprint_type::BigFishLevelBlueprint; @@ -1016,16 +1088,20 @@ pub use big_fish_run_get_input_type::BigFishRunGetInput; pub use big_fish_run_procedure_result_type::BigFishRunProcedureResult; pub use big_fish_run_start_input_type::BigFishRunStartInput; pub use big_fish_run_status_type::BigFishRunStatus; +pub use big_fish_runtime_entity_snapshot_type::BigFishRuntimeEntitySnapshot; pub use big_fish_runtime_params_type::BigFishRuntimeParams; pub use big_fish_runtime_run_table::*; pub use big_fish_runtime_run_type::BigFishRuntimeRun; +pub use big_fish_runtime_snapshot_type::BigFishRuntimeSnapshot; pub use big_fish_session_create_input_type::BigFishSessionCreateInput; pub use big_fish_session_get_input_type::BigFishSessionGetInput; pub use big_fish_session_procedure_result_type::BigFishSessionProcedureResult; pub use big_fish_session_snapshot_type::BigFishSessionSnapshot; +pub use big_fish_vector_2_type::BigFishVector2; pub use big_fish_work_delete_input_type::BigFishWorkDeleteInput; pub use big_fish_work_like_record_input_type::BigFishWorkLikeRecordInput; pub use big_fish_work_remix_input_type::BigFishWorkRemixInput; +pub use big_fish_work_summary_snapshot_type::BigFishWorkSummarySnapshot; pub use big_fish_works_list_input_type::BigFishWorksListInput; pub use big_fish_works_procedure_result_type::BigFishWorksProcedureResult; pub use bind_asset_object_to_entity_and_return_procedure::bind_asset_object_to_entity_and_return; @@ -1258,30 +1334,40 @@ pub use list_visual_novel_works_procedure::list_visual_novel_works; pub use mark_profile_recharge_order_paid_and_return_procedure::mark_profile_recharge_order_paid_and_return; pub use match_3_d_agent_message_finalize_input_type::Match3DAgentMessageFinalizeInput; pub use match_3_d_agent_message_row_type::Match3DAgentMessageRow; +pub use match_3_d_agent_message_snapshot_type::Match3DAgentMessageSnapshot; pub use match_3_d_agent_message_submit_input_type::Match3DAgentMessageSubmitInput; pub use match_3_d_agent_message_table::*; pub use match_3_d_agent_session_create_input_type::Match3DAgentSessionCreateInput; pub use match_3_d_agent_session_get_input_type::Match3DAgentSessionGetInput; pub use match_3_d_agent_session_procedure_result_type::Match3DAgentSessionProcedureResult; pub use match_3_d_agent_session_row_type::Match3DAgentSessionRow; +pub use match_3_d_agent_session_snapshot_type::Match3DAgentSessionSnapshot; pub use match_3_d_agent_session_table::*; pub use match_3_d_click_item_procedure_result_type::Match3DClickItemProcedureResult; +pub use match_3_d_creator_config_snapshot_type::Match3DCreatorConfigSnapshot; pub use match_3_d_draft_compile_input_type::Match3DDraftCompileInput; +pub use match_3_d_draft_snapshot_type::Match3DDraftSnapshot; +pub use match_3_d_gallery_view_row_type::Match3DGalleryViewRow; +pub use match_3_d_gallery_view_table::*; +pub use match_3_d_item_snapshot_type::Match3DItemSnapshot; pub use match_3_d_run_click_input_type::Match3DRunClickInput; pub use match_3_d_run_get_input_type::Match3DRunGetInput; pub use match_3_d_run_procedure_result_type::Match3DRunProcedureResult; pub use match_3_d_run_restart_input_type::Match3DRunRestartInput; +pub use match_3_d_run_snapshot_type::Match3DRunSnapshot; pub use match_3_d_run_start_input_type::Match3DRunStartInput; pub use match_3_d_run_stop_input_type::Match3DRunStopInput; pub use match_3_d_run_time_up_input_type::Match3DRunTimeUpInput; pub use match_3_d_runtime_run_row_type::Match3DRuntimeRunRow; pub use match_3_d_runtime_run_table::*; +pub use match_3_d_tray_slot_snapshot_type::Match3DTraySlotSnapshot; pub use match_3_d_work_delete_input_type::Match3DWorkDeleteInput; pub use match_3_d_work_get_input_type::Match3DWorkGetInput; pub use match_3_d_work_procedure_result_type::Match3DWorkProcedureResult; pub use match_3_d_work_profile_row_type::Match3DWorkProfileRow; pub use match_3_d_work_profile_table::*; pub use match_3_d_work_publish_input_type::Match3DWorkPublishInput; +pub use match_3_d_work_snapshot_type::Match3DWorkSnapshot; pub use match_3_d_work_update_input_type::Match3DWorkUpdateInput; pub use match_3_d_works_list_input_type::Match3DWorksListInput; pub use match_3_d_works_procedure_result_type::Match3DWorksProcedureResult; @@ -1355,34 +1441,60 @@ pub use puzzle_agent_message_finalize_input_type::PuzzleAgentMessageFinalizeInpu pub use puzzle_agent_message_kind_type::PuzzleAgentMessageKind; pub use puzzle_agent_message_role_type::PuzzleAgentMessageRole; pub use puzzle_agent_message_row_type::PuzzleAgentMessageRow; +pub use puzzle_agent_message_snapshot_type::PuzzleAgentMessageSnapshot; pub use puzzle_agent_message_submit_input_type::PuzzleAgentMessageSubmitInput; pub use puzzle_agent_message_table::*; pub use puzzle_agent_session_create_input_type::PuzzleAgentSessionCreateInput; pub use puzzle_agent_session_get_input_type::PuzzleAgentSessionGetInput; pub use puzzle_agent_session_procedure_result_type::PuzzleAgentSessionProcedureResult; pub use puzzle_agent_session_row_type::PuzzleAgentSessionRow; +pub use puzzle_agent_session_snapshot_type::PuzzleAgentSessionSnapshot; pub use puzzle_agent_session_table::*; pub use puzzle_agent_stage_type::PuzzleAgentStage; +pub use puzzle_agent_suggested_action_type::PuzzleAgentSuggestedAction; +pub use puzzle_anchor_item_type::PuzzleAnchorItem; +pub use puzzle_anchor_pack_type::PuzzleAnchorPack; +pub use puzzle_anchor_status_type::PuzzleAnchorStatus; +pub use puzzle_audio_asset_type::PuzzleAudioAsset; +pub use puzzle_board_snapshot_type::PuzzleBoardSnapshot; +pub use puzzle_cell_position_type::PuzzleCellPosition; +pub use puzzle_creator_intent_type::PuzzleCreatorIntent; pub use puzzle_draft_compile_input_type::PuzzleDraftCompileInput; +pub use puzzle_draft_level_type::PuzzleDraftLevel; pub use puzzle_event_kind_type::PuzzleEventKind; pub use puzzle_event_table::*; pub use puzzle_event_type::PuzzleEvent; pub use puzzle_form_draft_save_input_type::PuzzleFormDraftSaveInput; +pub use puzzle_form_draft_type::PuzzleFormDraft; +pub use puzzle_gallery_card_view_row_type::PuzzleGalleryCardViewRow; +pub use puzzle_gallery_card_view_table::*; pub use puzzle_gallery_view_table::*; +pub use puzzle_generated_image_candidate_type::PuzzleGeneratedImageCandidate; pub use puzzle_generated_images_save_input_type::PuzzleGeneratedImagesSaveInput; pub use puzzle_leaderboard_entry_row_type::PuzzleLeaderboardEntryRow; pub use puzzle_leaderboard_entry_table::*; +pub use puzzle_leaderboard_entry_type::PuzzleLeaderboardEntry; pub use puzzle_leaderboard_submit_input_type::PuzzleLeaderboardSubmitInput; +pub use puzzle_merged_group_state_type::PuzzleMergedGroupState; +pub use puzzle_piece_state_type::PuzzlePieceState; pub use puzzle_publication_status_type::PuzzlePublicationStatus; pub use puzzle_publish_input_type::PuzzlePublishInput; +pub use puzzle_recommended_next_work_type::PuzzleRecommendedNextWork; +pub use puzzle_result_draft_type::PuzzleResultDraft; +pub use puzzle_result_preview_blocker_type::PuzzleResultPreviewBlocker; +pub use puzzle_result_preview_envelope_type::PuzzleResultPreviewEnvelope; +pub use puzzle_result_preview_finding_type::PuzzleResultPreviewFinding; pub use puzzle_run_drag_input_type::PuzzleRunDragInput; pub use puzzle_run_get_input_type::PuzzleRunGetInput; pub use puzzle_run_next_level_input_type::PuzzleRunNextLevelInput; pub use puzzle_run_pause_input_type::PuzzleRunPauseInput; pub use puzzle_run_procedure_result_type::PuzzleRunProcedureResult; pub use puzzle_run_prop_input_type::PuzzleRunPropInput; +pub use puzzle_run_snapshot_type::PuzzleRunSnapshot; pub use puzzle_run_start_input_type::PuzzleRunStartInput; pub use puzzle_run_swap_input_type::PuzzleRunSwapInput; +pub use puzzle_runtime_level_snapshot_type::PuzzleRuntimeLevelSnapshot; +pub use puzzle_runtime_level_status_type::PuzzleRuntimeLevelStatus; pub use puzzle_runtime_run_row_type::PuzzleRuntimeRunRow; pub use puzzle_runtime_run_table::*; pub use puzzle_select_cover_image_input_type::PuzzleSelectCoverImageInput; @@ -1394,6 +1506,7 @@ pub use puzzle_work_point_incentive_claim_input_type::PuzzleWorkPointIncentiveCl pub use puzzle_work_procedure_result_type::PuzzleWorkProcedureResult; pub use puzzle_work_profile_row_type::PuzzleWorkProfileRow; pub use puzzle_work_profile_table::*; +pub use puzzle_work_profile_type::PuzzleWorkProfile; pub use puzzle_work_remix_input_type::PuzzleWorkRemixInput; pub use puzzle_work_upsert_input_type::PuzzleWorkUpsertInput; pub use puzzle_works_list_input_type::PuzzleWorksListInput; @@ -1585,30 +1698,43 @@ pub use seed_analytics_date_dimensions_reducer::seed_analytics_date_dimensions; pub use select_puzzle_cover_image_procedure::select_puzzle_cover_image; pub use square_hole_agent_message_finalize_input_type::SquareHoleAgentMessageFinalizeInput; pub use square_hole_agent_message_row_type::SquareHoleAgentMessageRow; +pub use square_hole_agent_message_snapshot_type::SquareHoleAgentMessageSnapshot; pub use square_hole_agent_message_submit_input_type::SquareHoleAgentMessageSubmitInput; pub use square_hole_agent_message_table::*; pub use square_hole_agent_session_create_input_type::SquareHoleAgentSessionCreateInput; pub use square_hole_agent_session_get_input_type::SquareHoleAgentSessionGetInput; pub use square_hole_agent_session_procedure_result_type::SquareHoleAgentSessionProcedureResult; pub use square_hole_agent_session_row_type::SquareHoleAgentSessionRow; +pub use square_hole_agent_session_snapshot_type::SquareHoleAgentSessionSnapshot; pub use square_hole_agent_session_table::*; +pub use square_hole_creator_config_snapshot_type::SquareHoleCreatorConfigSnapshot; pub use square_hole_draft_compile_input_type::SquareHoleDraftCompileInput; +pub use square_hole_draft_snapshot_type::SquareHoleDraftSnapshot; +pub use square_hole_drop_feedback_snapshot_type::SquareHoleDropFeedbackSnapshot; pub use square_hole_drop_shape_procedure_result_type::SquareHoleDropShapeProcedureResult; +pub use square_hole_gallery_view_row_type::SquareHoleGalleryViewRow; +pub use square_hole_gallery_view_table::*; +pub use square_hole_hole_option_snapshot_type::SquareHoleHoleOptionSnapshot; +pub use square_hole_hole_snapshot_type::SquareHoleHoleSnapshot; pub use square_hole_run_drop_input_type::SquareHoleRunDropInput; pub use square_hole_run_get_input_type::SquareHoleRunGetInput; pub use square_hole_run_procedure_result_type::SquareHoleRunProcedureResult; pub use square_hole_run_restart_input_type::SquareHoleRunRestartInput; +pub use square_hole_run_snapshot_type::SquareHoleRunSnapshot; pub use square_hole_run_start_input_type::SquareHoleRunStartInput; pub use square_hole_run_stop_input_type::SquareHoleRunStopInput; pub use square_hole_run_time_up_input_type::SquareHoleRunTimeUpInput; pub use square_hole_runtime_run_row_type::SquareHoleRuntimeRunRow; pub use square_hole_runtime_run_table::*; +pub use square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; +pub use square_hole_shape_snapshot_type::SquareHoleShapeSnapshot; pub use square_hole_work_delete_input_type::SquareHoleWorkDeleteInput; pub use square_hole_work_get_input_type::SquareHoleWorkGetInput; pub use square_hole_work_procedure_result_type::SquareHoleWorkProcedureResult; pub use square_hole_work_profile_row_type::SquareHoleWorkProfileRow; pub use square_hole_work_profile_table::*; pub use square_hole_work_publish_input_type::SquareHoleWorkPublishInput; +pub use square_hole_work_snapshot_type::SquareHoleWorkSnapshot; pub use square_hole_work_update_input_type::SquareHoleWorkUpdateInput; pub use square_hole_works_list_input_type::SquareHoleWorksListInput; pub use square_hole_works_procedure_result_type::SquareHoleWorksProcedureResult; @@ -1685,24 +1811,33 @@ pub use user_browse_history_table::*; pub use user_browse_history_type::UserBrowseHistory; pub use visual_novel_agent_message_finalize_input_type::VisualNovelAgentMessageFinalizeInput; pub use visual_novel_agent_message_row_type::VisualNovelAgentMessageRow; +pub use visual_novel_agent_message_snapshot_type::VisualNovelAgentMessageSnapshot; pub use visual_novel_agent_message_submit_input_type::VisualNovelAgentMessageSubmitInput; pub use visual_novel_agent_message_table::*; pub use visual_novel_agent_session_create_input_type::VisualNovelAgentSessionCreateInput; pub use visual_novel_agent_session_get_input_type::VisualNovelAgentSessionGetInput; pub use visual_novel_agent_session_procedure_result_type::VisualNovelAgentSessionProcedureResult; pub use visual_novel_agent_session_row_type::VisualNovelAgentSessionRow; +pub use visual_novel_agent_session_snapshot_type::VisualNovelAgentSessionSnapshot; pub use visual_novel_agent_session_table::*; +pub use visual_novel_gallery_view_row_type::VisualNovelGalleryViewRow; +pub use visual_novel_gallery_view_table::*; pub use visual_novel_history_procedure_result_type::VisualNovelHistoryProcedureResult; +pub use visual_novel_json_field_type::VisualNovelJsonField; +pub use visual_novel_json_value_type::VisualNovelJsonValue; pub use visual_novel_run_get_input_type::VisualNovelRunGetInput; pub use visual_novel_run_procedure_result_type::VisualNovelRunProcedureResult; +pub use visual_novel_run_snapshot_type::VisualNovelRunSnapshot; pub use visual_novel_run_snapshot_upsert_input_type::VisualNovelRunSnapshotUpsertInput; pub use visual_novel_run_start_input_type::VisualNovelRunStartInput; pub use visual_novel_runtime_event_procedure_result_type::VisualNovelRuntimeEventProcedureResult; pub use visual_novel_runtime_event_record_input_type::VisualNovelRuntimeEventRecordInput; +pub use visual_novel_runtime_event_snapshot_type::VisualNovelRuntimeEventSnapshot; pub use visual_novel_runtime_event_table::*; pub use visual_novel_runtime_event_type::VisualNovelRuntimeEvent; pub use visual_novel_runtime_history_append_input_type::VisualNovelRuntimeHistoryAppendInput; pub use visual_novel_runtime_history_entry_row_type::VisualNovelRuntimeHistoryEntryRow; +pub use visual_novel_runtime_history_entry_snapshot_type::VisualNovelRuntimeHistoryEntrySnapshot; pub use visual_novel_runtime_history_entry_table::*; pub use visual_novel_runtime_history_list_input_type::VisualNovelRuntimeHistoryListInput; pub use visual_novel_runtime_run_row_type::VisualNovelRuntimeRunRow; @@ -1714,6 +1849,7 @@ pub use visual_novel_work_procedure_result_type::VisualNovelWorkProcedureResult; pub use visual_novel_work_profile_row_type::VisualNovelWorkProfileRow; pub use visual_novel_work_profile_table::*; pub use visual_novel_work_publish_input_type::VisualNovelWorkPublishInput; +pub use visual_novel_work_snapshot_type::VisualNovelWorkSnapshot; pub use visual_novel_work_update_input_type::VisualNovelWorkUpdateInput; pub use visual_novel_works_list_input_type::VisualNovelWorksListInput; pub use visual_novel_works_procedure_result_type::VisualNovelWorksProcedureResult; @@ -2014,6 +2150,7 @@ pub struct DbUpdate { big_fish_asset_slot: __sdk::TableUpdate, big_fish_creation_session: __sdk::TableUpdate, big_fish_event: __sdk::TableUpdate, + big_fish_gallery_view: __sdk::TableUpdate, big_fish_runtime_run: __sdk::TableUpdate, chapter_progression: __sdk::TableUpdate, creation_entry_config: __sdk::TableUpdate, @@ -2030,6 +2167,7 @@ pub struct DbUpdate { inventory_slot: __sdk::TableUpdate, match_3_d_agent_message: __sdk::TableUpdate, match_3_d_agent_session: __sdk::TableUpdate, + match_3_d_gallery_view: __sdk::TableUpdate, match_3_d_runtime_run: __sdk::TableUpdate, match_3_d_work_profile: __sdk::TableUpdate, npc_state: __sdk::TableUpdate, @@ -2054,7 +2192,8 @@ pub struct DbUpdate { puzzle_agent_message: __sdk::TableUpdate, puzzle_agent_session: __sdk::TableUpdate, puzzle_event: __sdk::TableUpdate, - puzzle_gallery_view: __sdk::TableUpdate, + puzzle_gallery_card_view: __sdk::TableUpdate, + puzzle_gallery_view: __sdk::TableUpdate, puzzle_leaderboard_entry: __sdk::TableUpdate, puzzle_runtime_run: __sdk::TableUpdate, puzzle_work_profile: __sdk::TableUpdate, @@ -2065,6 +2204,7 @@ pub struct DbUpdate { runtime_snapshot: __sdk::TableUpdate, square_hole_agent_message: __sdk::TableUpdate, square_hole_agent_session: __sdk::TableUpdate, + square_hole_gallery_view: __sdk::TableUpdate, square_hole_runtime_run: __sdk::TableUpdate, square_hole_work_profile: __sdk::TableUpdate, story_event: __sdk::TableUpdate, @@ -2076,6 +2216,7 @@ pub struct DbUpdate { user_browse_history: __sdk::TableUpdate, visual_novel_agent_message: __sdk::TableUpdate, visual_novel_agent_session: __sdk::TableUpdate, + visual_novel_gallery_view: __sdk::TableUpdate, visual_novel_runtime_event: __sdk::TableUpdate, visual_novel_runtime_history_entry: __sdk::TableUpdate, visual_novel_runtime_run: __sdk::TableUpdate, @@ -2166,6 +2307,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "big_fish_event" => db_update .big_fish_event .append(big_fish_event_table::parse_table_update(table_update)?), + "big_fish_gallery_view" => db_update.big_fish_gallery_view.append( + big_fish_gallery_view_table::parse_table_update(table_update)?, + ), "big_fish_runtime_run" => db_update.big_fish_runtime_run.append( big_fish_runtime_run_table::parse_table_update(table_update)?, ), @@ -2216,6 +2360,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "match_3_d_agent_session" => db_update.match_3_d_agent_session.append( match_3_d_agent_session_table::parse_table_update(table_update)?, ), + "match_3_d_gallery_view" => db_update.match_3_d_gallery_view.append( + match_3_d_gallery_view_table::parse_table_update(table_update)?, + ), "match_3_d_runtime_run" => db_update.match_3_d_runtime_run.append( match_3_d_runtime_run_table::parse_table_update(table_update)?, ), @@ -2290,6 +2437,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "puzzle_event" => db_update .puzzle_event .append(puzzle_event_table::parse_table_update(table_update)?), + "puzzle_gallery_card_view" => db_update.puzzle_gallery_card_view.append( + puzzle_gallery_card_view_table::parse_table_update(table_update)?, + ), "puzzle_gallery_view" => db_update .puzzle_gallery_view .append(puzzle_gallery_view_table::parse_table_update(table_update)?), @@ -2323,6 +2473,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "square_hole_agent_session" => db_update.square_hole_agent_session.append( square_hole_agent_session_table::parse_table_update(table_update)?, ), + "square_hole_gallery_view" => db_update.square_hole_gallery_view.append( + square_hole_gallery_view_table::parse_table_update(table_update)?, + ), "square_hole_runtime_run" => db_update.square_hole_runtime_run.append( square_hole_runtime_run_table::parse_table_update(table_update)?, ), @@ -2356,6 +2509,9 @@ impl TryFrom<__ws::v2::TransactionUpdate> for DbUpdate { "visual_novel_agent_session" => db_update.visual_novel_agent_session.append( visual_novel_agent_session_table::parse_table_update(table_update)?, ), + "visual_novel_gallery_view" => db_update.visual_novel_gallery_view.append( + visual_novel_gallery_view_table::parse_table_update(table_update)?, + ), "visual_novel_runtime_event" => db_update.visual_novel_runtime_event.append( visual_novel_runtime_event_table::parse_table_update(table_update)?, ), @@ -2848,10 +3004,30 @@ impl __sdk::DbUpdate for DbUpdate { &self.visual_novel_work_profile, ) .with_updates_by_pk(|row| &row.profile_id); - diff.puzzle_gallery_view = cache.apply_diff_to_table::( + diff.big_fish_gallery_view = cache.apply_diff_to_table::( + "big_fish_gallery_view", + &self.big_fish_gallery_view, + ); + diff.match_3_d_gallery_view = cache.apply_diff_to_table::( + "match_3_d_gallery_view", + &self.match_3_d_gallery_view, + ); + diff.puzzle_gallery_card_view = cache.apply_diff_to_table::( + "puzzle_gallery_card_view", + &self.puzzle_gallery_card_view, + ); + diff.puzzle_gallery_view = cache.apply_diff_to_table::( "puzzle_gallery_view", &self.puzzle_gallery_view, ); + diff.square_hole_gallery_view = cache.apply_diff_to_table::( + "square_hole_gallery_view", + &self.square_hole_gallery_view, + ); + diff.visual_novel_gallery_view = cache.apply_diff_to_table::( + "visual_novel_gallery_view", + &self.visual_novel_gallery_view, + ); diff } @@ -2931,6 +3107,9 @@ impl __sdk::DbUpdate for DbUpdate { "big_fish_event" => db_update .big_fish_event .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "big_fish_gallery_view" => db_update + .big_fish_gallery_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "big_fish_runtime_run" => db_update .big_fish_runtime_run .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -2979,6 +3158,9 @@ impl __sdk::DbUpdate for DbUpdate { "match_3_d_agent_session" => db_update .match_3_d_agent_session .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "match_3_d_gallery_view" => db_update + .match_3_d_gallery_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "match_3_d_runtime_run" => db_update .match_3_d_runtime_run .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3051,6 +3233,9 @@ impl __sdk::DbUpdate for DbUpdate { "puzzle_event" => db_update .puzzle_event .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "puzzle_gallery_card_view" => db_update + .puzzle_gallery_card_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "puzzle_gallery_view" => db_update .puzzle_gallery_view .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3084,6 +3269,9 @@ impl __sdk::DbUpdate for DbUpdate { "square_hole_agent_session" => db_update .square_hole_agent_session .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "square_hole_gallery_view" => db_update + .square_hole_gallery_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "square_hole_runtime_run" => db_update .square_hole_runtime_run .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3117,6 +3305,9 @@ impl __sdk::DbUpdate for DbUpdate { "visual_novel_agent_session" => db_update .visual_novel_agent_session .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), + "visual_novel_gallery_view" => db_update + .visual_novel_gallery_view + .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), "visual_novel_runtime_event" => db_update .visual_novel_runtime_event .append(__sdk::parse_row_list_as_inserts(table_rows.rows)?), @@ -3214,6 +3405,9 @@ impl __sdk::DbUpdate for DbUpdate { "big_fish_event" => db_update .big_fish_event .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "big_fish_gallery_view" => db_update + .big_fish_gallery_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "big_fish_runtime_run" => db_update .big_fish_runtime_run .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3262,6 +3456,9 @@ impl __sdk::DbUpdate for DbUpdate { "match_3_d_agent_session" => db_update .match_3_d_agent_session .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "match_3_d_gallery_view" => db_update + .match_3_d_gallery_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "match_3_d_runtime_run" => db_update .match_3_d_runtime_run .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3334,6 +3531,9 @@ impl __sdk::DbUpdate for DbUpdate { "puzzle_event" => db_update .puzzle_event .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "puzzle_gallery_card_view" => db_update + .puzzle_gallery_card_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "puzzle_gallery_view" => db_update .puzzle_gallery_view .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3367,6 +3567,9 @@ impl __sdk::DbUpdate for DbUpdate { "square_hole_agent_session" => db_update .square_hole_agent_session .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "square_hole_gallery_view" => db_update + .square_hole_gallery_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "square_hole_runtime_run" => db_update .square_hole_runtime_run .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3400,6 +3603,9 @@ impl __sdk::DbUpdate for DbUpdate { "visual_novel_agent_session" => db_update .visual_novel_agent_session .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), + "visual_novel_gallery_view" => db_update + .visual_novel_gallery_view + .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), "visual_novel_runtime_event" => db_update .visual_novel_runtime_event .append(__sdk::parse_row_list_as_deletes(table_rows.rows)?), @@ -3453,6 +3659,7 @@ pub struct AppliedDiff<'r> { big_fish_asset_slot: __sdk::TableAppliedDiff<'r, BigFishAssetSlot>, big_fish_creation_session: __sdk::TableAppliedDiff<'r, BigFishCreationSession>, big_fish_event: __sdk::TableAppliedDiff<'r, BigFishEvent>, + big_fish_gallery_view: __sdk::TableAppliedDiff<'r, BigFishWorkSummarySnapshot>, big_fish_runtime_run: __sdk::TableAppliedDiff<'r, BigFishRuntimeRun>, chapter_progression: __sdk::TableAppliedDiff<'r, ChapterProgression>, creation_entry_config: __sdk::TableAppliedDiff<'r, CreationEntryConfig>, @@ -3469,6 +3676,7 @@ pub struct AppliedDiff<'r> { inventory_slot: __sdk::TableAppliedDiff<'r, InventorySlot>, match_3_d_agent_message: __sdk::TableAppliedDiff<'r, Match3DAgentMessageRow>, match_3_d_agent_session: __sdk::TableAppliedDiff<'r, Match3DAgentSessionRow>, + match_3_d_gallery_view: __sdk::TableAppliedDiff<'r, Match3DGalleryViewRow>, match_3_d_runtime_run: __sdk::TableAppliedDiff<'r, Match3DRuntimeRunRow>, match_3_d_work_profile: __sdk::TableAppliedDiff<'r, Match3DWorkProfileRow>, npc_state: __sdk::TableAppliedDiff<'r, NpcState>, @@ -3493,7 +3701,8 @@ pub struct AppliedDiff<'r> { puzzle_agent_message: __sdk::TableAppliedDiff<'r, PuzzleAgentMessageRow>, puzzle_agent_session: __sdk::TableAppliedDiff<'r, PuzzleAgentSessionRow>, puzzle_event: __sdk::TableAppliedDiff<'r, PuzzleEvent>, - puzzle_gallery_view: __sdk::TableAppliedDiff<'r, PuzzleGalleryViewRow>, + puzzle_gallery_card_view: __sdk::TableAppliedDiff<'r, PuzzleGalleryCardViewRow>, + puzzle_gallery_view: __sdk::TableAppliedDiff<'r, PuzzleWorkProfile>, puzzle_leaderboard_entry: __sdk::TableAppliedDiff<'r, PuzzleLeaderboardEntryRow>, puzzle_runtime_run: __sdk::TableAppliedDiff<'r, PuzzleRuntimeRunRow>, puzzle_work_profile: __sdk::TableAppliedDiff<'r, PuzzleWorkProfileRow>, @@ -3504,6 +3713,7 @@ pub struct AppliedDiff<'r> { runtime_snapshot: __sdk::TableAppliedDiff<'r, RuntimeSnapshotRow>, square_hole_agent_message: __sdk::TableAppliedDiff<'r, SquareHoleAgentMessageRow>, square_hole_agent_session: __sdk::TableAppliedDiff<'r, SquareHoleAgentSessionRow>, + square_hole_gallery_view: __sdk::TableAppliedDiff<'r, SquareHoleGalleryViewRow>, square_hole_runtime_run: __sdk::TableAppliedDiff<'r, SquareHoleRuntimeRunRow>, square_hole_work_profile: __sdk::TableAppliedDiff<'r, SquareHoleWorkProfileRow>, story_event: __sdk::TableAppliedDiff<'r, StoryEvent>, @@ -3515,6 +3725,7 @@ pub struct AppliedDiff<'r> { user_browse_history: __sdk::TableAppliedDiff<'r, UserBrowseHistory>, visual_novel_agent_message: __sdk::TableAppliedDiff<'r, VisualNovelAgentMessageRow>, visual_novel_agent_session: __sdk::TableAppliedDiff<'r, VisualNovelAgentSessionRow>, + visual_novel_gallery_view: __sdk::TableAppliedDiff<'r, VisualNovelGalleryViewRow>, visual_novel_runtime_event: __sdk::TableAppliedDiff<'r, VisualNovelRuntimeEvent>, visual_novel_runtime_history_entry: __sdk::TableAppliedDiff<'r, VisualNovelRuntimeHistoryEntryRow>, @@ -3645,6 +3856,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.big_fish_event, event, ); + callbacks.invoke_table_row_callbacks::( + "big_fish_gallery_view", + &self.big_fish_gallery_view, + event, + ); callbacks.invoke_table_row_callbacks::( "big_fish_runtime_run", &self.big_fish_runtime_run, @@ -3725,6 +3941,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.match_3_d_agent_session, event, ); + callbacks.invoke_table_row_callbacks::( + "match_3_d_gallery_view", + &self.match_3_d_gallery_view, + event, + ); callbacks.invoke_table_row_callbacks::( "match_3_d_runtime_run", &self.match_3_d_runtime_run, @@ -3841,7 +4062,12 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.puzzle_event, event, ); - callbacks.invoke_table_row_callbacks::( + callbacks.invoke_table_row_callbacks::( + "puzzle_gallery_card_view", + &self.puzzle_gallery_card_view, + event, + ); + callbacks.invoke_table_row_callbacks::( "puzzle_gallery_view", &self.puzzle_gallery_view, event, @@ -3892,6 +4118,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.square_hole_agent_session, event, ); + callbacks.invoke_table_row_callbacks::( + "square_hole_gallery_view", + &self.square_hole_gallery_view, + event, + ); callbacks.invoke_table_row_callbacks::( "square_hole_runtime_run", &self.square_hole_runtime_run, @@ -3943,6 +4174,11 @@ impl<'r> __sdk::AppliedDiff<'r> for AppliedDiff<'r> { &self.visual_novel_agent_session, event, ); + callbacks.invoke_table_row_callbacks::( + "visual_novel_gallery_view", + &self.visual_novel_gallery_view, + event, + ); callbacks.invoke_table_row_callbacks::( "visual_novel_runtime_event", &self.visual_novel_runtime_event, @@ -4647,6 +4883,7 @@ impl __sdk::SpacetimeModule for RemoteModule { big_fish_asset_slot_table::register_table(client_cache); big_fish_creation_session_table::register_table(client_cache); big_fish_event_table::register_table(client_cache); + big_fish_gallery_view_table::register_table(client_cache); big_fish_runtime_run_table::register_table(client_cache); chapter_progression_table::register_table(client_cache); creation_entry_config_table::register_table(client_cache); @@ -4663,6 +4900,7 @@ impl __sdk::SpacetimeModule for RemoteModule { inventory_slot_table::register_table(client_cache); match_3_d_agent_message_table::register_table(client_cache); match_3_d_agent_session_table::register_table(client_cache); + match_3_d_gallery_view_table::register_table(client_cache); match_3_d_runtime_run_table::register_table(client_cache); match_3_d_work_profile_table::register_table(client_cache); npc_state_table::register_table(client_cache); @@ -4687,6 +4925,7 @@ impl __sdk::SpacetimeModule for RemoteModule { puzzle_agent_message_table::register_table(client_cache); puzzle_agent_session_table::register_table(client_cache); puzzle_event_table::register_table(client_cache); + puzzle_gallery_card_view_table::register_table(client_cache); puzzle_gallery_view_table::register_table(client_cache); puzzle_leaderboard_entry_table::register_table(client_cache); puzzle_runtime_run_table::register_table(client_cache); @@ -4698,6 +4937,7 @@ impl __sdk::SpacetimeModule for RemoteModule { runtime_snapshot_table::register_table(client_cache); square_hole_agent_message_table::register_table(client_cache); square_hole_agent_session_table::register_table(client_cache); + square_hole_gallery_view_table::register_table(client_cache); square_hole_runtime_run_table::register_table(client_cache); square_hole_work_profile_table::register_table(client_cache); story_event_table::register_table(client_cache); @@ -4709,6 +4949,7 @@ impl __sdk::SpacetimeModule for RemoteModule { user_browse_history_table::register_table(client_cache); visual_novel_agent_message_table::register_table(client_cache); visual_novel_agent_session_table::register_table(client_cache); + visual_novel_gallery_view_table::register_table(client_cache); visual_novel_runtime_event_table::register_table(client_cache); visual_novel_runtime_history_entry_table::register_table(client_cache); visual_novel_runtime_run_table::register_table(client_cache); @@ -4739,6 +4980,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "big_fish_asset_slot", "big_fish_creation_session", "big_fish_event", + "big_fish_gallery_view", "big_fish_runtime_run", "chapter_progression", "creation_entry_config", @@ -4755,6 +4997,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "inventory_slot", "match_3_d_agent_message", "match_3_d_agent_session", + "match_3_d_gallery_view", "match_3_d_runtime_run", "match_3_d_work_profile", "npc_state", @@ -4779,6 +5022,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "puzzle_agent_message", "puzzle_agent_session", "puzzle_event", + "puzzle_gallery_card_view", "puzzle_gallery_view", "puzzle_leaderboard_entry", "puzzle_runtime_run", @@ -4790,6 +5034,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "runtime_snapshot", "square_hole_agent_message", "square_hole_agent_session", + "square_hole_gallery_view", "square_hole_runtime_run", "square_hole_work_profile", "story_event", @@ -4801,6 +5046,7 @@ impl __sdk::SpacetimeModule for RemoteModule { "user_browse_history", "visual_novel_agent_message", "visual_novel_agent_session", + "visual_novel_gallery_view", "visual_novel_runtime_event", "visual_novel_runtime_history_entry", "visual_novel_runtime_run", diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_snapshot_type.rs new file mode 100644 index 00000000..00dbec45 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_message_snapshot_type.rs @@ -0,0 +1,23 @@ +// 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::puzzle_agent_message_kind_type::PuzzleAgentMessageKind; +use super::puzzle_agent_message_role_type::PuzzleAgentMessageRole; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAgentMessageSnapshot { + pub message_id: String, + pub session_id: String, + pub role: PuzzleAgentMessageRole, + pub kind: PuzzleAgentMessageKind, + pub text: String, + pub created_at_micros: i64, +} + +impl __sdk::InModule for PuzzleAgentMessageSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_type.rs index 39506659..00de9f76 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::puzzle_agent_session_snapshot_type::PuzzleAgentSessionSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct PuzzleAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_snapshot_type.rs new file mode 100644 index 00000000..d099e6ac --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_session_snapshot_type.rs @@ -0,0 +1,36 @@ +// 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::puzzle_agent_message_snapshot_type::PuzzleAgentMessageSnapshot; +use super::puzzle_agent_stage_type::PuzzleAgentStage; +use super::puzzle_agent_suggested_action_type::PuzzleAgentSuggestedAction; +use super::puzzle_anchor_pack_type::PuzzleAnchorPack; +use super::puzzle_result_draft_type::PuzzleResultDraft; +use super::puzzle_result_preview_envelope_type::PuzzleResultPreviewEnvelope; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: PuzzleAgentStage, + pub anchor_pack: PuzzleAnchorPack, + pub draft: Option, + pub messages: Vec, + pub last_assistant_reply: Option, + pub published_profile_id: Option, + pub suggested_actions: Vec, + pub result_preview: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for PuzzleAgentSessionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_suggested_action_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_suggested_action_type.rs new file mode 100644 index 00000000..56593222 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_agent_suggested_action_type.rs @@ -0,0 +1,17 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAgentSuggestedAction { + pub id: String, + pub action_type: String, + pub label: String, +} + +impl __sdk::InModule for PuzzleAgentSuggestedAction { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_item_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_item_type.rs new file mode 100644 index 00000000..1280d719 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_item_type.rs @@ -0,0 +1,20 @@ +// 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::puzzle_anchor_status_type::PuzzleAnchorStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAnchorItem { + pub key: String, + pub label: String, + pub value: String, + pub status: PuzzleAnchorStatus, +} + +impl __sdk::InModule for PuzzleAnchorItem { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_pack_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_pack_type.rs new file mode 100644 index 00000000..db006609 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_pack_type.rs @@ -0,0 +1,21 @@ +// 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::puzzle_anchor_item_type::PuzzleAnchorItem; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAnchorPack { + pub theme_promise: PuzzleAnchorItem, + pub visual_subject: PuzzleAnchorItem, + pub visual_mood: PuzzleAnchorItem, + pub composition_hooks: PuzzleAnchorItem, + pub tags_and_forbidden: PuzzleAnchorItem, +} + +impl __sdk::InModule for PuzzleAnchorPack { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_status_type.rs new file mode 100644 index 00000000..feb7a650 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_anchor_status_type.rs @@ -0,0 +1,22 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum PuzzleAnchorStatus { + Missing, + + Inferred, + + Confirmed, + + Locked, +} + +impl __sdk::InModule for PuzzleAnchorStatus { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_audio_asset_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_audio_asset_type.rs new file mode 100644 index 00000000..e430a9c9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_audio_asset_type.rs @@ -0,0 +1,22 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleAudioAsset { + pub task_id: String, + pub provider: String, + pub asset_object_id: Option, + pub asset_kind: Option, + pub audio_src: String, + pub prompt: Option, + pub title: Option, + pub updated_at: Option, +} + +impl __sdk::InModule for PuzzleAudioAsset { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_board_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_board_snapshot_type.rs new file mode 100644 index 00000000..2408ef0c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_board_snapshot_type.rs @@ -0,0 +1,23 @@ +// 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::puzzle_merged_group_state_type::PuzzleMergedGroupState; +use super::puzzle_piece_state_type::PuzzlePieceState; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleBoardSnapshot { + pub rows: u32, + pub cols: u32, + pub pieces: Vec, + pub merged_groups: Vec, + pub selected_piece_id: Option, + pub all_tiles_resolved: bool, +} + +impl __sdk::InModule for PuzzleBoardSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_cell_position_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_cell_position_type.rs new file mode 100644 index 00000000..92942799 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_cell_position_type.rs @@ -0,0 +1,16 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleCellPosition { + pub row: u32, + pub col: u32, +} + +impl __sdk::InModule for PuzzleCellPosition { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_creator_intent_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_creator_intent_type.rs new file mode 100644 index 00000000..9d1cff85 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_creator_intent_type.rs @@ -0,0 +1,22 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleCreatorIntent { + pub source_mode: String, + pub raw_messages_summary: String, + pub theme_promise: String, + pub visual_subject: String, + pub visual_mood: Vec, + pub composition_hooks: Vec, + pub theme_tags: Vec, + pub forbidden_directives: Vec, +} + +impl __sdk::InModule for PuzzleCreatorIntent { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_level_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_level_type.rs new file mode 100644 index 00000000..36f12999 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_draft_level_type.rs @@ -0,0 +1,30 @@ +// 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::puzzle_audio_asset_type::PuzzleAudioAsset; +use super::puzzle_generated_image_candidate_type::PuzzleGeneratedImageCandidate; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleDraftLevel { + pub level_id: String, + pub level_name: String, + pub picture_description: String, + pub picture_reference: Option, + pub ui_background_prompt: Option, + pub ui_background_image_src: Option, + pub ui_background_image_object_key: Option, + pub background_music: Option, + pub candidates: Vec, + pub selected_candidate_id: Option, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub generation_status: String, +} + +impl __sdk::InModule for PuzzleDraftLevel { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_form_draft_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_form_draft_type.rs new file mode 100644 index 00000000..c949aae1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_form_draft_type.rs @@ -0,0 +1,17 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleFormDraft { + pub work_title: Option, + pub work_description: Option, + pub picture_description: Option, +} + +impl __sdk::InModule for PuzzleFormDraft { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_card_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_card_view_row_type.rs new file mode 100644 index 00000000..3828a2c2 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_card_view_row_type.rs @@ -0,0 +1,110 @@ +// 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::puzzle_publication_status_type::PuzzlePublicationStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleGalleryCardViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub publication_status: PuzzlePublicationStatus, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub play_count: u32, + pub remix_count: u32, + pub like_count: u32, + pub point_incentive_total_half_points: u64, + pub point_incentive_claimed_points: u64, + pub publish_ready: bool, + pub generation_status: Option, +} + +impl __sdk::InModule for PuzzleGalleryCardViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `PuzzleGalleryCardViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct PuzzleGalleryCardViewRowCols { + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col>, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub level_name: __sdk::__query_builder::Col, + pub summary: __sdk::__query_builder::Col, + pub theme_tags: __sdk::__query_builder::Col>, + pub cover_image_src: __sdk::__query_builder::Col>, + pub cover_asset_id: __sdk::__query_builder::Col>, + pub publication_status: + __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, + pub play_count: __sdk::__query_builder::Col, + pub remix_count: __sdk::__query_builder::Col, + pub like_count: __sdk::__query_builder::Col, + pub point_incentive_total_half_points: + __sdk::__query_builder::Col, + pub point_incentive_claimed_points: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub generation_status: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for PuzzleGalleryCardViewRow { + type Cols = PuzzleGalleryCardViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + PuzzleGalleryCardViewRowCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + level_name: __sdk::__query_builder::Col::new(table_name, "level_name"), + summary: __sdk::__query_builder::Col::new(table_name, "summary"), + theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + cover_asset_id: __sdk::__query_builder::Col::new(table_name, "cover_asset_id"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"), + like_count: __sdk::__query_builder::Col::new(table_name, "like_count"), + point_incentive_total_half_points: __sdk::__query_builder::Col::new( + table_name, + "point_incentive_total_half_points", + ), + point_incentive_claimed_points: __sdk::__query_builder::Col::new( + table_name, + "point_incentive_claimed_points", + ), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + generation_status: __sdk::__query_builder::Col::new(table_name, "generation_status"), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_card_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_card_view_table.rs new file mode 100644 index 00000000..58c1659b --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_card_view_table.rs @@ -0,0 +1,115 @@ +// 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 super::puzzle_gallery_card_view_row_type::PuzzleGalleryCardViewRow; +use super::puzzle_publication_status_type::PuzzlePublicationStatus; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `puzzle_gallery_card_view`. +/// +/// Obtain a handle from the [`PuzzleGalleryCardViewTableAccess::puzzle_gallery_card_view`] method on [`super::RemoteTables`], +/// like `ctx.db.puzzle_gallery_card_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.puzzle_gallery_card_view().on_insert(...)`. +pub struct PuzzleGalleryCardViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `puzzle_gallery_card_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait PuzzleGalleryCardViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`PuzzleGalleryCardViewTableHandle`], which mediates access to the table `puzzle_gallery_card_view`. + fn puzzle_gallery_card_view(&self) -> PuzzleGalleryCardViewTableHandle<'_>; +} + +impl PuzzleGalleryCardViewTableAccess for super::RemoteTables { + fn puzzle_gallery_card_view(&self) -> PuzzleGalleryCardViewTableHandle<'_> { + PuzzleGalleryCardViewTableHandle { + imp: self + .imp + .get_table::("puzzle_gallery_card_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct PuzzleGalleryCardViewInsertCallbackId(__sdk::CallbackId); +pub struct PuzzleGalleryCardViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for PuzzleGalleryCardViewTableHandle<'ctx> { + type Row = PuzzleGalleryCardViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = PuzzleGalleryCardViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleGalleryCardViewInsertCallbackId { + PuzzleGalleryCardViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: PuzzleGalleryCardViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = PuzzleGalleryCardViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> PuzzleGalleryCardViewDeleteCallbackId { + PuzzleGalleryCardViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: PuzzleGalleryCardViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("puzzle_gallery_card_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `PuzzleGalleryCardViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait puzzle_gallery_card_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `PuzzleGalleryCardViewRow`. + fn puzzle_gallery_card_view(&self) -> __sdk::__query_builder::Table; +} + +impl puzzle_gallery_card_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn puzzle_gallery_card_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("puzzle_gallery_card_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_view_table.rs index 229fa37a..24857cee 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_view_table.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_gallery_view_table.rs @@ -2,246 +2,12 @@ // WILL NOT BE SAVED. MODIFY TABLES IN YOUR MODULE SOURCE CODE INSTEAD. #![allow(unused, clippy::all)] +use super::puzzle_anchor_pack_type::PuzzleAnchorPack; +use super::puzzle_draft_level_type::PuzzleDraftLevel; +use super::puzzle_publication_status_type::PuzzlePublicationStatus; +use super::puzzle_work_profile_type::PuzzleWorkProfile; use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, Copy, PartialEq, Debug)] -#[sats(crate = __lib)] -#[derive(Eq, Hash)] -pub enum PuzzleGalleryAnchorStatus { - Missing, - Inferred, - Confirmed, - Locked, -} - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] -#[sats(crate = __lib)] -pub struct PuzzleGalleryAnchorItem { - pub key: String, - pub label: String, - pub value: String, - pub status: PuzzleGalleryAnchorStatus, -} - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] -#[sats(crate = __lib)] -pub struct PuzzleGalleryAnchorPack { - pub theme_promise: PuzzleGalleryAnchorItem, - pub visual_subject: PuzzleGalleryAnchorItem, - pub visual_mood: PuzzleGalleryAnchorItem, - pub composition_hooks: PuzzleGalleryAnchorItem, - pub tags_and_forbidden: PuzzleGalleryAnchorItem, -} - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] -#[sats(crate = __lib)] -pub struct PuzzleGalleryGeneratedImageCandidate { - pub candidate_id: String, - pub image_src: String, - pub asset_id: String, - pub prompt: String, - pub actual_prompt: Option, - pub source_type: String, - pub selected: bool, -} - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] -#[sats(crate = __lib)] -pub struct PuzzleGalleryAudioAsset { - pub task_id: String, - pub provider: String, - pub asset_object_id: Option, - pub asset_kind: Option, - pub audio_src: String, - pub prompt: Option, - pub title: Option, - pub updated_at: Option, -} - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] -#[sats(crate = __lib)] -pub struct PuzzleGalleryDraftLevel { - pub level_id: String, - pub level_name: String, - pub picture_description: String, - pub picture_reference: Option, - pub ui_background_prompt: Option, - pub ui_background_image_src: Option, - pub ui_background_image_object_key: Option, - pub background_music: Option, - pub candidates: Vec, - pub selected_candidate_id: Option, - pub cover_image_src: Option, - pub cover_asset_id: Option, - pub generation_status: String, -} - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, Copy, PartialEq, Debug)] -#[sats(crate = __lib)] -#[derive(Eq, Hash)] -pub enum PuzzleGalleryPublicationStatus { - Draft, - Published, -} - -#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] -#[sats(crate = __lib)] -pub struct PuzzleGalleryViewRow { - pub work_id: String, - pub profile_id: String, - pub owner_user_id: String, - pub source_session_id: Option, - pub author_display_name: String, - pub work_title: String, - pub work_description: String, - pub level_name: String, - pub summary: String, - pub theme_tags: Vec, - pub cover_image_src: Option, - pub cover_asset_id: Option, - pub levels: Vec, - pub publication_status: PuzzleGalleryPublicationStatus, - pub updated_at_micros: i64, - pub published_at_micros: Option, - pub play_count: u32, - pub remix_count: u32, - pub like_count: u32, - pub recent_play_count_7d: u32, - pub point_incentive_total_half_points: u64, - pub point_incentive_claimed_points: u64, - pub publish_ready: bool, - pub anchor_pack: PuzzleGalleryAnchorPack, -} - -impl From for module_puzzle::PuzzleAnchorStatus { - fn from(status: PuzzleGalleryAnchorStatus) -> Self { - match status { - PuzzleGalleryAnchorStatus::Missing => Self::Missing, - PuzzleGalleryAnchorStatus::Inferred => Self::Inferred, - PuzzleGalleryAnchorStatus::Confirmed => Self::Confirmed, - PuzzleGalleryAnchorStatus::Locked => Self::Locked, - } - } -} - -impl From for module_puzzle::PuzzleAnchorItem { - fn from(item: PuzzleGalleryAnchorItem) -> Self { - Self { - key: item.key, - label: item.label, - value: item.value, - status: item.status.into(), - } - } -} - -impl From for module_puzzle::PuzzleAnchorPack { - fn from(pack: PuzzleGalleryAnchorPack) -> Self { - Self { - theme_promise: pack.theme_promise.into(), - visual_subject: pack.visual_subject.into(), - visual_mood: pack.visual_mood.into(), - composition_hooks: pack.composition_hooks.into(), - tags_and_forbidden: pack.tags_and_forbidden.into(), - } - } -} - -impl From - for module_puzzle::PuzzleGeneratedImageCandidate -{ - fn from(candidate: PuzzleGalleryGeneratedImageCandidate) -> Self { - Self { - candidate_id: candidate.candidate_id, - image_src: candidate.image_src, - asset_id: candidate.asset_id, - prompt: candidate.prompt, - actual_prompt: candidate.actual_prompt, - source_type: candidate.source_type, - selected: candidate.selected, - } - } -} - -impl From for module_puzzle::PuzzleAudioAsset { - fn from(asset: PuzzleGalleryAudioAsset) -> Self { - Self { - task_id: asset.task_id, - provider: asset.provider, - asset_object_id: asset.asset_object_id, - asset_kind: asset.asset_kind, - audio_src: asset.audio_src, - prompt: asset.prompt, - title: asset.title, - updated_at: asset.updated_at, - } - } -} - -impl From for module_puzzle::PuzzleDraftLevel { - fn from(level: PuzzleGalleryDraftLevel) -> Self { - Self { - level_id: level.level_id, - level_name: level.level_name, - picture_description: level.picture_description, - picture_reference: level.picture_reference, - ui_background_prompt: level.ui_background_prompt, - ui_background_image_src: level.ui_background_image_src, - ui_background_image_object_key: level.ui_background_image_object_key, - background_music: level.background_music.map(Into::into), - candidates: level.candidates.into_iter().map(Into::into).collect(), - selected_candidate_id: level.selected_candidate_id, - cover_image_src: level.cover_image_src, - cover_asset_id: level.cover_asset_id, - generation_status: level.generation_status, - } - } -} - -impl From for module_puzzle::PuzzlePublicationStatus { - fn from(status: PuzzleGalleryPublicationStatus) -> Self { - match status { - PuzzleGalleryPublicationStatus::Draft => Self::Draft, - PuzzleGalleryPublicationStatus::Published => Self::Published, - } - } -} - -impl From for module_puzzle::PuzzleWorkProfile { - fn from(row: PuzzleGalleryViewRow) -> Self { - Self { - work_id: row.work_id, - profile_id: row.profile_id, - owner_user_id: row.owner_user_id, - source_session_id: row.source_session_id, - author_display_name: row.author_display_name, - work_title: row.work_title, - work_description: row.work_description, - level_name: row.level_name, - summary: row.summary, - theme_tags: row.theme_tags, - cover_image_src: row.cover_image_src, - cover_asset_id: row.cover_asset_id, - levels: row.levels.into_iter().map(Into::into).collect(), - publication_status: row.publication_status.into(), - updated_at_micros: row.updated_at_micros, - published_at_micros: row.published_at_micros, - play_count: row.play_count, - remix_count: row.remix_count, - like_count: row.like_count, - recent_play_count_7d: row.recent_play_count_7d, - point_incentive_total_half_points: row.point_incentive_total_half_points, - point_incentive_claimed_points: row.point_incentive_claimed_points, - publish_ready: row.publish_ready, - anchor_pack: row.anchor_pack.into(), - } - } -} - -impl __sdk::InModule for PuzzleGalleryViewRow { - type Module = super::RemoteModule; -} - /// Table handle for the table `puzzle_gallery_view`. /// /// Obtain a handle from the [`PuzzleGalleryViewTableAccess::puzzle_gallery_view`] method on [`super::RemoteTables`], @@ -251,7 +17,7 @@ impl __sdk::InModule for PuzzleGalleryViewRow { /// but to directly chain method calls, /// like `ctx.db.puzzle_gallery_view().on_insert(...)`. pub struct PuzzleGalleryViewTableHandle<'ctx> { - imp: __sdk::TableHandle, + imp: __sdk::TableHandle, ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, } @@ -270,7 +36,7 @@ impl PuzzleGalleryViewTableAccess for super::RemoteTables { PuzzleGalleryViewTableHandle { imp: self .imp - .get_table::("puzzle_gallery_view"), + .get_table::("puzzle_gallery_view"), ctx: std::marker::PhantomData, } } @@ -280,13 +46,13 @@ pub struct PuzzleGalleryViewInsertCallbackId(__sdk::CallbackId); pub struct PuzzleGalleryViewDeleteCallbackId(__sdk::CallbackId); impl<'ctx> __sdk::Table for PuzzleGalleryViewTableHandle<'ctx> { - type Row = PuzzleGalleryViewRow; + type Row = PuzzleWorkProfile; type EventContext = super::EventContext; fn count(&self) -> u64 { self.imp.count() } - fn iter(&self) -> impl Iterator + '_ { + fn iter(&self) -> impl Iterator + '_ { self.imp.iter() } @@ -319,32 +85,32 @@ impl<'ctx> __sdk::Table for PuzzleGalleryViewTableHandle<'ctx> { #[doc(hidden)] pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { - let _table = client_cache.get_or_make_table::("puzzle_gallery_view"); + let _table = client_cache.get_or_make_table::("puzzle_gallery_view"); } #[doc(hidden)] pub(super) fn parse_table_update( raw_updates: __ws::v2::TableUpdate, -) -> __sdk::Result<__sdk::TableUpdate> { +) -> __sdk::Result<__sdk::TableUpdate> { __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { - __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") .with_cause(e) .into() }) } #[allow(non_camel_case_types)] -/// Extension trait for query builder access to the table `PuzzleGalleryViewRow`. +/// Extension trait for query builder access to the table `PuzzleWorkProfile`. /// /// Implemented for [`__sdk::QueryTableAccessor`]. pub trait puzzle_gallery_viewQueryTableAccess { #[allow(non_snake_case)] - /// Get a query builder for the table `PuzzleGalleryViewRow`. - fn puzzle_gallery_view(&self) -> __sdk::__query_builder::Table; + /// Get a query builder for the table `PuzzleWorkProfile`. + fn puzzle_gallery_view(&self) -> __sdk::__query_builder::Table; } impl puzzle_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { - fn puzzle_gallery_view(&self) -> __sdk::__query_builder::Table { + fn puzzle_gallery_view(&self) -> __sdk::__query_builder::Table { __sdk::__query_builder::Table::new("puzzle_gallery_view") } } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_generated_image_candidate_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_generated_image_candidate_type.rs new file mode 100644 index 00000000..6dd003d7 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_generated_image_candidate_type.rs @@ -0,0 +1,21 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleGeneratedImageCandidate { + pub candidate_id: String, + pub image_src: String, + pub asset_id: String, + pub prompt: String, + pub actual_prompt: Option, + pub source_type: String, + pub selected: bool, +} + +impl __sdk::InModule for PuzzleGeneratedImageCandidate { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_leaderboard_entry_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_leaderboard_entry_type.rs new file mode 100644 index 00000000..474c7ffa --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_leaderboard_entry_type.rs @@ -0,0 +1,19 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleLeaderboardEntry { + pub rank: u32, + pub nickname: String, + pub elapsed_ms: u64, + pub visible_tags: Vec, + pub is_current_player: bool, +} + +impl __sdk::InModule for PuzzleLeaderboardEntry { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_merged_group_state_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_merged_group_state_type.rs new file mode 100644 index 00000000..b6cba30c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_merged_group_state_type.rs @@ -0,0 +1,19 @@ +// 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::puzzle_cell_position_type::PuzzleCellPosition; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleMergedGroupState { + pub group_id: String, + pub piece_ids: Vec, + pub occupied_cells: Vec, +} + +impl __sdk::InModule for PuzzleMergedGroupState { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_piece_state_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_piece_state_type.rs new file mode 100644 index 00000000..7cb0ef6d --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_piece_state_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzlePieceState { + pub piece_id: String, + pub correct_row: u32, + pub correct_col: u32, + pub current_row: u32, + pub current_col: u32, + pub merged_group_id: Option, +} + +impl __sdk::InModule for PuzzlePieceState { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_recommended_next_work_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_recommended_next_work_type.rs new file mode 100644 index 00000000..69d26ad1 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_recommended_next_work_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleRecommendedNextWork { + pub profile_id: String, + pub level_name: String, + pub author_display_name: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub similarity_score: f32, +} + +impl __sdk::InModule for PuzzleRecommendedNextWork { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_draft_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_draft_type.rs new file mode 100644 index 00000000..adb2dff4 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_draft_type.rs @@ -0,0 +1,35 @@ +// 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::puzzle_anchor_pack_type::PuzzleAnchorPack; +use super::puzzle_creator_intent_type::PuzzleCreatorIntent; +use super::puzzle_draft_level_type::PuzzleDraftLevel; +use super::puzzle_form_draft_type::PuzzleFormDraft; +use super::puzzle_generated_image_candidate_type::PuzzleGeneratedImageCandidate; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleResultDraft { + pub work_title: String, + pub work_description: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub forbidden_directives: Vec, + pub creator_intent: Option, + pub anchor_pack: PuzzleAnchorPack, + pub candidates: Vec, + pub selected_candidate_id: Option, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub generation_status: String, + pub levels: Vec, + pub form_draft: Option, +} + +impl __sdk::InModule for PuzzleResultDraft { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_blocker_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_blocker_type.rs new file mode 100644 index 00000000..e604eb40 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_blocker_type.rs @@ -0,0 +1,17 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleResultPreviewBlocker { + pub id: String, + pub code: String, + pub message: String, +} + +impl __sdk::InModule for PuzzleResultPreviewBlocker { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_envelope_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_envelope_type.rs new file mode 100644 index 00000000..4e09e613 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_envelope_type.rs @@ -0,0 +1,22 @@ +// 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::puzzle_result_draft_type::PuzzleResultDraft; +use super::puzzle_result_preview_blocker_type::PuzzleResultPreviewBlocker; +use super::puzzle_result_preview_finding_type::PuzzleResultPreviewFinding; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleResultPreviewEnvelope { + pub draft: PuzzleResultDraft, + pub blockers: Vec, + pub quality_findings: Vec, + pub publish_ready: bool, +} + +impl __sdk::InModule for PuzzleResultPreviewEnvelope { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_finding_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_finding_type.rs new file mode 100644 index 00000000..a43c4a16 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_result_preview_finding_type.rs @@ -0,0 +1,18 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleResultPreviewFinding { + pub id: String, + pub severity: String, + pub code: String, + pub message: String, +} + +impl __sdk::InModule for PuzzleResultPreviewFinding { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_type.rs index 54f6349b..5b1a430c 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::puzzle_run_snapshot_type::PuzzleRunSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct PuzzleRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_snapshot_type.rs new file mode 100644 index 00000000..b32fe5d0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_run_snapshot_type.rs @@ -0,0 +1,32 @@ +// 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::puzzle_leaderboard_entry_type::PuzzleLeaderboardEntry; +use super::puzzle_recommended_next_work_type::PuzzleRecommendedNextWork; +use super::puzzle_runtime_level_snapshot_type::PuzzleRuntimeLevelSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleRunSnapshot { + pub run_id: String, + pub entry_profile_id: String, + pub cleared_level_count: u32, + pub current_level_index: u32, + pub current_grid_size: u32, + pub played_profile_ids: Vec, + pub previous_level_tags: Vec, + pub current_level: Option, + pub recommended_next_profile_id: Option, + pub next_level_mode: String, + pub next_level_profile_id: Option, + pub next_level_id: Option, + pub recommended_next_works: Vec, + pub leaderboard_entries: Vec, +} + +impl __sdk::InModule for PuzzleRunSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_snapshot_type.rs new file mode 100644 index 00000000..3554ed20 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_snapshot_type.rs @@ -0,0 +1,44 @@ +// 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::puzzle_audio_asset_type::PuzzleAudioAsset; +use super::puzzle_board_snapshot_type::PuzzleBoardSnapshot; +use super::puzzle_leaderboard_entry_type::PuzzleLeaderboardEntry; +use super::puzzle_runtime_level_status_type::PuzzleRuntimeLevelStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleRuntimeLevelSnapshot { + pub run_id: String, + pub level_index: u32, + pub level_id: Option, + pub grid_size: u32, + pub profile_id: String, + pub level_name: String, + pub author_display_name: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub ui_background_image_src: Option, + pub ui_background_image_object_key: Option, + pub background_music: Option, + pub board: PuzzleBoardSnapshot, + pub status: PuzzleRuntimeLevelStatus, + pub started_at_ms: u64, + pub cleared_at_ms: Option, + pub elapsed_ms: Option, + pub time_limit_ms: u64, + pub remaining_ms: u64, + pub paused_accumulated_ms: u64, + pub pause_started_at_ms: Option, + pub freeze_accumulated_ms: u64, + pub freeze_started_at_ms: Option, + pub freeze_until_ms: Option, + pub leaderboard_entries: Vec, +} + +impl __sdk::InModule for PuzzleRuntimeLevelSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_status_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_status_type.rs new file mode 100644 index 00000000..dd491ccf --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_runtime_level_status_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +#[derive(Copy, Eq, Hash)] +pub enum PuzzleRuntimeLevelStatus { + Playing, + + Cleared, + + Failed, +} + +impl __sdk::InModule for PuzzleRuntimeLevelStatus { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_type.rs index d59a56cc..019c8f94 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::puzzle_work_profile_type::PuzzleWorkProfile; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct PuzzleWorkProcedureResult { pub ok: bool, - pub item_json: Option, + pub item: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_type.rs new file mode 100644 index 00000000..6b41228e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_work_profile_type.rs @@ -0,0 +1,119 @@ +// 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::puzzle_anchor_pack_type::PuzzleAnchorPack; +use super::puzzle_draft_level_type::PuzzleDraftLevel; +use super::puzzle_publication_status_type::PuzzlePublicationStatus; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct PuzzleWorkProfile { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub levels: Vec, + pub publication_status: PuzzlePublicationStatus, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub play_count: u32, + pub remix_count: u32, + pub like_count: u32, + pub recent_play_count_7_d: u32, + pub point_incentive_total_half_points: u64, + pub point_incentive_claimed_points: u64, + pub publish_ready: bool, + pub anchor_pack: PuzzleAnchorPack, +} + +impl __sdk::InModule for PuzzleWorkProfile { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `PuzzleWorkProfile`. +/// +/// Provides typed access to columns for query building. +pub struct PuzzleWorkProfileCols { + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col>, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub level_name: __sdk::__query_builder::Col, + pub summary: __sdk::__query_builder::Col, + pub theme_tags: __sdk::__query_builder::Col>, + pub cover_image_src: __sdk::__query_builder::Col>, + pub cover_asset_id: __sdk::__query_builder::Col>, + pub levels: __sdk::__query_builder::Col>, + pub publication_status: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, + pub play_count: __sdk::__query_builder::Col, + pub remix_count: __sdk::__query_builder::Col, + pub like_count: __sdk::__query_builder::Col, + pub recent_play_count_7_d: __sdk::__query_builder::Col, + pub point_incentive_total_half_points: __sdk::__query_builder::Col, + pub point_incentive_claimed_points: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub anchor_pack: __sdk::__query_builder::Col, +} + +impl __sdk::__query_builder::HasCols for PuzzleWorkProfile { + type Cols = PuzzleWorkProfileCols; + fn cols(table_name: &'static str) -> Self::Cols { + PuzzleWorkProfileCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + level_name: __sdk::__query_builder::Col::new(table_name, "level_name"), + summary: __sdk::__query_builder::Col::new(table_name, "summary"), + theme_tags: __sdk::__query_builder::Col::new(table_name, "theme_tags"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + cover_asset_id: __sdk::__query_builder::Col::new(table_name, "cover_asset_id"), + levels: __sdk::__query_builder::Col::new(table_name, "levels"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + remix_count: __sdk::__query_builder::Col::new(table_name, "remix_count"), + like_count: __sdk::__query_builder::Col::new(table_name, "like_count"), + recent_play_count_7_d: __sdk::__query_builder::Col::new( + table_name, + "recent_play_count_7_d", + ), + point_incentive_total_half_points: __sdk::__query_builder::Col::new( + table_name, + "point_incentive_total_half_points", + ), + point_incentive_claimed_points: __sdk::__query_builder::Col::new( + table_name, + "point_incentive_claimed_points", + ), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + anchor_pack: __sdk::__query_builder::Col::new(table_name, "anchor_pack"), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_type.rs index 6a34c60f..53204197 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/puzzle_works_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::puzzle_work_profile_type::PuzzleWorkProfile; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct PuzzleWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_message_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_message_snapshot_type.rs new file mode 100644 index 00000000..8af09de8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_message_snapshot_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleAgentMessageSnapshot { + pub message_id: String, + pub session_id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at_micros: i64, +} + +impl __sdk::InModule for SquareHoleAgentMessageSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_session_procedure_result_type.rs index 5ea89d13..0b7384a7 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_session_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_session_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::square_hole_agent_session_snapshot_type::SquareHoleAgentSessionSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct SquareHoleAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_session_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_session_snapshot_type.rs new file mode 100644 index 00000000..47130393 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_agent_session_snapshot_type.rs @@ -0,0 +1,31 @@ +// 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::square_hole_agent_message_snapshot_type::SquareHoleAgentMessageSnapshot; +use super::square_hole_creator_config_snapshot_type::SquareHoleCreatorConfigSnapshot; +use super::square_hole_draft_snapshot_type::SquareHoleDraftSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub seed_text: String, + pub current_turn: u32, + pub progress_percent: u32, + pub stage: String, + pub config: SquareHoleCreatorConfigSnapshot, + pub draft: Option, + pub messages: Vec, + pub last_assistant_reply: String, + pub published_profile_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for SquareHoleAgentSessionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_creator_config_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_creator_config_snapshot_type.rs new file mode 100644 index 00000000..b10dd3e9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_creator_config_snapshot_type.rs @@ -0,0 +1,26 @@ +// 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::square_hole_hole_option_snapshot_type::SquareHoleHoleOptionSnapshot; +use super::square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleCreatorConfigSnapshot { + pub theme_text: String, + pub twist_rule: String, + pub shape_count: u32, + pub difficulty: u32, + pub shape_options: Vec, + pub hole_options: Vec, + pub background_prompt: String, + pub cover_image_src: String, + pub background_image_src: String, +} + +impl __sdk::InModule for SquareHoleCreatorConfigSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_draft_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_draft_snapshot_type.rs new file mode 100644 index 00000000..810103ea --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_draft_snapshot_type.rs @@ -0,0 +1,30 @@ +// 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::square_hole_hole_option_snapshot_type::SquareHoleHoleOptionSnapshot; +use super::square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleDraftSnapshot { + pub profile_id: String, + pub game_name: String, + pub theme_text: String, + pub twist_rule: String, + pub summary_text: String, + pub tags: Vec, + pub cover_image_src: String, + pub background_prompt: String, + pub background_image_src: String, + pub shape_options: Vec, + pub hole_options: Vec, + pub shape_count: u32, + pub difficulty: u32, +} + +impl __sdk::InModule for SquareHoleDraftSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_drop_feedback_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_drop_feedback_snapshot_type.rs new file mode 100644 index 00000000..3ff25600 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_drop_feedback_snapshot_type.rs @@ -0,0 +1,17 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleDropFeedbackSnapshot { + pub accepted: bool, + pub reject_reason: Option, + pub message: String, +} + +impl __sdk::InModule for SquareHoleDropFeedbackSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_drop_shape_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_drop_shape_procedure_result_type.rs index 06ba3616..0d2b6665 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_drop_shape_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_drop_shape_procedure_result_type.rs @@ -4,13 +4,16 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::square_hole_drop_feedback_snapshot_type::SquareHoleDropFeedbackSnapshot; +use super::square_hole_run_snapshot_type::SquareHoleRunSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct SquareHoleDropShapeProcedureResult { pub ok: bool, pub status: String, - pub run_json: Option, - pub feedback_json: Option, + pub run: Option, + pub feedback: Option, pub failure_reason: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_gallery_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_gallery_view_row_type.rs new file mode 100644 index 00000000..997f82d8 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_gallery_view_row_type.rs @@ -0,0 +1,108 @@ +// 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::square_hole_hole_option_snapshot_type::SquareHoleHoleOptionSnapshot; +use super::square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleGalleryViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub game_name: String, + pub theme_text: String, + pub twist_rule: String, + pub summary_text: String, + pub tags: Vec, + pub cover_image_src: String, + pub background_prompt: String, + pub background_image_src: String, + pub shape_options: Vec, + pub hole_options: Vec, + pub shape_count: u32, + pub difficulty: u32, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for SquareHoleGalleryViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `SquareHoleGalleryViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct SquareHoleGalleryViewRowCols { + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col, + pub author_display_name: __sdk::__query_builder::Col, + pub game_name: __sdk::__query_builder::Col, + pub theme_text: __sdk::__query_builder::Col, + pub twist_rule: __sdk::__query_builder::Col, + pub summary_text: __sdk::__query_builder::Col, + pub tags: __sdk::__query_builder::Col>, + pub cover_image_src: __sdk::__query_builder::Col, + pub background_prompt: __sdk::__query_builder::Col, + pub background_image_src: __sdk::__query_builder::Col, + pub shape_options: + __sdk::__query_builder::Col>, + pub hole_options: + __sdk::__query_builder::Col>, + pub shape_count: __sdk::__query_builder::Col, + pub difficulty: __sdk::__query_builder::Col, + pub publication_status: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for SquareHoleGalleryViewRow { + type Cols = SquareHoleGalleryViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + SquareHoleGalleryViewRowCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + game_name: __sdk::__query_builder::Col::new(table_name, "game_name"), + theme_text: __sdk::__query_builder::Col::new(table_name, "theme_text"), + twist_rule: __sdk::__query_builder::Col::new(table_name, "twist_rule"), + summary_text: __sdk::__query_builder::Col::new(table_name, "summary_text"), + tags: __sdk::__query_builder::Col::new(table_name, "tags"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + background_prompt: __sdk::__query_builder::Col::new(table_name, "background_prompt"), + background_image_src: __sdk::__query_builder::Col::new( + table_name, + "background_image_src", + ), + shape_options: __sdk::__query_builder::Col::new(table_name, "shape_options"), + hole_options: __sdk::__query_builder::Col::new(table_name, "hole_options"), + shape_count: __sdk::__query_builder::Col::new(table_name, "shape_count"), + difficulty: __sdk::__query_builder::Col::new(table_name, "difficulty"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_gallery_view_table.rs new file mode 100644 index 00000000..62f4b4b2 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_gallery_view_table.rs @@ -0,0 +1,116 @@ +// 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 super::square_hole_gallery_view_row_type::SquareHoleGalleryViewRow; +use super::square_hole_hole_option_snapshot_type::SquareHoleHoleOptionSnapshot; +use super::square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `square_hole_gallery_view`. +/// +/// Obtain a handle from the [`SquareHoleGalleryViewTableAccess::square_hole_gallery_view`] method on [`super::RemoteTables`], +/// like `ctx.db.square_hole_gallery_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.square_hole_gallery_view().on_insert(...)`. +pub struct SquareHoleGalleryViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `square_hole_gallery_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait SquareHoleGalleryViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`SquareHoleGalleryViewTableHandle`], which mediates access to the table `square_hole_gallery_view`. + fn square_hole_gallery_view(&self) -> SquareHoleGalleryViewTableHandle<'_>; +} + +impl SquareHoleGalleryViewTableAccess for super::RemoteTables { + fn square_hole_gallery_view(&self) -> SquareHoleGalleryViewTableHandle<'_> { + SquareHoleGalleryViewTableHandle { + imp: self + .imp + .get_table::("square_hole_gallery_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct SquareHoleGalleryViewInsertCallbackId(__sdk::CallbackId); +pub struct SquareHoleGalleryViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for SquareHoleGalleryViewTableHandle<'ctx> { + type Row = SquareHoleGalleryViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = SquareHoleGalleryViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SquareHoleGalleryViewInsertCallbackId { + SquareHoleGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: SquareHoleGalleryViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = SquareHoleGalleryViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> SquareHoleGalleryViewDeleteCallbackId { + SquareHoleGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: SquareHoleGalleryViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("square_hole_gallery_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `SquareHoleGalleryViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait square_hole_gallery_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `SquareHoleGalleryViewRow`. + fn square_hole_gallery_view(&self) -> __sdk::__query_builder::Table; +} + +impl square_hole_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn square_hole_gallery_view(&self) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("square_hole_gallery_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_hole_option_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_hole_option_snapshot_type.rs new file mode 100644 index 00000000..e0251660 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_hole_option_snapshot_type.rs @@ -0,0 +1,19 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleHoleOptionSnapshot { + pub hole_id: String, + pub hole_kind: String, + pub label: String, + pub image_prompt: String, + pub image_src: String, +} + +impl __sdk::InModule for SquareHoleHoleOptionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_hole_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_hole_snapshot_type.rs new file mode 100644 index 00000000..5663a23f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_hole_snapshot_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleHoleSnapshot { + pub hole_id: String, + pub hole_kind: String, + pub label: String, + pub x: f32, + pub y: f32, + pub image_src: String, +} + +impl __sdk::InModule for SquareHoleHoleSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_run_procedure_result_type.rs index e4a5817d..ab11a2f4 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_run_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_run_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::square_hole_run_snapshot_type::SquareHoleRunSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct SquareHoleRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_run_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_run_snapshot_type.rs new file mode 100644 index 00000000..a8d1e8b0 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_run_snapshot_type.rs @@ -0,0 +1,39 @@ +// 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::square_hole_drop_feedback_snapshot_type::SquareHoleDropFeedbackSnapshot; +use super::square_hole_hole_snapshot_type::SquareHoleHoleSnapshot; +use super::square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; +use super::square_hole_shape_snapshot_type::SquareHoleShapeSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleRunSnapshot { + pub run_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub status: String, + pub snapshot_version: u64, + pub started_at_ms: i64, + pub duration_limit_ms: i64, + pub server_now_ms: i64, + pub remaining_ms: i64, + pub total_shape_count: u32, + pub completed_shape_count: u32, + pub combo: u32, + pub best_combo: u32, + pub score: u32, + pub rule_label: String, + pub background_image_src: String, + pub shape_options: Vec, + pub current_shape: Option, + pub holes: Vec, + pub last_feedback: Option, +} + +impl __sdk::InModule for SquareHoleRunSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_shape_option_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_shape_option_snapshot_type.rs new file mode 100644 index 00000000..8a0d062e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_shape_option_snapshot_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleShapeOptionSnapshot { + pub option_id: String, + pub shape_kind: String, + pub label: String, + pub target_hole_id: String, + pub image_prompt: String, + pub image_src: String, +} + +impl __sdk::InModule for SquareHoleShapeOptionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_shape_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_shape_snapshot_type.rs new file mode 100644 index 00000000..2c16b1c9 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_shape_snapshot_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleShapeSnapshot { + pub shape_id: String, + pub shape_kind: String, + pub label: String, + pub target_hole_id: String, + pub color: String, + pub image_src: String, +} + +impl __sdk::InModule for SquareHoleShapeSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_work_procedure_result_type.rs index 0565f0e9..a3682071 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_work_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_work_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::square_hole_work_snapshot_type::SquareHoleWorkSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct SquareHoleWorkProcedureResult { pub ok: bool, - pub work_json: Option, + pub work: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_work_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_work_snapshot_type.rs new file mode 100644 index 00000000..54786576 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_work_snapshot_type.rs @@ -0,0 +1,41 @@ +// 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::square_hole_creator_config_snapshot_type::SquareHoleCreatorConfigSnapshot; +use super::square_hole_hole_option_snapshot_type::SquareHoleHoleOptionSnapshot; +use super::square_hole_shape_option_snapshot_type::SquareHoleShapeOptionSnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct SquareHoleWorkSnapshot { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub game_name: String, + pub theme_text: String, + pub twist_rule: String, + pub summary_text: String, + pub tags: Vec, + pub cover_image_src: String, + pub background_prompt: String, + pub background_image_src: String, + pub shape_options: Vec, + pub hole_options: Vec, + pub shape_count: u32, + pub difficulty: u32, + pub config: SquareHoleCreatorConfigSnapshot, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for SquareHoleWorkSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_works_procedure_result_type.rs index 6f7ca3f3..09faad0f 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/square_hole_works_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/square_hole_works_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::square_hole_work_snapshot_type::SquareHoleWorkSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct SquareHoleWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_message_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_message_snapshot_type.rs new file mode 100644 index 00000000..a337915a --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_message_snapshot_type.rs @@ -0,0 +1,20 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelAgentMessageSnapshot { + pub message_id: String, + pub session_id: String, + pub role: String, + pub kind: String, + pub text: String, + pub created_at_micros: i64, +} + +impl __sdk::InModule for VisualNovelAgentMessageSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_session_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_session_procedure_result_type.rs index f04d06eb..7ed44833 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_session_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_session_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::visual_novel_agent_session_snapshot_type::VisualNovelAgentSessionSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct VisualNovelAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_session_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_session_snapshot_type.rs new file mode 100644 index 00000000..623a380e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_agent_session_snapshot_type.rs @@ -0,0 +1,32 @@ +// 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::visual_novel_agent_message_snapshot_type::VisualNovelAgentMessageSnapshot; +use super::visual_novel_json_value_type::VisualNovelJsonValue; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelAgentSessionSnapshot { + pub session_id: String, + pub owner_user_id: String, + pub source_mode: String, + pub status: String, + pub seed_text: String, + pub source_asset_ids: Vec, + pub current_turn: u32, + pub progress_percent: u32, + pub messages: Vec, + pub draft: Option, + pub pending_action: Option, + pub last_assistant_reply: Option, + pub published_profile_id: Option, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for VisualNovelAgentSessionSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_gallery_view_row_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_gallery_view_row_type.rs new file mode 100644 index 00000000..e0199208 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_gallery_view_row_type.rs @@ -0,0 +1,82 @@ +// 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}; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelGalleryViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub tags: Vec, + pub cover_image_src: Option, + pub source_asset_ids: Vec, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub created_at_micros: i64, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for VisualNovelGalleryViewRow { + type Module = super::RemoteModule; +} + +/// Column accessor struct for the table `VisualNovelGalleryViewRow`. +/// +/// Provides typed access to columns for query building. +pub struct VisualNovelGalleryViewRowCols { + pub work_id: __sdk::__query_builder::Col, + pub profile_id: __sdk::__query_builder::Col, + pub owner_user_id: __sdk::__query_builder::Col, + pub source_session_id: __sdk::__query_builder::Col>, + pub author_display_name: __sdk::__query_builder::Col, + pub work_title: __sdk::__query_builder::Col, + pub work_description: __sdk::__query_builder::Col, + pub tags: __sdk::__query_builder::Col>, + pub cover_image_src: __sdk::__query_builder::Col>, + pub source_asset_ids: __sdk::__query_builder::Col>, + pub publication_status: __sdk::__query_builder::Col, + pub publish_ready: __sdk::__query_builder::Col, + pub play_count: __sdk::__query_builder::Col, + pub created_at_micros: __sdk::__query_builder::Col, + pub updated_at_micros: __sdk::__query_builder::Col, + pub published_at_micros: __sdk::__query_builder::Col>, +} + +impl __sdk::__query_builder::HasCols for VisualNovelGalleryViewRow { + type Cols = VisualNovelGalleryViewRowCols; + fn cols(table_name: &'static str) -> Self::Cols { + VisualNovelGalleryViewRowCols { + work_id: __sdk::__query_builder::Col::new(table_name, "work_id"), + profile_id: __sdk::__query_builder::Col::new(table_name, "profile_id"), + owner_user_id: __sdk::__query_builder::Col::new(table_name, "owner_user_id"), + source_session_id: __sdk::__query_builder::Col::new(table_name, "source_session_id"), + author_display_name: __sdk::__query_builder::Col::new( + table_name, + "author_display_name", + ), + work_title: __sdk::__query_builder::Col::new(table_name, "work_title"), + work_description: __sdk::__query_builder::Col::new(table_name, "work_description"), + tags: __sdk::__query_builder::Col::new(table_name, "tags"), + cover_image_src: __sdk::__query_builder::Col::new(table_name, "cover_image_src"), + source_asset_ids: __sdk::__query_builder::Col::new(table_name, "source_asset_ids"), + publication_status: __sdk::__query_builder::Col::new(table_name, "publication_status"), + publish_ready: __sdk::__query_builder::Col::new(table_name, "publish_ready"), + play_count: __sdk::__query_builder::Col::new(table_name, "play_count"), + created_at_micros: __sdk::__query_builder::Col::new(table_name, "created_at_micros"), + updated_at_micros: __sdk::__query_builder::Col::new(table_name, "updated_at_micros"), + published_at_micros: __sdk::__query_builder::Col::new( + table_name, + "published_at_micros", + ), + } + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_gallery_view_table.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_gallery_view_table.rs new file mode 100644 index 00000000..a1f70563 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_gallery_view_table.rs @@ -0,0 +1,117 @@ +// 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 super::visual_novel_gallery_view_row_type::VisualNovelGalleryViewRow; +use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; + +/// Table handle for the table `visual_novel_gallery_view`. +/// +/// Obtain a handle from the [`VisualNovelGalleryViewTableAccess::visual_novel_gallery_view`] method on [`super::RemoteTables`], +/// like `ctx.db.visual_novel_gallery_view()`. +/// +/// Users are encouraged not to explicitly reference this type, +/// but to directly chain method calls, +/// like `ctx.db.visual_novel_gallery_view().on_insert(...)`. +pub struct VisualNovelGalleryViewTableHandle<'ctx> { + imp: __sdk::TableHandle, + ctx: std::marker::PhantomData<&'ctx super::RemoteTables>, +} + +#[allow(non_camel_case_types)] +/// Extension trait for access to the table `visual_novel_gallery_view`. +/// +/// Implemented for [`super::RemoteTables`]. +pub trait VisualNovelGalleryViewTableAccess { + #[allow(non_snake_case)] + /// Obtain a [`VisualNovelGalleryViewTableHandle`], which mediates access to the table `visual_novel_gallery_view`. + fn visual_novel_gallery_view(&self) -> VisualNovelGalleryViewTableHandle<'_>; +} + +impl VisualNovelGalleryViewTableAccess for super::RemoteTables { + fn visual_novel_gallery_view(&self) -> VisualNovelGalleryViewTableHandle<'_> { + VisualNovelGalleryViewTableHandle { + imp: self + .imp + .get_table::("visual_novel_gallery_view"), + ctx: std::marker::PhantomData, + } + } +} + +pub struct VisualNovelGalleryViewInsertCallbackId(__sdk::CallbackId); +pub struct VisualNovelGalleryViewDeleteCallbackId(__sdk::CallbackId); + +impl<'ctx> __sdk::Table for VisualNovelGalleryViewTableHandle<'ctx> { + type Row = VisualNovelGalleryViewRow; + type EventContext = super::EventContext; + + fn count(&self) -> u64 { + self.imp.count() + } + fn iter(&self) -> impl Iterator + '_ { + self.imp.iter() + } + + type InsertCallbackId = VisualNovelGalleryViewInsertCallbackId; + + fn on_insert( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> VisualNovelGalleryViewInsertCallbackId { + VisualNovelGalleryViewInsertCallbackId(self.imp.on_insert(Box::new(callback))) + } + + fn remove_on_insert(&self, callback: VisualNovelGalleryViewInsertCallbackId) { + self.imp.remove_on_insert(callback.0) + } + + type DeleteCallbackId = VisualNovelGalleryViewDeleteCallbackId; + + fn on_delete( + &self, + callback: impl FnMut(&Self::EventContext, &Self::Row) + Send + 'static, + ) -> VisualNovelGalleryViewDeleteCallbackId { + VisualNovelGalleryViewDeleteCallbackId(self.imp.on_delete(Box::new(callback))) + } + + fn remove_on_delete(&self, callback: VisualNovelGalleryViewDeleteCallbackId) { + self.imp.remove_on_delete(callback.0) + } +} + +#[doc(hidden)] +pub(super) fn register_table(client_cache: &mut __sdk::ClientCache) { + let _table = + client_cache.get_or_make_table::("visual_novel_gallery_view"); +} + +#[doc(hidden)] +pub(super) fn parse_table_update( + raw_updates: __ws::v2::TableUpdate, +) -> __sdk::Result<__sdk::TableUpdate> { + __sdk::TableUpdate::parse_table_update(raw_updates).map_err(|e| { + __sdk::InternalError::failed_parse("TableUpdate", "TableUpdate") + .with_cause(e) + .into() + }) +} + +#[allow(non_camel_case_types)] +/// Extension trait for query builder access to the table `VisualNovelGalleryViewRow`. +/// +/// Implemented for [`__sdk::QueryTableAccessor`]. +pub trait visual_novel_gallery_viewQueryTableAccess { + #[allow(non_snake_case)] + /// Get a query builder for the table `VisualNovelGalleryViewRow`. + fn visual_novel_gallery_view(&self) + -> __sdk::__query_builder::Table; +} + +impl visual_novel_gallery_viewQueryTableAccess for __sdk::QueryTableAccessor { + fn visual_novel_gallery_view( + &self, + ) -> __sdk::__query_builder::Table { + __sdk::__query_builder::Table::new("visual_novel_gallery_view") + } +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_history_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_history_procedure_result_type.rs index c5c5935c..f0a3e7cd 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_history_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_history_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::visual_novel_runtime_history_entry_snapshot_type::VisualNovelRuntimeHistoryEntrySnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct VisualNovelHistoryProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_json_field_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_json_field_type.rs new file mode 100644 index 00000000..d0789512 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_json_field_type.rs @@ -0,0 +1,18 @@ +// 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::visual_novel_json_value_type::VisualNovelJsonValue; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelJsonField { + pub key: String, + pub value: VisualNovelJsonValue, +} + +impl __sdk::InModule for VisualNovelJsonField { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_json_value_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_json_value_type.rs new file mode 100644 index 00000000..31bb6ffb --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_json_value_type.rs @@ -0,0 +1,27 @@ +// 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::visual_novel_json_field_type::VisualNovelJsonField; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub enum VisualNovelJsonValue { + Null, + + Bool(bool), + + Number(f64), + + String(String), + + Array(Vec), + + Object(Vec), +} + +impl __sdk::InModule for VisualNovelJsonValue { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_run_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_run_procedure_result_type.rs index 66bbe483..b9bdde61 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_run_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_run_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::visual_novel_run_snapshot_type::VisualNovelRunSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct VisualNovelRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_run_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_run_snapshot_type.rs new file mode 100644 index 00000000..a4ea47f6 --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_run_snapshot_type.rs @@ -0,0 +1,32 @@ +// 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::visual_novel_json_value_type::VisualNovelJsonValue; +use super::visual_novel_runtime_history_entry_snapshot_type::VisualNovelRuntimeHistoryEntrySnapshot; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelRunSnapshot { + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub mode: String, + pub status: String, + pub current_scene_id: Option, + pub current_phase_id: Option, + pub visible_character_ids: Vec, + pub flags: VisualNovelJsonValue, + pub metrics: VisualNovelJsonValue, + pub history: Vec, + pub available_choices: VisualNovelJsonValue, + pub text_mode_enabled: bool, + pub created_at_micros: i64, + pub updated_at_micros: i64, +} + +impl __sdk::InModule for VisualNovelRunSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_event_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_event_procedure_result_type.rs index 58bdfbfb..ccabc4be 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_event_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_event_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::visual_novel_runtime_event_snapshot_type::VisualNovelRuntimeEventSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct VisualNovelRuntimeEventProcedureResult { pub ok: bool, - pub event_json: Option, + pub event: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_event_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_event_snapshot_type.rs new file mode 100644 index 00000000..8749eb5c --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_event_snapshot_type.rs @@ -0,0 +1,25 @@ +// 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::visual_novel_json_value_type::VisualNovelJsonValue; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelRuntimeEventSnapshot { + pub event_id: String, + pub run_id: Option, + pub owner_user_id: String, + pub profile_id: Option, + pub event_kind: String, + pub client_event_id: Option, + pub history_entry_id: Option, + pub payload: VisualNovelJsonValue, + pub occurred_at_micros: i64, +} + +impl __sdk::InModule for VisualNovelRuntimeEventSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_history_entry_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_history_entry_snapshot_type.rs new file mode 100644 index 00000000..af62143e --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_runtime_history_entry_snapshot_type.rs @@ -0,0 +1,27 @@ +// 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::visual_novel_json_value_type::VisualNovelJsonValue; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelRuntimeHistoryEntrySnapshot { + pub entry_id: String, + pub run_id: String, + pub owner_user_id: String, + pub profile_id: String, + pub turn_index: u32, + pub source: String, + pub action_text: Option, + pub steps: VisualNovelJsonValue, + pub snapshot_before_hash: Option, + pub snapshot_after_hash: Option, + pub created_at_micros: i64, +} + +impl __sdk::InModule for VisualNovelRuntimeHistoryEntrySnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_work_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_work_procedure_result_type.rs index f7e63a1c..72535982 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_work_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_work_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::visual_novel_work_snapshot_type::VisualNovelWorkSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct VisualNovelWorkProcedureResult { pub ok: bool, - pub work_json: Option, + pub work: Option, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_work_snapshot_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_work_snapshot_type.rs new file mode 100644 index 00000000..46b8180f --- /dev/null +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_work_snapshot_type.rs @@ -0,0 +1,33 @@ +// 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::visual_novel_json_value_type::VisualNovelJsonValue; + +#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] +#[sats(crate = __lib)] +pub struct VisualNovelWorkSnapshot { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub tags: Vec, + pub cover_image_src: Option, + pub source_asset_ids: Vec, + pub draft: VisualNovelJsonValue, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub created_at_micros: i64, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + +impl __sdk::InModule for VisualNovelWorkSnapshot { + type Module = super::RemoteModule; +} diff --git a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_works_procedure_result_type.rs b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_works_procedure_result_type.rs index 72558fcd..d6c89c1c 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_works_procedure_result_type.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/visual_novel_works_procedure_result_type.rs @@ -4,11 +4,13 @@ #![allow(unused, clippy::all)] use spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws}; +use super::visual_novel_work_snapshot_type::VisualNovelWorkSnapshot; + #[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)] #[sats(crate = __lib)] pub struct VisualNovelWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } diff --git a/server-rs/crates/spacetime-client/src/puzzle.rs b/server-rs/crates/spacetime-client/src/puzzle.rs index 1006e6f1..f6ddd839 100644 --- a/server-rs/crates/spacetime-client/src/puzzle.rs +++ b/server-rs/crates/spacetime-client/src/puzzle.rs @@ -5,45 +5,6 @@ use crate::module_bindings::delete_puzzle_work_procedure::delete_puzzle_work; use crate::module_bindings::record_puzzle_work_like_procedure::record_puzzle_work_like; use crate::module_bindings::remix_puzzle_work_procedure::remix_puzzle_work; use crate::module_bindings::save_puzzle_ui_background_procedure::save_puzzle_ui_background; -use std::collections::HashMap; -use std::time::{SystemTime, UNIX_EPOCH}; - -const PUBLIC_WORK_PLAY_DAY_MICROS: i64 = 86_400_000_000; -const PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS: i64 = 7; - -fn current_unix_micros() -> i64 { - SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|duration| duration.as_micros() as i64) - .unwrap_or(0) -} - -fn current_public_work_day() -> i64 { - current_unix_micros().div_euclid(PUBLIC_WORK_PLAY_DAY_MICROS) -} - -fn puzzle_gallery_recent_play_counts(connection: &DbConnection) -> HashMap { - let current_day = current_public_work_day(); - let first_day = current_day - (PUBLIC_WORK_RECENT_PLAY_WINDOW_DAYS - 1); - let mut counts = HashMap::new(); - - for row in connection - .db() - .public_work_play_daily_stat() - .iter() - { - if row.source_type != "puzzle" - || row.played_day < first_day - || row.played_day > current_day - { - continue; - } - let entry: &mut u32 = counts.entry(row.profile_id).or_insert(0); - *entry = (*entry).saturating_add(row.play_count); - } - - counts -} impl SpacetimeClient { pub async fn create_puzzle_agent_session( @@ -441,20 +402,28 @@ impl SpacetimeClient { pub async fn list_puzzle_gallery( &self, - ) -> Result, SpacetimeClientError> { - self.read_after_connect(move |connection| { - let mut items = connection.db().puzzle_gallery_view().iter().collect::>(); - items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros)); - let recent_play_counts = puzzle_gallery_recent_play_counts(connection); + ) -> Result, SpacetimeClientError> { + self.read_after_connect("list_puzzle_gallery", move |connection| { + let mut items = connection + .db() + .puzzle_gallery_card_view() + .iter() + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + let recent_play_counts = public_work_recent_play_counts(connection, "puzzle"); Ok(items .into_iter() .map(|item| { - let mut record = map_puzzle_work_profile(item.into()); - record.recent_play_count_7d = recent_play_counts - .get(&record.profile_id) + let recent_play_count_7d = recent_play_counts + .get(&item.profile_id) .copied() .unwrap_or(0); - record + map_puzzle_gallery_card_view_row(item, recent_play_count_7d) }) .collect()) }) diff --git a/server-rs/crates/spacetime-client/src/runtime.rs b/server-rs/crates/spacetime-client/src/runtime.rs index a4b3aa29..baac3495 100644 --- a/server-rs/crates/spacetime-client/src/runtime.rs +++ b/server-rs/crates/spacetime-client/src/runtime.rs @@ -3,6 +3,49 @@ use super::*; impl SpacetimeClient { pub async fn get_creation_entry_config( &self, + ) -> Result { + match self + .read_after_connect("get_creation_entry_config", move |connection| { + let config_id = module_runtime::CREATION_ENTRY_CONFIG_GLOBAL_ID.to_string(); + let header = connection + .db() + .creation_entry_config() + .config_id() + .find(&config_id) + .ok_or_else(|| SpacetimeClientError::missing_snapshot("创作入口配置快照"))?; + let creation_types = connection + .db() + .creation_entry_type_config() + .iter() + .collect::>(); + Ok(build_creation_entry_config_record_from_rows( + header, + creation_types, + )) + }) + .await + { + Ok(config) => { + self.cache_creation_entry_config(config.clone()).await; + Ok(config) + } + Err(_) => { + if let Some(config) = self.read_cached_creation_entry_config().await { + return Ok(config); + } + match self.fetch_creation_entry_config_via_procedure().await { + Ok(config) => { + self.cache_creation_entry_config(config.clone()).await; + Ok(config) + } + Err(fallback_error) => Err(fallback_error), + } + } + } + } + + async fn fetch_creation_entry_config_via_procedure( + &self, ) -> Result { self.call_after_connect("get_creation_entry_config", move |connection, sender| { connection @@ -22,20 +65,26 @@ impl SpacetimeClient { input: module_runtime::CreationEntryTypeAdminUpsertInput, ) -> Result { let procedure_input: CreationEntryTypeAdminUpsertInput = input.into(); - self.call_after_connect( - "upsert_creation_entry_type_config", - move |connection, sender| { - connection - .procedures() - .upsert_creation_entry_type_config_then(procedure_input, move |_, result| { - let mapped = result - .map_err(SpacetimeClientError::from_sdk_error) - .and_then(map_creation_entry_config_procedure_result); - send_once(&sender, mapped); - }); - }, - ) - .await + let config = self + .call_after_connect( + "upsert_creation_entry_type_config", + move |connection, sender| { + connection + .procedures() + .upsert_creation_entry_type_config_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(SpacetimeClientError::from_sdk_error) + .and_then(map_creation_entry_config_procedure_result); + send_once(&sender, mapped); + }, + ); + }, + ) + .await?; + self.cache_creation_entry_config(config.clone()).await; + Ok(config) } pub async fn get_runtime_settings( diff --git a/server-rs/crates/spacetime-client/src/square_hole.rs b/server-rs/crates/spacetime-client/src/square_hole.rs index ffeb616f..0b8e9e26 100644 --- a/server-rs/crates/spacetime-client/src/square_hole.rs +++ b/server-rs/crates/spacetime-client/src/square_hole.rs @@ -228,10 +228,22 @@ impl SpacetimeClient { pub async fn list_square_hole_gallery( &self, ) -> Result, SpacetimeClientError> { - self.list_square_hole_works_with_input(SquareHoleWorksListInput { - // 中文注释:公开广场只依赖 published_only,owner_user_id 用固定值通过输入校验。 - owner_user_id: "square-hole-public-gallery".to_string(), - published_only: true, + self.read_after_connect("list_square_hole_gallery", move |connection| { + let mut items = connection + .db() + .square_hole_gallery_view() + .iter() + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + Ok(items + .into_iter() + .map(map_square_hole_gallery_view_row) + .collect()) }) .await } diff --git a/server-rs/crates/spacetime-client/src/telemetry.rs b/server-rs/crates/spacetime-client/src/telemetry.rs index 9fdd9885..c89e0f19 100644 --- a/server-rs/crates/spacetime-client/src/telemetry.rs +++ b/server-rs/crates/spacetime-client/src/telemetry.rs @@ -10,6 +10,11 @@ pub(crate) struct ProcedureMetricsGuard { started_at: std::time::Instant, } +pub(crate) struct ReadMetricsGuard { + read: &'static str, + started_at: std::time::Instant, +} + pub(crate) fn begin_procedure(procedure: &'static str) -> ProcedureMetricsGuard { ProcedureMetricsGuard { procedure, @@ -17,6 +22,13 @@ pub(crate) fn begin_procedure(procedure: &'static str) -> ProcedureMetricsGuard } } +pub(crate) fn begin_read(read: &'static str) -> ReadMetricsGuard { + ReadMetricsGuard { + read, + started_at: std::time::Instant::now(), + } +} + impl ProcedureMetricsGuard { pub(crate) fn finish(&self, result: &Result) { let duration = self.started_at.elapsed(); @@ -24,10 +36,20 @@ impl ProcedureMetricsGuard { } } +impl ReadMetricsGuard { + pub(crate) fn finish(&self, result: &Result) { + let duration = self.started_at.elapsed(); + record_read(self.read, duration, result.is_err()); + } +} + struct SpacetimeMetrics { calls: Counter, errors: Counter, duration_ms: opentelemetry::metrics::Histogram, + read_calls: Counter, + read_errors: Counter, + read_duration_ms: opentelemetry::metrics::Histogram, } fn spacetime_metrics() -> &'static SpacetimeMetrics { @@ -48,21 +70,27 @@ fn spacetime_metrics() -> &'static SpacetimeMetrics { .with_unit("ms") .with_description("SpacetimeDB procedure duration in milliseconds") .build(), + read_calls: meter + .u64_counter("genarrative.spacetime.read.calls") + .with_description("SpacetimeDB local subscription cache read count") + .build(), + read_errors: meter + .u64_counter("genarrative.spacetime.read.errors") + .with_description("SpacetimeDB local subscription cache read error count") + .build(), + read_duration_ms: meter + .f64_histogram("genarrative.spacetime.read.duration_ms") + .with_unit("ms") + .with_description("SpacetimeDB local subscription cache read duration in milliseconds") + .build(), } }) } -fn record_procedure( - procedure: &'static str, - duration: Duration, - failed: bool, -) { +fn record_procedure(procedure: &'static str, duration: Duration, failed: bool) { let labels = vec![ KeyValue::new("procedure", procedure), - KeyValue::new( - "status_class", - if failed { "error" } else { "ok" }, - ), + KeyValue::new("status_class", if failed { "error" } else { "ok" }), ]; let metrics = spacetime_metrics(); metrics.calls.add(1, &labels); @@ -73,3 +101,18 @@ fn record_procedure( metrics.errors.add(1, &labels); } } + +fn record_read(read: &'static str, duration: Duration, failed: bool) { + let labels = vec![ + KeyValue::new("read", read), + KeyValue::new("status_class", if failed { "error" } else { "ok" }), + ]; + let metrics = spacetime_metrics(); + metrics.read_calls.add(1, &labels); + metrics + .read_duration_ms + .record(duration.as_secs_f64() * 1000.0, &labels); + if failed { + metrics.read_errors.add(1, &labels); + } +} diff --git a/server-rs/crates/spacetime-client/src/visual_novel.rs b/server-rs/crates/spacetime-client/src/visual_novel.rs index bbf7a00f..3454298f 100644 --- a/server-rs/crates/spacetime-client/src/visual_novel.rs +++ b/server-rs/crates/spacetime-client/src/visual_novel.rs @@ -6,9 +6,9 @@ use crate::mapper::{ VisualNovelRunSnapshotRecordInput, VisualNovelRunStartRecordInput, VisualNovelRuntimeEventRecord, VisualNovelWorkCompileRecordInput, VisualNovelWorkProfileRecord, VisualNovelWorkUpdateRecordInput, map_visual_novel_agent_session_procedure_result, - map_visual_novel_history_procedure_result, map_visual_novel_run_procedure_result, - map_visual_novel_runtime_event_procedure_result, map_visual_novel_work_procedure_result, - map_visual_novel_works_procedure_result, + map_visual_novel_gallery_view_row, map_visual_novel_history_procedure_result, + map_visual_novel_run_procedure_result, map_visual_novel_runtime_event_procedure_result, + map_visual_novel_work_procedure_result, map_visual_novel_works_procedure_result, }; impl SpacetimeClient { @@ -239,10 +239,22 @@ impl SpacetimeClient { pub async fn list_visual_novel_gallery( &self, ) -> Result, SpacetimeClientError> { - self.list_visual_novel_works_with_input(VisualNovelWorksListInput { - // 中文注释:公开列表只依赖 published_only,owner_user_id 用固定值满足 procedure 输入契约。 - owner_user_id: "visual-novel-public-gallery".to_string(), - published_only: true, + self.read_after_connect("list_visual_novel_gallery", move |connection| { + let mut items = connection + .db() + .visual_novel_gallery_view() + .iter() + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + Ok(items + .into_iter() + .map(map_visual_novel_gallery_view_row) + .collect()) }) .await } diff --git a/server-rs/crates/spacetime-module/src/ai/snapshots.rs b/server-rs/crates/spacetime-module/src/ai/snapshots.rs index f6a9284c..8ee4c0be 100644 --- a/server-rs/crates/spacetime-module/src/ai/snapshots.rs +++ b/server-rs/crates/spacetime-module/src/ai/snapshots.rs @@ -33,8 +33,8 @@ pub(crate) fn build_ai_task_snapshot_from_row( let mut stages = ctx .db .ai_task_stage() - .iter() - .filter(|stage| stage.task_id == row.task_id) + .by_ai_task_stage_task_id() + .filter(&row.task_id) .map(|stage| build_ai_task_stage_snapshot_from_row(&stage)) .collect::>(); stages.sort_by_key(|stage| stage.order); @@ -42,8 +42,8 @@ pub(crate) fn build_ai_task_snapshot_from_row( let mut result_references = ctx .db .ai_result_reference() - .iter() - .filter(|reference| reference.task_id == row.task_id) + .by_ai_result_reference_task_id() + .filter(&row.task_id) .map(|reference| build_ai_result_reference_snapshot_from_row(&reference)) .collect::>(); result_references.sort_by_key(|reference| reference.created_at_micros); diff --git a/server-rs/crates/spacetime-module/src/ai/stages.rs b/server-rs/crates/spacetime-module/src/ai/stages.rs index ed908daf..8ffea6f2 100644 --- a/server-rs/crates/spacetime-module/src/ai/stages.rs +++ b/server-rs/crates/spacetime-module/src/ai/stages.rs @@ -318,8 +318,8 @@ pub(crate) fn replace_ai_task_stages( let stage_ids = ctx .db .ai_task_stage() - .iter() - .filter(|row| row.task_id == task_id) + .by_ai_task_stage_task_id() + .filter(task_id) .map(|row| row.task_stage_id.clone()) .collect::>(); for stage_id in stage_ids { @@ -341,7 +341,8 @@ pub(crate) fn collect_ai_stage_text_output( let mut chunks = ctx .db .ai_text_chunk() - .iter() + .by_ai_text_chunk_task_id() + .filter(task_id) .filter(|row| row.task_id == task_id && row.stage_kind == stage_kind) .map(|row| build_ai_text_chunk_snapshot_from_row(&row)) .collect::>(); diff --git a/server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs b/server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs index 50a6c649..98bd9b4d 100644 --- a/server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs +++ b/server-rs/crates/spacetime-module/src/asset_metadata/bindings.rs @@ -66,12 +66,16 @@ fn upsert_asset_entity_binding( return Err("asset_entity_binding.asset_object_id 对应的 asset_object 不存在".to_string()); } - // 首版绑定按 entity_kind + entity_id + slot 幂等定位,后续访问量明确后再改为组合索引扫描。 - let current = ctx.db.asset_entity_binding().iter().find(|row| { - row.entity_kind == input.entity_kind - && row.entity_id == input.entity_id - && row.slot == input.slot - }); + let current = ctx + .db + .asset_entity_binding() + .by_entity_slot() + .filter(( + input.entity_kind.as_str(), + input.entity_id.as_str(), + input.slot.as_str(), + )) + .next(); let snapshot = match current { Some(existing) => { diff --git a/server-rs/crates/spacetime-module/src/asset_metadata/objects.rs b/server-rs/crates/spacetime-module/src/asset_metadata/objects.rs index e6650242..01cb9a38 100644 --- a/server-rs/crates/spacetime-module/src/asset_metadata/objects.rs +++ b/server-rs/crates/spacetime-module/src/asset_metadata/objects.rs @@ -128,12 +128,12 @@ pub(crate) fn upsert_asset_object( ) .map_err(|error| error.to_string())?; - // 这里先保持最小可发布实现:查重语义已经冻结,后续再把实现优化回组合索引扫描。 let current = ctx .db .asset_object() - .iter() - .find(|row| row.bucket == input.bucket && row.object_key == input.object_key); + .by_bucket_object_key() + .filter((input.bucket.as_str(), input.object_key.as_str())) + .next(); let snapshot = match current { Some(existing) => { @@ -196,8 +196,9 @@ pub(crate) fn upsert_asset_object( pub(crate) fn has_asset_object(ctx: &ReducerContext, asset_object_id: &str) -> bool { ctx.db .asset_object() - .iter() - .any(|row| row.asset_object_id == asset_object_id) + .asset_object_id() + .find(&asset_object_id.to_string()) + .is_some() } fn list_asset_history( @@ -224,8 +225,8 @@ fn list_asset_history( let mut entries = ctx .db .asset_object() - .iter() - .filter(|row| row.asset_kind == asset_kind) + .asset_kind() + .filter(&asset_kind.to_string()) .map(|row| AssetHistoryEntrySnapshot { asset_object_id: row.asset_object_id, asset_kind: row.asset_kind, diff --git a/server-rs/crates/spacetime-module/src/bark_battle/mod.rs b/server-rs/crates/spacetime-module/src/bark_battle/mod.rs index d4afb89b..2d104b7e 100644 --- a/server-rs/crates/spacetime-module/src/bark_battle/mod.rs +++ b/server-rs/crates/spacetime-module/src/bark_battle/mod.rs @@ -1,6 +1,5 @@ use crate::*; -use serde::Serialize; -use serde::de::DeserializeOwned; +use serde::{Serialize, de::DeserializeOwned}; use sha2::{Digest, Sha256}; pub(crate) mod tables; @@ -15,7 +14,7 @@ pub fn create_bark_battle_draft( input: BarkBattleDraftCreateInput, ) -> BarkBattleProcedureResult { match ctx.try_with_tx(|tx| create_bark_battle_draft_tx(tx, input.clone())) { - Ok(snapshot) => bark_battle_json_result(&snapshot), + Ok(snapshot) => bark_battle_draft_config_result(snapshot), Err(error) => bark_battle_error_result(error), } } @@ -26,7 +25,7 @@ pub fn update_bark_battle_draft_config( input: BarkBattleDraftConfigUpsertInput, ) -> BarkBattleProcedureResult { match ctx.try_with_tx(|tx| update_bark_battle_draft_config_tx(tx, input.clone())) { - Ok(snapshot) => bark_battle_json_result(&snapshot), + Ok(snapshot) => bark_battle_draft_config_result(snapshot), Err(error) => bark_battle_error_result(error), } } @@ -37,7 +36,7 @@ pub fn publish_bark_battle_work( input: BarkBattleWorkPublishInput, ) -> BarkBattleProcedureResult { match ctx.try_with_tx(|tx| publish_bark_battle_work_tx(tx, input.clone())) { - Ok(snapshot) => bark_battle_json_result(&snapshot), + Ok(snapshot) => bark_battle_runtime_config_result(snapshot), Err(error) => bark_battle_error_result(error), } } @@ -48,7 +47,7 @@ pub fn get_bark_battle_runtime_config( input: BarkBattleRuntimeConfigGetInput, ) -> BarkBattleProcedureResult { match ctx.try_with_tx(|tx| get_bark_battle_runtime_config_tx(tx, input.clone())) { - Ok(snapshot) => bark_battle_json_result(&snapshot), + Ok(snapshot) => bark_battle_runtime_config_result(snapshot), Err(error) => bark_battle_error_result(error), } } @@ -59,7 +58,7 @@ pub fn start_bark_battle_run( input: BarkBattleRunStartInput, ) -> BarkBattleProcedureResult { match ctx.try_with_tx(|tx| start_bark_battle_run_tx(tx, input.clone())) { - Ok(snapshot) => bark_battle_json_result(&snapshot), + Ok(snapshot) => bark_battle_run_result(snapshot), Err(error) => bark_battle_error_result(error), } } @@ -70,7 +69,7 @@ pub fn finish_bark_battle_run( input: BarkBattleRunFinishInput, ) -> BarkBattleProcedureResult { match ctx.try_with_tx(|tx| finish_bark_battle_run_tx(tx, input.clone())) { - Ok(snapshot) => bark_battle_json_result(&snapshot), + Ok(snapshot) => bark_battle_run_result(snapshot), Err(error) => bark_battle_error_result(error), } } @@ -81,7 +80,7 @@ pub fn get_bark_battle_run( input: BarkBattleRunGetInput, ) -> BarkBattleProcedureResult { match ctx.try_with_tx(|tx| get_bark_battle_run_tx(tx, input.clone())) { - Ok(snapshot) => bark_battle_json_result(&snapshot), + Ok(snapshot) => bark_battle_run_result(snapshot), Err(error) => bark_battle_error_result(error), } } @@ -584,10 +583,36 @@ fn validate_json(value: &str, field_name: &str) -> Result<( .map_err(|error| format!("bark_battle {field_name} JSON 无效: {error}")) } -fn bark_battle_json_result(value: &T) -> BarkBattleProcedureResult { +fn bark_battle_draft_config_result( + draft_config: BarkBattleDraftConfigSnapshot, +) -> BarkBattleProcedureResult { BarkBattleProcedureResult { ok: true, - row_json: Some(to_json_string(value)), + draft_config: Some(draft_config), + runtime_config: None, + run: None, + error_message: None, + } +} + +fn bark_battle_runtime_config_result( + runtime_config: BarkBattleRuntimeConfigSnapshot, +) -> BarkBattleProcedureResult { + BarkBattleProcedureResult { + ok: true, + draft_config: None, + runtime_config: Some(runtime_config), + run: None, + error_message: None, + } +} + +fn bark_battle_run_result(run: BarkBattleRunSnapshot) -> BarkBattleProcedureResult { + BarkBattleProcedureResult { + ok: true, + draft_config: None, + runtime_config: None, + run: Some(run), error_message: None, } } @@ -595,7 +620,9 @@ fn bark_battle_json_result(value: &T) -> BarkBattleProcedureResult fn bark_battle_error_result(error: String) -> BarkBattleProcedureResult { BarkBattleProcedureResult { ok: false, - row_json: None, + draft_config: None, + runtime_config: None, + run: None, error_message: Some(error), } } @@ -850,7 +877,21 @@ mod tests { let result = BarkBattleProcedureResult { ok: true, - row_json: Some(input.config_json.clone()), + draft_config: Some(BarkBattleDraftConfigSnapshot { + draft_id: input.draft_id.clone(), + owner_user_id: input.owner_user_id.clone(), + work_id: input.work_id.clone(), + config_version: input.config_version, + ruleset_version: input.ruleset_version.clone(), + difficulty_preset: input.difficulty_preset.clone(), + leaderboard_enabled: input.leaderboard_enabled, + config_json: input.config_json.clone(), + editor_state_json: "{}".to_string(), + created_at_micros: 1_700_000, + updated_at_micros: input.updated_at_micros, + }), + runtime_config: None, + run: None, error_message: None, }; diff --git a/server-rs/crates/spacetime-module/src/bark_battle/types.rs b/server-rs/crates/spacetime-module/src/bark_battle/types.rs index e26a2747..a1652d78 100644 --- a/server-rs/crates/spacetime-module/src/bark_battle/types.rs +++ b/server-rs/crates/spacetime-module/src/bark_battle/types.rs @@ -102,14 +102,16 @@ pub struct BarkBattleRunGetInput { pub owner_user_id: String, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct BarkBattleProcedureResult { pub ok: bool, - pub row_json: Option, + pub draft_config: Option, + pub runtime_config: Option, + pub run: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct BarkBattleEditorConfigSnapshot { pub title: String, @@ -121,7 +123,7 @@ pub struct BarkBattleEditorConfigSnapshot { pub leaderboard_enabled: bool, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct BarkBattleDraftConfigSnapshot { pub draft_id: String, @@ -137,7 +139,7 @@ pub struct BarkBattleDraftConfigSnapshot { pub updated_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct BarkBattleRuntimeConfigSnapshot { pub work_id: String, @@ -153,7 +155,7 @@ pub struct BarkBattleRuntimeConfigSnapshot { pub updated_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct BarkBattleRunSnapshot { pub run_id: String, diff --git a/server-rs/crates/spacetime-module/src/big_fish/assets.rs b/server-rs/crates/spacetime-module/src/big_fish/assets.rs index 2f0f1fa4..1da68d3c 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/assets.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/assets.rs @@ -222,8 +222,8 @@ pub(crate) fn list_big_fish_asset_slots( let mut slots = ctx .db .big_fish_asset_slot() - .iter() - .filter(|slot| slot.session_id == session_id) + .by_big_fish_asset_session_id() + .filter(&session_id.to_string()) .map(|slot| BigFishAssetSlotSnapshot { slot_id: slot.slot_id, session_id: slot.session_id, diff --git a/server-rs/crates/spacetime-module/src/big_fish/runtime.rs b/server-rs/crates/spacetime-module/src/big_fish/runtime.rs index fefdadd4..6e0f56f8 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/runtime.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/runtime.rs @@ -16,12 +16,12 @@ pub fn start_big_fish_run( match ctx.try_with_tx(|tx| start_big_fish_run_tx(tx, input.clone())) { Ok(run) => BigFishRunProcedureResult { ok: true, - run_json: Some(serialize_big_fish_run_json(&run)), + run: Some(run), error_message: None, }, Err(message) => BigFishRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -35,12 +35,12 @@ pub fn get_big_fish_run( match ctx.try_with_tx(|tx| get_big_fish_run_tx(tx, input.clone())) { Ok(run) => BigFishRunProcedureResult { ok: true, - run_json: Some(serialize_big_fish_run_json(&run)), + run: Some(run), error_message: None, }, Err(message) => BigFishRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -54,12 +54,12 @@ pub fn submit_big_fish_input( match ctx.try_with_tx(|tx| submit_big_fish_input_tx(tx, input.clone())) { Ok(run) => BigFishRunProcedureResult { ok: true, - run_json: Some(serialize_big_fish_run_json(&run)), + run: Some(run), error_message: None, }, Err(message) => BigFishRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -225,7 +225,3 @@ fn replace_big_fish_runtime_run( }); Ok(()) } - -fn serialize_big_fish_run_json(run: &BigFishRuntimeSnapshot) -> String { - serialize_runtime_snapshot(run).unwrap_or_else(|_| "{}".to_string()) -} diff --git a/server-rs/crates/spacetime-module/src/big_fish/session.rs b/server-rs/crates/spacetime-module/src/big_fish/session.rs index b7815e0a..5ae78d2a 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/session.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/session.rs @@ -8,9 +8,42 @@ use crate::runtime::{ }; use crate::*; use module_big_fish::{EvaluateBigFishPublishReadinessCommand, evaluate_publish_readiness}; +use spacetimedb::AnonymousViewContext; const INITIAL_BIG_FISH_CREATION_PROGRESS_PERCENT: u32 = 0; +/// 大鱼吃小鱼公开广场列表投影。 +/// +/// 公开列表从已发布 creation session 生成卡片字段;7 日播放数由 +/// `api-server` 订阅 `public_work_play_daily_stat` 后在本地聚合。 +#[spacetimedb::view(accessor = big_fish_gallery_view, public)] +pub fn big_fish_gallery_view(ctx: &AnonymousViewContext) -> Vec { + let mut items = ctx + .db + .big_fish_creation_session() + .by_big_fish_session_stage() + .filter(BigFishCreationStage::Published) + .filter_map(|row| match build_big_fish_gallery_view_row(ctx, &row) { + Ok(snapshot) => Some(snapshot), + Err(error) => { + log::warn!( + "大鱼吃小鱼公开广场 view 跳过损坏的作品投影 session_id={}: {}", + row.session_id, + error + ); + None + } + }) + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.source_session_id.cmp(&right.source_session_id)) + }); + items +} + #[spacetimedb::procedure] pub fn create_big_fish_session( ctx: &mut ProcedureContext, @@ -55,21 +88,14 @@ pub fn list_big_fish_works( input: BigFishWorksListInput, ) -> BigFishWorksProcedureResult { match ctx.try_with_tx(|tx| list_big_fish_works_tx(tx, input.clone())) { - Ok(items) => match serde_json::to_string(&items) { - Ok(items_json) => BigFishWorksProcedureResult { - ok: true, - items_json: Some(items_json), - error_message: None, - }, - Err(error) => BigFishWorksProcedureResult { - ok: false, - items_json: None, - error_message: Some(error.to_string()), - }, + Ok(items) => BigFishWorksProcedureResult { + ok: true, + items, + error_message: None, }, Err(message) => BigFishWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -81,21 +107,14 @@ pub fn delete_big_fish_work( input: BigFishWorkDeleteInput, ) -> BigFishWorksProcedureResult { match ctx.try_with_tx(|tx| delete_big_fish_work_tx(tx, input.clone())) { - Ok(items) => match serde_json::to_string(&items) { - Ok(items_json) => BigFishWorksProcedureResult { - ok: true, - items_json: Some(items_json), - error_message: None, - }, - Err(error) => BigFishWorksProcedureResult { - ok: false, - items_json: None, - error_message: Some(error.to_string()), - }, + Ok(items) => BigFishWorksProcedureResult { + ok: true, + items, + error_message: None, }, Err(message) => BigFishWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -107,21 +126,14 @@ pub fn record_big_fish_play( input: BigFishPlayRecordInput, ) -> BigFishWorksProcedureResult { match ctx.try_with_tx(|tx| record_big_fish_play_tx(tx, input.clone())) { - Ok(items) => match serde_json::to_string(&items) { - Ok(items_json) => BigFishWorksProcedureResult { - ok: true, - items_json: Some(items_json), - error_message: None, - }, - Err(error) => BigFishWorksProcedureResult { - ok: false, - items_json: None, - error_message: Some(error.to_string()), - }, + Ok(items) => BigFishWorksProcedureResult { + ok: true, + items, + error_message: None, }, Err(message) => BigFishWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -133,21 +145,14 @@ pub fn record_big_fish_like( input: BigFishWorkLikeRecordInput, ) -> BigFishWorksProcedureResult { match ctx.try_with_tx(|tx| record_big_fish_like_tx(tx, input.clone())) { - Ok(items) => match serde_json::to_string(&items) { - Ok(items_json) => BigFishWorksProcedureResult { - ok: true, - items_json: Some(items_json), - error_message: None, - }, - Err(error) => BigFishWorksProcedureResult { - ok: false, - items_json: None, - error_message: Some(error.to_string()), - }, + Ok(items) => BigFishWorksProcedureResult { + ok: true, + items, + error_message: None, }, Err(message) => BigFishWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -321,16 +326,20 @@ pub(crate) fn list_big_fish_works_tx( validate_works_list_input(&input).map_err(|error| error.to_string())?; let now_micros = ctx.timestamp.to_micros_since_unix_epoch(); - let mut items = ctx + let rows = ctx .db .big_fish_creation_session() + .by_big_fish_session_owner_user_id() + .filter(&input.owner_user_id) + .collect::>(); + let mut items = rows .iter() .filter(|row| { if input.published_only { return row.stage == BigFishCreationStage::Published; } - row.owner_user_id == input.owner_user_id && should_include_big_fish_work(ctx, row) + should_include_big_fish_work(ctx, row) }) .map(|row| build_big_fish_work_summary(ctx, &row, now_micros)) .collect::, _>>()?; @@ -349,10 +358,11 @@ fn should_include_big_fish_work(ctx: &ReducerContext, row: &BigFishCreationSessi return true; } - ctx.db.big_fish_agent_message().iter().any(|message| { - message.session_id == row.session_id - && matches!(message.role, BigFishAgentMessageRole::User) - }) + ctx.db + .big_fish_agent_message() + .by_big_fish_message_session_id() + .filter(&row.session_id) + .any(|message| matches!(message.role, BigFishAgentMessageRole::User)) } fn big_fish_session_has_direct_work_content(row: &BigFishCreationSession) -> bool { @@ -387,8 +397,8 @@ pub(crate) fn delete_big_fish_work_tx( for message in ctx .db .big_fish_agent_message() - .iter() - .filter(|row| row.session_id == input.session_id) + .by_big_fish_message_session_id() + .filter(&input.session_id) .collect::>() { ctx.db @@ -399,8 +409,8 @@ pub(crate) fn delete_big_fish_work_tx( for slot in ctx .db .big_fish_asset_slot() - .iter() - .filter(|row| row.session_id == input.session_id) + .by_big_fish_asset_session_id() + .filter(&input.session_id) .collect::>() { ctx.db.big_fish_asset_slot().slot_id().delete(&slot.slot_id); @@ -408,8 +418,8 @@ pub(crate) fn delete_big_fish_work_tx( for run in ctx .db .big_fish_runtime_run() - .iter() - .filter(|row| row.session_id == input.session_id) + .by_big_fish_run_session_id() + .filter(&input.session_id) .collect::>() { ctx.db.big_fish_runtime_run().run_id().delete(&run.run_id); @@ -952,8 +962,8 @@ pub(crate) fn build_big_fish_session_snapshot( let mut messages = ctx .db .big_fish_agent_message() - .iter() - .filter(|message| message.session_id == row.session_id) + .by_big_fish_message_session_id() + .filter(&row.session_id) .map(|message| BigFishAgentMessageSnapshot { message_id: message.message_id, session_id: message.session_id, @@ -988,6 +998,16 @@ pub(crate) fn build_big_fish_work_summary( ctx: &ReducerContext, row: &BigFishCreationSession, now_micros: i64, +) -> Result { + let mut summary = build_big_fish_work_summary_without_recent_count(ctx, row)?; + summary.recent_play_count_7d = + count_recent_public_work_plays(ctx, "big-fish", &row.session_id, now_micros); + Ok(summary) +} + +fn build_big_fish_work_summary_without_recent_count( + ctx: &ReducerContext, + row: &BigFishCreationSession, ) -> Result { let draft = row .draft_json @@ -1052,12 +1072,7 @@ pub(crate) fn build_big_fish_work_summary( play_count: row.play_count, remix_count: row.remix_count, like_count: row.like_count, - recent_play_count_7d: count_recent_public_work_plays( - ctx, - "big-fish", - &row.session_id, - now_micros, - ), + recent_play_count_7d: 0, published_at_micros: row .published_at .or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at)) @@ -1065,6 +1080,113 @@ pub(crate) fn build_big_fish_work_summary( }) } +fn build_big_fish_gallery_view_row( + ctx: &AnonymousViewContext, + row: &BigFishCreationSession, +) -> Result { + let draft = row + .draft_json + .as_deref() + .map(deserialize_draft) + .transpose() + .map_err(|error| format!("big_fish.draft_json 非法: {error}"))?; + let asset_slots = list_big_fish_asset_slots_for_view(ctx, &row.session_id); + let coverage = build_asset_coverage(draft.as_ref(), &asset_slots); + let cover_image_src = asset_slots + .iter() + .find(|slot| slot.asset_kind == BigFishAssetKind::StageBackground) + .and_then(|slot| slot.asset_url.clone()) + .or_else(|| { + asset_slots + .iter() + .find(|slot| slot.asset_kind == BigFishAssetKind::LevelMainImage) + .and_then(|slot| slot.asset_url.clone()) + }); + let title = draft + .as_ref() + .map(|value| value.title.clone()) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| "未命名大鱼草稿".to_string()); + let subtitle = draft + .as_ref() + .map(|value| value.subtitle.clone()) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| "等待整理玩法草稿".to_string()); + let summary = draft + .as_ref() + .map(|value| value.core_fun.clone()) + .filter(|value| !value.trim().is_empty()) + .unwrap_or_else(|| { + row.last_assistant_reply + .clone() + .unwrap_or_else(|| "继续补齐锚点后即可生成玩法草稿。".to_string()) + }); + + Ok(BigFishWorkSummarySnapshot { + work_id: format!("big-fish-work-{}", row.session_id), + source_session_id: row.session_id.clone(), + owner_user_id: row.owner_user_id.clone(), + title, + subtitle, + summary, + cover_image_src, + status: if row.stage == BigFishCreationStage::Published { + "published".to_string() + } else { + "draft".to_string() + }, + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + publish_ready: coverage.publish_ready, + level_count: draft + .as_ref() + .map(|value| value.runtime_params.level_count) + .unwrap_or(BIG_FISH_DEFAULT_LEVEL_COUNT), + level_main_image_ready_count: coverage.level_main_image_ready_count, + level_motion_ready_count: coverage.level_motion_ready_count, + background_ready: coverage.background_ready, + play_count: row.play_count, + remix_count: row.remix_count, + like_count: row.like_count, + recent_play_count_7d: 0, + published_at_micros: row + .published_at + .or_else(|| (row.stage == BigFishCreationStage::Published).then_some(row.updated_at)) + .map(|value| value.to_micros_since_unix_epoch()), + }) +} + +fn list_big_fish_asset_slots_for_view( + ctx: &AnonymousViewContext, + session_id: &str, +) -> Vec { + let mut slots = ctx + .db + .big_fish_asset_slot() + .by_big_fish_asset_session_id() + .filter(session_id) + .map(|slot| BigFishAssetSlotSnapshot { + slot_id: slot.slot_id, + session_id: slot.session_id, + asset_kind: slot.asset_kind, + level: slot.level, + motion_key: slot.motion_key, + status: slot.status, + asset_url: slot.asset_url, + prompt_snapshot: slot.prompt_snapshot, + updated_at_micros: slot.updated_at.to_micros_since_unix_epoch(), + }) + .collect::>(); + slots.sort_by_key(|slot| { + ( + slot.level.unwrap_or(0), + slot.asset_kind.as_str().to_string(), + slot.motion_key.clone().unwrap_or_default(), + slot.slot_id.clone(), + ) + }); + slots +} + fn build_public_big_fish_gallery_list_input() -> BigFishWorksListInput { BigFishWorksListInput { // 中文注释:published_only 分支不会按 owner 过滤;非空占位用于兼容旧部署模块的前置校验。 diff --git a/server-rs/crates/spacetime-module/src/big_fish/tables.rs b/server-rs/crates/spacetime-module/src/big_fish/tables.rs index 997371e8..fa480120 100644 --- a/server-rs/crates/spacetime-module/src/big_fish/tables.rs +++ b/server-rs/crates/spacetime-module/src/big_fish/tables.rs @@ -2,7 +2,8 @@ use crate::*; #[spacetimedb::table( accessor = big_fish_creation_session, - index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])) + index(accessor = by_big_fish_session_owner_user_id, btree(columns = [owner_user_id])), + index(accessor = by_big_fish_session_stage, btree(columns = [stage])) )] pub struct BigFishCreationSession { #[primary_key] diff --git a/server-rs/crates/spacetime-module/src/custom_world/mod.rs b/server-rs/crates/spacetime-module/src/custom_world/mod.rs index da42008c..36228bfe 100644 --- a/server-rs/crates/spacetime-module/src/custom_world/mod.rs +++ b/server-rs/crates/spacetime-module/src/custom_world/mod.rs @@ -436,7 +436,8 @@ fn delete_custom_world_agent_session_tx( let published_profile = ctx .db .custom_world_profile() - .iter() + .by_custom_world_profile_owner_user_id() + .filter(&input.owner_user_id) .find(|row| { row.owner_user_id == input.owner_user_id && row.source_agent_session_id.as_deref() == Some(input.session_id.as_str()) @@ -471,8 +472,8 @@ fn delete_custom_world_agent_session_tx( for message in ctx .db .custom_world_agent_message() - .iter() - .filter(|row| row.session_id == input.session_id) + .by_custom_world_agent_message_session_id() + .filter(&input.session_id) .collect::>() { ctx.db @@ -483,8 +484,8 @@ fn delete_custom_world_agent_session_tx( for operation in ctx .db .custom_world_agent_operation() - .iter() - .filter(|row| row.session_id == input.session_id) + .by_custom_world_agent_operation_session_id() + .filter(&input.session_id) .collect::>() { ctx.db @@ -495,8 +496,8 @@ fn delete_custom_world_agent_session_tx( for card in ctx .db .custom_world_draft_card() - .iter() - .filter(|row| row.session_id == input.session_id) + .by_custom_world_draft_card_session_id() + .filter(&input.session_id) .collect::>() { ctx.db @@ -1184,9 +1185,17 @@ fn upsert_custom_world_profile_record( .source_agent_session_id .as_ref() .and_then(|session_id| { - ctx.db.custom_world_profile().iter().find(|row| { - is_same_agent_draft_profile_candidate(row, &input.owner_user_id, session_id) - }) + ctx.db + .custom_world_profile() + .by_custom_world_profile_owner_user_id() + .filter(&input.owner_user_id) + .find(|row| { + is_same_agent_draft_profile_candidate( + row, + &input.owner_user_id, + session_id, + ) + }) }) }); @@ -1534,8 +1543,9 @@ fn list_custom_world_profile_snapshots( let mut entries = ctx .db .custom_world_profile() - .iter() - .filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none()) + .by_custom_world_profile_owner_user_id() + .filter(&input.owner_user_id) + .filter(|row| row.deleted_at.is_none()) .map(|row| build_custom_world_profile_snapshot(&row)) .collect::>(); @@ -1676,8 +1686,9 @@ fn get_custom_world_gallery_detail_record_by_code( let gallery_entry = ctx .db .custom_world_gallery_entry() - .iter() - .find(|row| row.public_work_code == normalized_public_work_code); + .by_custom_world_gallery_public_work_code() + .filter(&normalized_public_work_code) + .next(); let profile = gallery_entry.as_ref().and_then(|row| { ctx.db @@ -1974,9 +1985,14 @@ fn list_custom_world_work_snapshots( let mut items = Vec::new(); let mut active_agent_session_ids = HashSet::new(); - for session in ctx.db.custom_world_agent_session().iter().filter(|row| { - row.owner_user_id == input.owner_user_id - && row.stage != RpgAgentStage::Published + let sessions = ctx + .db + .custom_world_agent_session() + .by_custom_world_agent_session_owner_user_id() + .filter(&input.owner_user_id) + .collect::>(); + for session in sessions.iter().filter(|row| { + row.stage != RpgAgentStage::Published && should_include_custom_world_agent_session_work(ctx, row) }) { active_agent_session_ids.insert(session.session_id.clone()); @@ -2021,8 +2037,9 @@ fn list_custom_world_work_snapshots( for profile in ctx .db .custom_world_profile() - .iter() - .filter(|row| row.owner_user_id == input.owner_user_id && row.deleted_at.is_none()) + .by_custom_world_profile_owner_user_id() + .filter(&input.owner_user_id) + .filter(|row| row.deleted_at.is_none()) .filter(|row| should_include_custom_world_profile_work(row, &active_agent_session_ids)) { items.push(CustomWorldWorkSummarySnapshot { @@ -2086,16 +2103,20 @@ fn should_include_custom_world_agent_session_work( return true; } - if ctx.db.custom_world_agent_message().iter().any(|message| { - message.session_id == session.session_id - && matches!(message.role, RpgAgentMessageRole::User) - }) { + if ctx + .db + .custom_world_agent_message() + .by_custom_world_agent_message_session_id() + .filter(&session.session_id) + .any(|message| matches!(message.role, RpgAgentMessageRole::User)) + { return true; } ctx.db .custom_world_draft_card() - .iter() + .by_custom_world_draft_card_session_id() + .filter(&session.session_id) .any(|card| card.session_id == session.session_id) } @@ -3446,10 +3467,12 @@ fn update_role_asset_cards( label: &str, updated_at_micros: i64, ) { - for card in - ctx.db.custom_world_draft_card().iter().filter(|row| { - row.session_id == session_id && row.kind == RpgAgentDraftCardKind::Character - }) + for card in ctx + .db + .custom_world_draft_card() + .by_custom_world_draft_card_session_id() + .filter(&session_id.to_string()) + .filter(|row| row.kind == RpgAgentDraftCardKind::Character) { replace_custom_world_draft_card( ctx, @@ -4590,8 +4613,8 @@ fn resolve_session_work_counts( for card in ctx .db .custom_world_draft_card() - .iter() - .filter(|row| row.session_id == session.session_id) + .by_custom_world_draft_card_session_id() + .filter(&session.session_id) { match card.kind { RpgAgentDraftCardKind::Character => { @@ -4827,11 +4850,9 @@ fn sync_missing_custom_world_gallery_entries(ctx: &ReducerContext) -> Result<(), let published_profiles = ctx .db .custom_world_profile() - .iter() - .filter(|profile| { - profile.publication_status == CustomWorldPublicationStatus::Published - && profile.deleted_at.is_none() - }) + .by_custom_world_profile_publication_status() + .filter(CustomWorldPublicationStatus::Published) + .filter(|profile| profile.deleted_at.is_none()) .collect::>(); for profile in published_profiles { @@ -4973,8 +4994,8 @@ fn build_custom_world_agent_session_snapshot( let mut messages = ctx .db .custom_world_agent_message() - .iter() - .filter(|message| message.session_id == row.session_id) + .by_custom_world_agent_message_session_id() + .filter(&row.session_id) .map(|message| build_custom_world_agent_message_snapshot(&message)) .collect::>(); messages.sort_by_key(|message| (message.created_at_micros, message.message_id.clone())); @@ -4982,8 +5003,8 @@ fn build_custom_world_agent_session_snapshot( let mut draft_cards = ctx .db .custom_world_draft_card() - .iter() - .filter(|card| card.session_id == row.session_id) + .by_custom_world_draft_card_session_id() + .filter(&row.session_id) .map(|card| build_custom_world_draft_card_snapshot(&card)) .collect::>(); draft_cards.sort_by_key(|card| (card.created_at_micros, card.card_id.clone())); @@ -4991,8 +5012,8 @@ fn build_custom_world_agent_session_snapshot( let mut operations = ctx .db .custom_world_agent_operation() - .iter() - .filter(|operation| operation.session_id == row.session_id) + .by_custom_world_agent_operation_session_id() + .filter(&row.session_id) .map(|operation| build_custom_world_agent_operation_snapshot(&operation)) .collect::>(); operations diff --git a/server-rs/crates/spacetime-module/src/gameplay/mod.rs b/server-rs/crates/spacetime-module/src/gameplay/mod.rs index db62e53a..5202342f 100644 --- a/server-rs/crates/spacetime-module/src/gameplay/mod.rs +++ b/server-rs/crates/spacetime-module/src/gameplay/mod.rs @@ -415,11 +415,9 @@ fn apply_inventory_mutation_tx( let current_slots = ctx .db .inventory_slot() - .iter() - .filter(|slot| { - slot.runtime_session_id == input.runtime_session_id - && slot.actor_user_id == input.actor_user_id - }) + .by_inventory_runtime_session_id() + .filter(&input.runtime_session_id) + .filter(|slot| slot.actor_user_id == input.actor_user_id) .map(|row| build_inventory_slot_snapshot_from_row(&row)) .collect::>(); @@ -587,11 +585,9 @@ fn get_runtime_inventory_state_tx( let slots = ctx .db .inventory_slot() - .iter() - .filter(|row| { - row.runtime_session_id == validated_input.runtime_session_id - && row.actor_user_id == validated_input.actor_user_id - }) + .by_inventory_runtime_session_id() + .filter(&validated_input.runtime_session_id) + .filter(|row| row.actor_user_id == validated_input.actor_user_id) .map(|row| build_inventory_slot_snapshot_from_row(&row)) .collect::>(); @@ -926,8 +922,8 @@ fn get_story_session_state_tx( let mut events = ctx .db .story_event() - .iter() - .filter(|row| row.story_session_id == input.story_session_id) + .by_story_session_id() + .filter(&input.story_session_id) .map(|row| build_story_event_snapshot_from_row(&row)) .collect::>(); events.sort_by_key(|event| (event.created_at_micros, event.event_id.clone())); @@ -1439,11 +1435,9 @@ fn inventory_reward_source_already_granted( ctx.db .inventory_slot() - .iter() - .filter(|row| { - row.runtime_session_id == first_mutation.runtime_session_id - && row.actor_user_id == first_mutation.actor_user_id - }) + .by_inventory_runtime_session_id() + .filter(&first_mutation.runtime_session_id) + .filter(|row| row.actor_user_id == first_mutation.actor_user_id) .any(|row| row.source_reference_id.as_deref() == Some(source_reference_id)) } diff --git a/server-rs/crates/spacetime-module/src/match3d/mod.rs b/server-rs/crates/spacetime-module/src/match3d/mod.rs index 70a38de2..b4154fc2 100644 --- a/server-rs/crates/spacetime-module/src/match3d/mod.rs +++ b/server-rs/crates/spacetime-module/src/match3d/mod.rs @@ -19,6 +19,62 @@ use module_match3d::{ use serde::Serialize; use serde::de::DeserializeOwned; use serde_json::Value; +use spacetimedb::AnonymousViewContext; + +/// 抓大鹅公开广场列表投影。 +/// +/// `match3d_work_profile` 是玩法源表,HTTP gallery 只订阅这个轻量 view, +/// 避免每个公开列表请求重新调用 procedure 扫描和组装全量列表。 +#[spacetimedb::view(accessor = match3d_gallery_view, public)] +pub fn match3d_gallery_view(ctx: &AnonymousViewContext) -> Vec { + let mut items = ctx + .db + .match3d_work_profile() + .by_match3d_work_publication_status() + .filter(MATCH3D_PUBLICATION_PUBLISHED) + .filter_map(|row| match build_gallery_view_row(&row) { + Ok(item) => Some(item), + Err(error) => { + log::warn!( + "抓大鹅公开广场 view 跳过损坏的作品投影 profile_id={}: {}", + row.profile_id, + error + ); + None + } + }) + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + items +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct Match3DGalleryViewRow { + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub game_name: String, + pub theme_text: String, + pub summary_text: String, + pub tags: Vec, + pub cover_image_src: String, + pub cover_asset_id: String, + pub reference_image_src: Option, + pub clear_count: u32, + pub difficulty: u32, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub generated_item_assets_json: Option, +} #[spacetimedb::procedure] pub fn create_match3d_agent_session( @@ -105,12 +161,12 @@ pub fn list_match3d_works( match ctx.try_with_tx(|tx| list_match3d_works_tx(tx, input.clone())) { Ok(items) => Match3DWorksProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => Match3DWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -135,12 +191,12 @@ pub fn delete_match3d_work( match ctx.try_with_tx(|tx| delete_match3d_work_tx(tx, input.clone())) { Ok(items) => Match3DWorksProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => Match3DWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -178,7 +234,7 @@ pub fn click_match3d_item( Err(message) => Match3DClickItemProcedureResult { ok: false, status: MATCH3D_CLICK_REJECTED_NOT_CLICKABLE.to_string(), - run_json: None, + run: None, accepted_item_instance_id: None, cleared_item_instance_ids: Vec::new(), failure_reason: None, @@ -634,17 +690,22 @@ fn list_match3d_works_tx( ctx: &ReducerContext, input: Match3DWorksListInput, ) -> Result, String> { - let mut items = ctx - .db - .match3d_work_profile() + let rows = if input.published_only { + ctx.db + .match3d_work_profile() + .by_match3d_work_publication_status() + .filter(&MATCH3D_PUBLICATION_PUBLISHED.to_string()) + .collect::>() + } else { + require_non_empty(&input.owner_user_id, "match3d owner_user_id")?; + ctx.db + .match3d_work_profile() + .by_match3d_work_owner_user_id() + .filter(&input.owner_user_id) + .collect::>() + }; + let mut items = rows .iter() - .filter(|row| { - if input.published_only { - row.publication_status == MATCH3D_PUBLICATION_PUBLISHED - } else { - row.owner_user_id == input.owner_user_id - } - }) .map(|row| build_work_snapshot(&row)) .collect::, _>>()?; items.sort_by(|left, right| { @@ -685,10 +746,9 @@ fn delete_match3d_work_tx( for run in ctx .db .match3d_runtime_run() - .iter() - .filter(|row| { - row.profile_id == input.profile_id && row.owner_user_id == input.owner_user_id - }) + .by_match3d_run_profile_id() + .filter(&input.profile_id) + .filter(|row| row.owner_user_id == input.owner_user_id) .collect::>() { ctx.db.match3d_runtime_run().run_id().delete(&run.run_id); @@ -931,8 +991,8 @@ fn build_session_snapshot( let mut messages = ctx .db .match3d_agent_message() - .iter() - .filter(|message| message.session_id == row.session_id) + .by_match3d_agent_message_session_id() + .filter(&row.session_id) .map(|message| Match3DAgentMessageSnapshot { message_id: message.message_id, session_id: message.session_id, @@ -1004,6 +1064,35 @@ fn build_work_snapshot(row: &Match3DWorkProfileRow) -> Result Result { + let config = parse_config(&row.config_json)?; + Ok(Match3DGalleryViewRow { + profile_id: row.profile_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + game_name: row.game_name.clone(), + theme_text: row.theme_text.clone(), + summary_text: row.summary_text.clone(), + tags: parse_tags(&row.tags_json)?, + cover_image_src: row.cover_image_src.clone(), + cover_asset_id: row.cover_asset_id.clone(), + reference_image_src: config.reference_image_src, + clear_count: row.clear_count, + difficulty: row.difficulty, + publication_status: row.publication_status.clone(), + publish_ready: is_work_publish_ready(row), + play_count: row.play_count, + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + published_at_micros: row + .published_at + .map(|value| value.to_micros_since_unix_epoch()), + generated_item_assets_json: normalize_generated_item_assets_json( + row.generated_item_assets_json.as_deref(), + )?, + }) +} + fn build_initial_run_snapshot( run_id: &str, work: &Match3DWorkProfileRow, @@ -1156,10 +1245,10 @@ fn click_result( Match3DClickItemProcedureResult { ok: true, status: status.to_string(), - run_json: Some(to_json_string(&snapshot)), + failure_reason: snapshot.failure_reason.clone(), + run: Some(snapshot), accepted_item_instance_id, cleared_item_instance_ids, - failure_reason: snapshot.failure_reason, error_message: None, } } @@ -1717,7 +1806,7 @@ fn to_json_string(value: &T) -> String { fn session_result(session: Match3DAgentSessionSnapshot) -> Match3DAgentSessionProcedureResult { Match3DAgentSessionProcedureResult { ok: true, - session_json: Some(to_json_string(&session)), + session: Some(session), error_message: None, } } @@ -1725,7 +1814,7 @@ fn session_result(session: Match3DAgentSessionSnapshot) -> Match3DAgentSessionPr fn session_error(message: String) -> Match3DAgentSessionProcedureResult { Match3DAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), } } @@ -1733,7 +1822,7 @@ fn session_error(message: String) -> Match3DAgentSessionProcedureResult { fn work_result(work: Match3DWorkSnapshot) -> Match3DWorkProcedureResult { Match3DWorkProcedureResult { ok: true, - work_json: Some(to_json_string(&work)), + work: Some(work), error_message: None, } } @@ -1741,7 +1830,7 @@ fn work_result(work: Match3DWorkSnapshot) -> Match3DWorkProcedureResult { fn work_error(message: String) -> Match3DWorkProcedureResult { Match3DWorkProcedureResult { ok: false, - work_json: None, + work: None, error_message: Some(message), } } @@ -1749,7 +1838,7 @@ fn work_error(message: String) -> Match3DWorkProcedureResult { fn run_result(run: Match3DRunSnapshot) -> Match3DRunProcedureResult { Match3DRunProcedureResult { ok: true, - run_json: Some(to_json_string(&run)), + run: Some(run), error_message: None, } } @@ -1757,7 +1846,7 @@ fn run_result(run: Match3DRunSnapshot) -> Match3DRunProcedureResult { fn run_error(message: String) -> Match3DRunProcedureResult { Match3DRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), } } @@ -1908,8 +1997,7 @@ mod tests { }; let row_json = to_json_string(&draft); - let restored = - parse_json::(&row_json, "match3d draft_json").unwrap(); + let restored = parse_json::(&row_json, "match3d draft_json").unwrap(); assert_eq!( restored.generated_item_assets_json.as_deref(), diff --git a/server-rs/crates/spacetime-module/src/match3d/types.rs b/server-rs/crates/spacetime-module/src/match3d/types.rs index ab79903f..4d17b024 100644 --- a/server-rs/crates/spacetime-module/src/match3d/types.rs +++ b/server-rs/crates/spacetime-module/src/match3d/types.rs @@ -182,43 +182,43 @@ pub struct Match3DRunTimeUpInput { #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct Match3DAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct Match3DWorkProcedureResult { pub ok: bool, - pub work_json: Option, + pub work: Option, pub error_message: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct Match3DWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct Match3DRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct Match3DClickItemProcedureResult { pub ok: bool, pub status: String, - pub run_json: Option, + pub run: Option, pub accepted_item_instance_id: Option, pub cleared_item_instance_ids: Vec, pub failure_reason: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DCreatorConfigSnapshot { pub theme_text: String, @@ -235,7 +235,7 @@ pub struct Match3DCreatorConfigSnapshot { pub generate_click_sound: bool, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DAgentMessageSnapshot { pub message_id: String, @@ -246,7 +246,7 @@ pub struct Match3DAgentMessageSnapshot { pub created_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DDraftSnapshot { pub profile_id: String, @@ -260,7 +260,7 @@ pub struct Match3DDraftSnapshot { pub generated_item_assets_json: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DAgentSessionSnapshot { pub session_id: String, @@ -278,7 +278,7 @@ pub struct Match3DAgentSessionSnapshot { pub updated_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DWorkSnapshot { pub profile_id: String, @@ -302,7 +302,7 @@ pub struct Match3DWorkSnapshot { pub generated_item_assets_json: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DItemSnapshot { pub item_instance_id: String, @@ -316,7 +316,7 @@ pub struct Match3DItemSnapshot { pub clickable: bool, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DTraySlotSnapshot { pub slot_index: u32, @@ -325,7 +325,7 @@ pub struct Match3DTraySlotSnapshot { pub visual_key: Option, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct Match3DRunSnapshot { pub run_id: String, diff --git a/server-rs/crates/spacetime-module/src/puzzle.rs b/server-rs/crates/spacetime-module/src/puzzle.rs index 1d65956d..7b027bd4 100644 --- a/server-rs/crates/spacetime-module/src/puzzle.rs +++ b/server-rs/crates/spacetime-module/src/puzzle.rs @@ -114,10 +114,10 @@ pub struct PuzzleWorkProfileRow { point_incentive_claimed_points: u64, } -/// 拼图广场公开列表投影。 +/// 拼图广场公开详情兼容投影。 /// -/// `puzzle_work_profile` 是私有真相表,HTTP gallery 只订阅这个 view, -/// 避免每次请求回到 procedure 重新扫表、组装列表和跨层 JSON 往返。 +/// 该 view 返回完整 `PuzzleWorkProfile`,包含 levels / anchor_pack 等详情级字段。 +/// 公开列表主路径应订阅更轻量的 `puzzle_gallery_card_view`。 #[spacetimedb::view(accessor = puzzle_gallery_view, public)] pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec { let mut items = ctx @@ -125,11 +125,40 @@ pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec .puzzle_work_profile() .by_puzzle_work_publication_status() .filter(PuzzlePublicationStatus::Published) - .filter_map(|row| match build_puzzle_work_profile_from_row_without_recent_count(&row) { - Ok(profile) => Some(profile), + .filter_map( + |row| match build_puzzle_work_profile_from_row_without_recent_count(&row) { + Ok(profile) => Some(profile), + Err(error) => { + log::warn!( + "拼图广场 view 跳过损坏的作品投影 profile_id={}: {}", + row.profile_id, + error + ); + None + } + }, + ) + .collect::>(); + items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros)); + items +} + +/// 拼图广场公开列表卡片投影。 +/// +/// 该 view 只暴露前端列表首屏需要的公开卡片字段,不携带 levels / anchor_pack +/// 等详情级载荷,供 api-server 热点缓存订阅和组装列表窗口。 +#[spacetimedb::view(accessor = puzzle_gallery_card_view, public)] +pub fn puzzle_gallery_card_view(ctx: &AnonymousViewContext) -> Vec { + let mut items = ctx + .db + .puzzle_work_profile() + .by_puzzle_work_publication_status() + .filter(PuzzlePublicationStatus::Published) + .filter_map(|row| match build_puzzle_gallery_card_view_row(&row) { + Ok(item) => Some(item), Err(error) => { log::warn!( - "拼图广场 view 跳过损坏的作品投影 profile_id={}: {}", + "拼图广场卡片 view 跳过损坏的作品投影 profile_id={}: {}", row.profile_id, error ); @@ -137,10 +166,41 @@ pub fn puzzle_gallery_view(ctx: &AnonymousViewContext) -> Vec } }) .collect::>(); - items.sort_by(|left, right| right.updated_at_micros.cmp(&left.updated_at_micros)); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); items } +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct PuzzleGalleryCardViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub level_name: String, + pub summary: String, + pub theme_tags: Vec, + pub cover_image_src: Option, + pub cover_asset_id: Option, + pub publication_status: PuzzlePublicationStatus, + pub updated_at_micros: i64, + pub published_at_micros: Option, + pub play_count: u32, + pub remix_count: u32, + pub like_count: u32, + pub point_incentive_total_half_points: u64, + pub point_incentive_claimed_points: u64, + pub publish_ready: bool, + pub generation_status: Option, +} + /// 拼图创作事件类型。 /// /// 事件表只广播跨层订阅需要的轻量事实,作品真相仍以 @@ -216,12 +276,12 @@ pub fn create_puzzle_agent_session( match ctx.try_with_tx(|tx| create_puzzle_agent_session_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -235,12 +295,12 @@ pub fn get_puzzle_agent_session( match ctx.try_with_tx(|tx| get_puzzle_agent_session_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -254,12 +314,12 @@ pub fn submit_puzzle_agent_message( match ctx.try_with_tx(|tx| submit_puzzle_agent_message_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -273,12 +333,12 @@ pub fn finalize_puzzle_agent_message_turn( match ctx.try_with_tx(|tx| finalize_puzzle_agent_message_turn_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -292,12 +352,12 @@ pub fn compile_puzzle_agent_draft( match ctx.try_with_tx(|tx| compile_puzzle_agent_draft_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -313,12 +373,12 @@ pub fn save_puzzle_form_draft( match ctx.try_with_tx(|tx| save_puzzle_form_draft_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -332,12 +392,12 @@ pub fn save_puzzle_generated_images( match ctx.try_with_tx(|tx| save_puzzle_generated_images_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -351,12 +411,12 @@ pub fn save_puzzle_ui_background( match ctx.try_with_tx(|tx| save_puzzle_ui_background_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -370,12 +430,12 @@ pub fn select_puzzle_cover_image( match ctx.try_with_tx(|tx| select_puzzle_cover_image_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -389,12 +449,12 @@ pub fn publish_puzzle_work( match ctx.try_with_tx(|tx| publish_puzzle_work_tx(tx, input.clone())) { Ok(item) => PuzzleWorkProcedureResult { ok: true, - item_json: Some(serialize_json(&item)), + item: Some(item), error_message: None, }, Err(message) => PuzzleWorkProcedureResult { ok: false, - item_json: None, + item: None, error_message: Some(message), }, } @@ -408,12 +468,12 @@ pub fn list_puzzle_works( match ctx.try_with_tx(|tx| list_puzzle_works_tx(tx, input.clone())) { Ok(items) => PuzzleWorksProcedureResult { ok: true, - items_json: Some(serialize_json(&items)), + items, error_message: None, }, Err(message) => PuzzleWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -427,12 +487,12 @@ pub fn get_puzzle_work_detail( match ctx.try_with_tx(|tx| get_puzzle_work_detail_tx(tx, input.clone())) { Ok(item) => PuzzleWorkProcedureResult { ok: true, - item_json: Some(serialize_json(&item)), + item: Some(item), error_message: None, }, Err(message) => PuzzleWorkProcedureResult { ok: false, - item_json: None, + item: None, error_message: Some(message), }, } @@ -446,12 +506,12 @@ pub fn update_puzzle_work( match ctx.try_with_tx(|tx| update_puzzle_work_tx(tx, input.clone())) { Ok(item) => PuzzleWorkProcedureResult { ok: true, - item_json: Some(serialize_json(&item)), + item: Some(item), error_message: None, }, Err(message) => PuzzleWorkProcedureResult { ok: false, - item_json: None, + item: None, error_message: Some(message), }, } @@ -465,12 +525,12 @@ pub fn delete_puzzle_work( match ctx.try_with_tx(|tx| delete_puzzle_work_tx(tx, input.clone())) { Ok(items) => PuzzleWorksProcedureResult { ok: true, - items_json: Some(serialize_json(&items)), + items, error_message: None, }, Err(message) => PuzzleWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -481,12 +541,12 @@ pub fn list_puzzle_gallery(ctx: &mut ProcedureContext) -> PuzzleWorksProcedureRe match ctx.try_with_tx(|tx| list_puzzle_gallery_tx(tx)) { Ok(items) => PuzzleWorksProcedureResult { ok: true, - items_json: Some(serialize_json(&items)), + items, error_message: None, }, Err(message) => PuzzleWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -500,12 +560,12 @@ pub fn get_puzzle_gallery_detail( match ctx.try_with_tx(|tx| get_puzzle_gallery_detail_tx(tx, input.clone())) { Ok(item) => PuzzleWorkProcedureResult { ok: true, - item_json: Some(serialize_json(&item)), + item: Some(item), error_message: None, }, Err(message) => PuzzleWorkProcedureResult { ok: false, - item_json: None, + item: None, error_message: Some(message), }, } @@ -519,12 +579,12 @@ pub fn record_puzzle_work_like( match ctx.try_with_tx(|tx| record_puzzle_work_like_tx(tx, input.clone())) { Ok(item) => PuzzleWorkProcedureResult { ok: true, - item_json: Some(serialize_json(&item)), + item: Some(item), error_message: None, }, Err(message) => PuzzleWorkProcedureResult { ok: false, - item_json: None, + item: None, error_message: Some(message), }, } @@ -538,12 +598,12 @@ pub fn remix_puzzle_work( match ctx.try_with_tx(|tx| remix_puzzle_work_tx(tx, input.clone())) { Ok(session) => PuzzleAgentSessionProcedureResult { ok: true, - session_json: Some(serialize_json(&session)), + session: Some(session), error_message: None, }, Err(message) => PuzzleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), }, } @@ -557,12 +617,12 @@ pub fn start_puzzle_run( match ctx.try_with_tx(|tx| start_puzzle_run_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -576,12 +636,12 @@ pub fn get_puzzle_run( match ctx.try_with_tx(|tx| get_puzzle_run_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -595,12 +655,12 @@ pub fn swap_puzzle_pieces( match ctx.try_with_tx(|tx| swap_puzzle_pieces_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -614,12 +674,12 @@ pub fn drag_puzzle_piece_or_group( match ctx.try_with_tx(|tx| drag_puzzle_piece_or_group_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -633,12 +693,12 @@ pub fn advance_puzzle_next_level( match ctx.try_with_tx(|tx| advance_puzzle_next_level_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -652,12 +712,12 @@ pub fn update_puzzle_run_pause( match ctx.try_with_tx(|tx| update_puzzle_run_pause_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -671,12 +731,12 @@ pub fn use_puzzle_runtime_prop( match ctx.try_with_tx(|tx| use_puzzle_runtime_prop_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -690,12 +750,12 @@ pub fn claim_puzzle_work_point_incentive( match ctx.try_with_tx(|tx| claim_puzzle_work_point_incentive_tx(tx, input.clone())) { Ok(item) => PuzzleWorkProcedureResult { ok: true, - item_json: Some(serialize_json(&item)), + item: Some(item), error_message: None, }, Err(message) => PuzzleWorkProcedureResult { ok: false, - item_json: None, + item: None, error_message: Some(message), }, } @@ -709,12 +769,12 @@ pub fn submit_puzzle_leaderboard_entry( match ctx.try_with_tx(|tx| submit_puzzle_leaderboard_entry_tx(tx, input.clone())) { Ok(run) => PuzzleRunProcedureResult { ok: true, - run_json: Some(serialize_json(&run)), + run: Some(run), error_message: None, }, Err(message) => PuzzleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), }, } @@ -899,10 +959,7 @@ fn compile_puzzle_agent_draft_tx( } let anchor_pack = infer_anchor_pack(&row.seed_text, Some(&row.seed_text)); let messages = list_session_messages(ctx, &row.session_id); - let draft = mark_puzzle_draft_generation_status( - compile_result_draft_from_seed(&anchor_pack, &messages, Some(&row.seed_text)), - "generating", - ); + let draft = compile_result_draft_from_seed(&anchor_pack, &messages, Some(&row.seed_text)); // 创作中心的拼图草稿卡只是 Agent session 的列表投影, // 每次编译结果页时同步 upsert,保证后续能按 source_session_id 恢复聊天。 upsert_puzzle_draft_work_profile( @@ -2447,6 +2504,68 @@ fn build_puzzle_work_profile_from_row_without_recent_count( }) } +fn build_puzzle_gallery_card_view_row( + row: &PuzzleWorkProfileRow, +) -> Result { + let levels = build_profile_levels_from_row(row)?; + Ok(PuzzleGalleryCardViewRow { + work_id: row.work_id.clone(), + profile_id: row.profile_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + work_title: if row.work_title.trim().is_empty() { + row.level_name.clone() + } else { + row.work_title.clone() + }, + work_description: if row.work_description.trim().is_empty() { + row.summary.clone() + } else { + row.work_description.clone() + }, + level_name: row.level_name.clone(), + summary: row.summary.clone(), + theme_tags: deserialize_theme_tags(&row.theme_tags_json)?, + cover_image_src: row.cover_image_src.clone(), + cover_asset_id: row.cover_asset_id.clone(), + publication_status: row.publication_status, + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + published_at_micros: row + .published_at + .map(|value| value.to_micros_since_unix_epoch()), + play_count: row.play_count, + remix_count: row.remix_count, + like_count: row.like_count, + point_incentive_total_half_points: row.point_incentive_total_half_points, + point_incentive_claimed_points: row.point_incentive_claimed_points, + publish_ready: row.publish_ready, + generation_status: resolve_puzzle_gallery_generation_status(&levels), + }) +} + +fn resolve_puzzle_gallery_generation_status( + levels: &[module_puzzle::PuzzleDraftLevel], +) -> Option { + levels + .iter() + .map(|level| level.generation_status.trim()) + .find(|status| *status == "generating") + .or_else(|| { + levels + .iter() + .map(|level| level.generation_status.trim()) + .find(|status| *status == "ready") + }) + .or_else(|| { + levels + .iter() + .map(|level| level.generation_status.trim()) + .find(|status| !status.is_empty()) + }) + .map(str::to_string) +} + fn build_profile_levels_from_row( row: &PuzzleWorkProfileRow, ) -> Result, String> { @@ -2503,52 +2622,10 @@ fn profile_for_single_level( level: &module_puzzle::PuzzleDraftLevel, ) -> PuzzleWorkProfile { let mut next_profile = profile.clone(); - let ui_background_carrier = profile.levels.iter().find(|candidate| { - candidate - .ui_background_image_src - .as_deref() - .map(str::trim) - .map(|value| !value.is_empty()) - .unwrap_or(false) - || candidate - .ui_background_image_object_key - .as_deref() - .map(str::trim) - .map(|value| !value.is_empty()) - .unwrap_or(false) - }); - let mut single_level = level.clone(); - if single_level - .ui_background_image_src - .as_deref() - .map(str::trim) - .unwrap_or("") - .is_empty() - && single_level - .ui_background_image_object_key - .as_deref() - .map(str::trim) - .unwrap_or("") - .is_empty() - && let Some(carrier) = ui_background_carrier - { - single_level.ui_background_image_src = carrier - .ui_background_image_src - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(str::to_string); - single_level.ui_background_image_object_key = carrier - .ui_background_image_object_key - .as_deref() - .map(str::trim) - .filter(|value| !value.is_empty()) - .map(|value| value.trim_start_matches('/').to_string()); - } next_profile.level_name = level.level_name.clone(); next_profile.cover_image_src = level.cover_image_src.clone(); next_profile.cover_asset_id = level.cover_asset_id.clone(); - next_profile.levels = vec![single_level]; + next_profile.levels = vec![level.clone()]; next_profile } @@ -2569,17 +2646,6 @@ fn micros_to_millis(value: i64) -> u64 { (value as u64).saturating_div(1_000) } -fn mark_puzzle_draft_generation_status( - mut draft: PuzzleResultDraft, - generation_status: &str, -) -> PuzzleResultDraft { - draft.generation_status = generation_status.to_string(); - for level in &mut draft.levels { - level.generation_status = generation_status.to_string(); - } - draft -} - fn upsert_puzzle_draft_work_profile( ctx: &TxContext, session_id: &str, @@ -3550,37 +3616,6 @@ mod tests { assert!(preview.publish_ready); } - #[test] - fn puzzle_draft_generation_status_updates_all_levels() { - let anchor_pack = infer_anchor_pack("蒸汽城市雨夜猫咪", Some("蒸汽城市雨夜猫咪")); - let mut draft = compile_result_draft(&anchor_pack, &[]); - draft.levels.push(module_puzzle::PuzzleDraftLevel { - level_id: "puzzle-level-2".to_string(), - level_name: "第二关".to_string(), - picture_description: "第二关画面".to_string(), - picture_reference: None, - ui_background_prompt: None, - ui_background_image_src: None, - ui_background_image_object_key: None, - background_music: None, - candidates: Vec::new(), - selected_candidate_id: None, - cover_image_src: None, - cover_asset_id: None, - generation_status: "idle".to_string(), - }); - - let draft = mark_puzzle_draft_generation_status(draft, "generating"); - - assert_eq!(draft.generation_status, "generating"); - assert!( - draft - .levels - .iter() - .all(|level| level.generation_status == "generating") - ); - } - #[test] fn puzzle_generated_images_replace_existing_candidate() { let anchor_pack = infer_anchor_pack("蒸汽城市雨夜猫咪", Some("蒸汽城市雨夜猫咪")); diff --git a/server-rs/crates/spacetime-module/src/runtime/browse_history.rs b/server-rs/crates/spacetime-module/src/runtime/browse_history.rs index a4886067..7183fa2b 100644 --- a/server-rs/crates/spacetime-module/src/runtime/browse_history.rs +++ b/server-rs/crates/spacetime-module/src/runtime/browse_history.rs @@ -95,8 +95,8 @@ fn list_platform_browse_history_rows( let mut entries = ctx .db .user_browse_history() - .iter() - .filter(|row| row.user_id == validated_input.user_id) + .by_browse_history_user_id() + .filter(&validated_input.user_id) .map(|row| build_runtime_browse_history_snapshot_from_row(&row)) .collect::>(); @@ -165,8 +165,8 @@ fn clear_platform_browse_history_rows( let row_ids = ctx .db .user_browse_history() - .iter() - .filter(|row| row.user_id == validated_input.user_id) + .by_browse_history_user_id() + .filter(&validated_input.user_id) .map(|row| row.browse_history_id.clone()) .collect::>(); diff --git a/server-rs/crates/spacetime-module/src/runtime/profile.rs b/server-rs/crates/spacetime-module/src/runtime/profile.rs index c4ba135f..10f3c59e 100644 --- a/server-rs/crates/spacetime-module/src/runtime/profile.rs +++ b/server-rs/crates/spacetime-module/src/runtime/profile.rs @@ -1079,8 +1079,8 @@ pub(crate) fn list_profile_save_archive_rows( let mut entries = ctx .db .profile_save_archive() - .iter() - .filter(|row| row.user_id == validated_input.user_id) + .by_profile_save_archive_user_id() + .filter(&validated_input.user_id) .map(|row| build_profile_save_archive_snapshot_from_row(&row)) .collect::>(); @@ -1104,10 +1104,12 @@ pub(crate) fn resume_profile_save_archive_record( let archive = ctx .db .profile_save_archive() - .iter() - .find(|row| { - row.user_id == validated_input.user_id && row.world_key == validated_input.world_key - }) + .by_profile_save_archive_user_world_key() + .filter(( + validated_input.user_id.as_str(), + validated_input.world_key.as_str(), + )) + .next() .ok_or_else(|| "profile_save_archive 对应 world_key 不存在".to_string())?; let existing_snapshot = ctx @@ -2052,8 +2054,8 @@ fn get_profile_dashboard_snapshot( let played_world_count = ctx .db .profile_played_world() - .iter() - .filter(|row| row.user_id == validated_input.user_id) + .by_profile_played_world_user_id() + .filter(&validated_input.user_id) .count() as u32; Ok(match state { @@ -2084,8 +2086,8 @@ fn list_profile_wallet_ledger_entries( let mut entries = ctx .db .profile_wallet_ledger() - .iter() - .filter(|row| row.user_id == validated_input.user_id) + .by_profile_wallet_ledger_user_id() + .filter(&validated_input.user_id) .map(|row| build_profile_wallet_ledger_snapshot_from_row(&row)) .collect::>(); @@ -2114,8 +2116,8 @@ fn get_profile_play_stats_snapshot( let mut played_works = ctx .db .profile_played_world() - .iter() - .filter(|row| row.user_id == validated_input.user_id) + .by_profile_played_world_user_id() + .filter(&validated_input.user_id) .map(|row| build_profile_played_world_snapshot_from_row(&row)) .collect::>(); @@ -2727,17 +2729,16 @@ fn build_profile_referral_invite_center_snapshot( let code = ensure_profile_invite_code(ctx, user_id); let today_inviter_reward_count = count_today_profile_referral_inviter_rewards(ctx, user_id, ctx.timestamp); - let invited_count = ctx + let invited_relations = ctx .db .profile_referral_relation() + .by_profile_referral_inviter_user_id() + .filter(user_id) + .collect::>(); + let invited_count = invited_relations.len() as u32; + let rewarded_invite_count = invited_relations .iter() - .filter(|row| row.inviter_user_id == user_id) - .count() as u32; - let rewarded_invite_count = ctx - .db - .profile_referral_relation() - .iter() - .filter(|row| row.inviter_user_id == user_id && row.inviter_reward_granted) + .filter(|row| row.inviter_reward_granted) .count() as u32; let bound_relation = ctx .db @@ -2918,7 +2919,8 @@ fn count_today_profile_referral_inviter_rewards( let day_start_micros = runtime_profile_day_start_micros(now.to_micros_since_unix_epoch()); ctx.db .profile_wallet_ledger() - .iter() + .by_profile_wallet_ledger_user_id() + .filter(user_id) .filter(|row| { row.user_id == user_id && row.source_type == RuntimeProfileWalletLedgerSourceType::InviteInviterReward @@ -3422,7 +3424,11 @@ fn query_analytics_metric_buckets( let stats = ctx .db .tracking_daily_stat() - .iter() + .by_tracking_daily_stat_scope_day() + .filter(( + validated_input.scope_kind, + validated_input.scope_id.as_str(), + )) .filter(|row| { row.event_key.trim() == validated_input.event_key && row.scope_kind == validated_input.scope_kind @@ -4023,27 +4029,39 @@ fn apply_profile_wallet_signed_delta( } fn has_profile_points_recharged(ctx: &ReducerContext, user_id: &str) -> bool { - ctx.db.profile_recharge_order().iter().any(|row| { - row.user_id == user_id - && row.kind == RuntimeProfileRechargeProductKind::Points - && row.status == RuntimeProfileRechargeOrderStatus::Paid - }) + ctx.db + .profile_recharge_order() + .by_profile_recharge_order_user_id() + .filter(user_id) + .any(|row| { + row.user_id == user_id + && row.kind == RuntimeProfileRechargeProductKind::Points + && row.status == RuntimeProfileRechargeOrderStatus::Paid + }) } fn has_profile_product_recharged(ctx: &ReducerContext, user_id: &str, product_id: &str) -> bool { - ctx.db.profile_recharge_order().iter().any(|row| { - row.user_id == user_id - && row.product_id == product_id - && row.kind == RuntimeProfileRechargeProductKind::Points - && row.status == RuntimeProfileRechargeOrderStatus::Paid - }) + ctx.db + .profile_recharge_order() + .by_profile_recharge_order_user_id() + .filter(user_id) + .any(|row| { + row.user_id == user_id + && row.product_id == product_id + && row.kind == RuntimeProfileRechargeProductKind::Points + && row.status == RuntimeProfileRechargeOrderStatus::Paid + }) } fn has_profile_business_wallet_ledger(ctx: &ReducerContext, user_id: &str) -> bool { - ctx.db.profile_wallet_ledger().iter().any(|row| { - row.user_id == user_id - && row.source_type != RuntimeProfileWalletLedgerSourceType::SnapshotSync - }) + ctx.db + .profile_wallet_ledger() + .by_profile_wallet_ledger_user_id() + .filter(user_id) + .any(|row| { + row.user_id == user_id + && row.source_type != RuntimeProfileWalletLedgerSourceType::SnapshotSync + }) } fn latest_profile_recharge_order( @@ -4053,8 +4071,8 @@ fn latest_profile_recharge_order( let mut orders = ctx .db .profile_recharge_order() - .iter() - .filter(|row| row.user_id == user_id) + .by_profile_recharge_order_user_id() + .filter(user_id) .collect::>(); orders.sort_by(|left, right| { right diff --git a/server-rs/crates/spacetime-module/src/square_hole/mod.rs b/server-rs/crates/spacetime-module/src/square_hole/mod.rs index 0d371ec0..4358722a 100644 --- a/server-rs/crates/spacetime-module/src/square_hole/mod.rs +++ b/server-rs/crates/spacetime-module/src/square_hole/mod.rs @@ -26,6 +26,65 @@ use module_square_hole::{ }; use serde::Serialize; use serde::de::DeserializeOwned; +use spacetimedb::AnonymousViewContext; + +/// 方洞挑战公开广场列表投影。 +/// +/// HTTP gallery 通过 `spacetime-client` 订阅该 view 后读本地 cache, +/// 不再在每个公开列表请求里调用 `list_square_hole_works` procedure。 +#[spacetimedb::view(accessor = square_hole_gallery_view, public)] +pub fn square_hole_gallery_view(ctx: &AnonymousViewContext) -> Vec { + let mut items = ctx + .db + .square_hole_work_profile() + .by_square_hole_work_publication_status() + .filter(SQUARE_HOLE_PUBLICATION_PUBLISHED) + .filter_map(|row| match build_gallery_view_row(&row) { + Ok(item) => Some(item), + Err(error) => { + log::warn!( + "方洞挑战公开广场 view 跳过损坏的作品投影 profile_id={}: {}", + row.profile_id, + error + ); + None + } + }) + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + items +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct SquareHoleGalleryViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: String, + pub author_display_name: String, + pub game_name: String, + pub theme_text: String, + pub twist_rule: String, + pub summary_text: String, + pub tags: Vec, + pub cover_image_src: String, + pub background_prompt: String, + pub background_image_src: String, + pub shape_options: Vec, + pub hole_options: Vec, + pub shape_count: u32, + pub difficulty: u32, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} #[spacetimedb::procedure] pub fn create_square_hole_agent_session( @@ -112,12 +171,12 @@ pub fn list_square_hole_works( match ctx.try_with_tx(|tx| list_square_hole_works_tx(tx, input.clone())) { Ok(items) => SquareHoleWorksProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => SquareHoleWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -142,12 +201,12 @@ pub fn delete_square_hole_work( match ctx.try_with_tx(|tx| delete_square_hole_work_tx(tx, input.clone())) { Ok(items) => SquareHoleWorksProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => SquareHoleWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -185,8 +244,8 @@ pub fn drop_square_hole_shape( Err(message) => SquareHoleDropShapeProcedureResult { ok: false, status: SQUARE_HOLE_DROP_REJECTED.to_string(), - run_json: None, - feedback_json: None, + run: None, + feedback: None, failure_reason: None, error_message: Some(message), }, @@ -743,10 +802,8 @@ fn drop_square_hole_shape_tx( Ok(SquareHoleDropShapeProcedureResult { ok: true, status: status.to_string(), - run_json: Some(to_json_string(&next)), - feedback_json: Some(to_json_string(&feedback_from_domain( - &confirmation.feedback, - ))), + run: Some(next), + feedback: Some(feedback_from_domain(&confirmation.feedback)), failure_reason: confirmation .feedback .reject_reason @@ -880,6 +937,38 @@ fn build_work_snapshot(row: &SquareHoleWorkProfileRow) -> Result Result { + let config = parse_config(&row.config_json)?; + Ok(SquareHoleGalleryViewRow { + work_id: row.work_id.clone(), + profile_id: row.profile_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: row.source_session_id.clone(), + author_display_name: row.author_display_name.clone(), + game_name: row.game_name.clone(), + theme_text: row.theme_text.clone(), + twist_rule: row.twist_rule.clone(), + summary_text: row.summary_text.clone(), + tags: parse_tags(&row.tags_json)?, + cover_image_src: row.cover_image_src.clone(), + background_prompt: config.background_prompt, + background_image_src: config.background_image_src, + shape_options: config.shape_options, + hole_options: config.hole_options, + shape_count: row.shape_count, + difficulty: row.difficulty, + publication_status: row.publication_status.clone(), + publish_ready: is_work_publish_ready(row), + play_count: row.play_count, + updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), + published_at_micros: row + .published_at + .map(|value| value.to_micros_since_unix_epoch()), + }) +} + fn refresh_run_row( ctx: &ReducerContext, row: SquareHoleRuntimeRunRow, @@ -1502,7 +1591,7 @@ fn session_result( ) -> SquareHoleAgentSessionProcedureResult { SquareHoleAgentSessionProcedureResult { ok: true, - session_json: Some(to_json_string(&session)), + session: Some(session), error_message: None, } } @@ -1510,7 +1599,7 @@ fn session_result( fn session_error(message: String) -> SquareHoleAgentSessionProcedureResult { SquareHoleAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), } } @@ -1518,7 +1607,7 @@ fn session_error(message: String) -> SquareHoleAgentSessionProcedureResult { fn work_result(work: SquareHoleWorkSnapshot) -> SquareHoleWorkProcedureResult { SquareHoleWorkProcedureResult { ok: true, - work_json: Some(to_json_string(&work)), + work: Some(work), error_message: None, } } @@ -1526,7 +1615,7 @@ fn work_result(work: SquareHoleWorkSnapshot) -> SquareHoleWorkProcedureResult { fn work_error(message: String) -> SquareHoleWorkProcedureResult { SquareHoleWorkProcedureResult { ok: false, - work_json: None, + work: None, error_message: Some(message), } } @@ -1534,7 +1623,7 @@ fn work_error(message: String) -> SquareHoleWorkProcedureResult { fn run_result(run: SquareHoleRunSnapshot) -> SquareHoleRunProcedureResult { SquareHoleRunProcedureResult { ok: true, - run_json: Some(to_json_string(&run)), + run: Some(run), error_message: None, } } @@ -1542,7 +1631,7 @@ fn run_result(run: SquareHoleRunSnapshot) -> SquareHoleRunProcedureResult { fn run_error(message: String) -> SquareHoleRunProcedureResult { SquareHoleRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), } } diff --git a/server-rs/crates/spacetime-module/src/square_hole/types.rs b/server-rs/crates/spacetime-module/src/square_hole/types.rs index 232002e1..70a86c66 100644 --- a/server-rs/crates/spacetime-module/src/square_hole/types.rs +++ b/server-rs/crates/spacetime-module/src/square_hole/types.rs @@ -168,42 +168,42 @@ pub struct SquareHoleRunTimeUpInput { #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct SquareHoleAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct SquareHoleWorkProcedureResult { pub ok: bool, - pub work_json: Option, + pub work: Option, pub error_message: Option, } #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct SquareHoleWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct SquareHoleRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct SquareHoleDropShapeProcedureResult { pub ok: bool, pub status: String, - pub run_json: Option, - pub feedback_json: Option, + pub run: Option, + pub feedback: Option, pub failure_reason: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleCreatorConfigSnapshot { pub theme_text: String, @@ -222,7 +222,7 @@ pub struct SquareHoleCreatorConfigSnapshot { pub background_image_src: String, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleShapeOptionSnapshot { pub option_id: String, @@ -235,7 +235,7 @@ pub struct SquareHoleShapeOptionSnapshot { pub image_src: String, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleHoleOptionSnapshot { pub hole_id: String, @@ -247,7 +247,7 @@ pub struct SquareHoleHoleOptionSnapshot { pub image_src: String, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleAgentMessageSnapshot { pub message_id: String, @@ -258,7 +258,7 @@ pub struct SquareHoleAgentMessageSnapshot { pub created_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleDraftSnapshot { pub profile_id: String, @@ -281,7 +281,7 @@ pub struct SquareHoleDraftSnapshot { pub difficulty: u32, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleAgentSessionSnapshot { pub session_id: String, @@ -299,7 +299,7 @@ pub struct SquareHoleAgentSessionSnapshot { pub updated_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleWorkSnapshot { pub work_id: String, @@ -331,7 +331,7 @@ pub struct SquareHoleWorkSnapshot { pub published_at_micros: Option, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleShapeSnapshot { pub shape_id: String, @@ -344,7 +344,7 @@ pub struct SquareHoleShapeSnapshot { pub image_src: String, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleHoleSnapshot { pub hole_id: String, @@ -356,7 +356,7 @@ pub struct SquareHoleHoleSnapshot { pub image_src: String, } -#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleDropFeedbackSnapshot { pub accepted: bool, @@ -364,7 +364,7 @@ pub struct SquareHoleDropFeedbackSnapshot { pub message: String, } -#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct SquareHoleRunSnapshot { pub run_id: String, diff --git a/server-rs/crates/spacetime-module/src/visual_novel.rs b/server-rs/crates/spacetime-module/src/visual_novel.rs index 1e64046c..f377e312 100644 --- a/server-rs/crates/spacetime-module/src/visual_novel.rs +++ b/server-rs/crates/spacetime-module/src/visual_novel.rs @@ -1,6 +1,7 @@ use crate::*; use serde::Serialize; use serde::de::DeserializeOwned; +use spacetimedb::AnonymousViewContext; pub const VISUAL_NOVEL_SOURCE_IDEA: &str = "idea"; pub const VISUAL_NOVEL_SOURCE_DOCUMENT: &str = "document"; @@ -166,6 +167,58 @@ pub struct VisualNovelRuntimeEvent { pub(crate) occurred_at: Timestamp, } +/// 视觉小说公开广场列表投影。 +/// +/// 该 view 只暴露已发布作品卡片需要的公开字段,HTTP gallery 订阅后 +/// 从本地 cache 读取,避免每个列表请求调用 `list_visual_novel_works` procedure。 +#[spacetimedb::view(accessor = visual_novel_gallery_view, public)] +pub fn visual_novel_gallery_view(ctx: &AnonymousViewContext) -> Vec { + let mut items = ctx + .db + .visual_novel_work_profile() + .by_visual_novel_work_publication_status() + .filter(VISUAL_NOVEL_PUBLICATION_PUBLISHED) + .filter_map(|row| match build_gallery_view_row(&row) { + Ok(item) => Some(item), + Err(error) => { + log::warn!( + "视觉小说公开广场 view 跳过损坏的作品投影 profile_id={}: {}", + row.profile_id, + error + ); + None + } + }) + .collect::>(); + items.sort_by(|left, right| { + right + .updated_at_micros + .cmp(&left.updated_at_micros) + .then_with(|| left.profile_id.cmp(&right.profile_id)) + }); + items +} + +#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +pub struct VisualNovelGalleryViewRow { + pub work_id: String, + pub profile_id: String, + pub owner_user_id: String, + pub source_session_id: Option, + pub author_display_name: String, + pub work_title: String, + pub work_description: String, + pub tags: Vec, + pub cover_image_src: Option, + pub source_asset_ids: Vec, + pub publication_status: String, + pub publish_ready: bool, + pub play_count: u32, + pub created_at_micros: i64, + pub updated_at_micros: i64, + pub published_at_micros: Option, +} + #[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] pub struct VisualNovelAgentSessionCreateInput { pub session_id: String, @@ -326,49 +379,65 @@ pub struct VisualNovelRuntimeEventRecordInput { pub occurred_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct VisualNovelAgentSessionProcedureResult { pub ok: bool, - pub session_json: Option, + pub session: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct VisualNovelWorkProcedureResult { pub ok: bool, - pub work_json: Option, + pub work: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct VisualNovelWorksProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct VisualNovelRunProcedureResult { pub ok: bool, - pub run_json: Option, + pub run: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct VisualNovelHistoryProcedureResult { pub ok: bool, - pub items_json: Option, + pub items: Vec, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Eq, SpacetimeType)] +#[derive(Clone, Debug, PartialEq, SpacetimeType)] pub struct VisualNovelRuntimeEventProcedureResult { pub ok: bool, - pub event_json: Option, + pub event: Option, pub error_message: Option, } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] +pub struct VisualNovelJsonField { + pub key: String, + pub value: VisualNovelJsonValue, +} + +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] +pub enum VisualNovelJsonValue { + Null, + Bool(bool), + Number(f64), + String(String), + Array(Vec), + Object(Vec), +} + +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct VisualNovelAgentMessageSnapshot { pub message_id: String, @@ -379,7 +448,7 @@ pub struct VisualNovelAgentMessageSnapshot { pub created_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct VisualNovelAgentSessionSnapshot { pub session_id: String, @@ -391,15 +460,15 @@ pub struct VisualNovelAgentSessionSnapshot { pub current_turn: u32, pub progress_percent: u32, pub messages: Vec, - pub draft: Option, - pub pending_action: Option, + pub draft: Option, + pub pending_action: Option, pub last_assistant_reply: Option, pub published_profile_id: Option, pub created_at_micros: i64, pub updated_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct VisualNovelWorkSnapshot { pub work_id: String, @@ -412,7 +481,7 @@ pub struct VisualNovelWorkSnapshot { pub tags: Vec, pub cover_image_src: Option, pub source_asset_ids: Vec, - pub draft: JsonValue, + pub draft: VisualNovelJsonValue, pub publication_status: String, pub publish_ready: bool, pub play_count: u32, @@ -421,7 +490,7 @@ pub struct VisualNovelWorkSnapshot { pub published_at_micros: Option, } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct VisualNovelRuntimeHistoryEntrySnapshot { pub entry_id: String, @@ -431,13 +500,13 @@ pub struct VisualNovelRuntimeHistoryEntrySnapshot { pub turn_index: u32, pub source: String, pub action_text: Option, - pub steps: JsonValue, + pub steps: VisualNovelJsonValue, pub snapshot_before_hash: Option, pub snapshot_after_hash: Option, pub created_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct VisualNovelRunSnapshot { pub run_id: String, @@ -448,16 +517,16 @@ pub struct VisualNovelRunSnapshot { pub current_scene_id: Option, pub current_phase_id: Option, pub visible_character_ids: Vec, - pub flags: JsonValue, - pub metrics: JsonValue, + pub flags: VisualNovelJsonValue, + pub metrics: VisualNovelJsonValue, pub history: Vec, - pub available_choices: JsonValue, + pub available_choices: VisualNovelJsonValue, pub text_mode_enabled: bool, pub created_at_micros: i64, pub updated_at_micros: i64, } -#[derive(Clone, Debug, PartialEq, Serialize)] +#[derive(Clone, Debug, PartialEq, Serialize, SpacetimeType)] #[serde(rename_all = "camelCase")] pub struct VisualNovelRuntimeEventSnapshot { pub event_id: String, @@ -467,7 +536,7 @@ pub struct VisualNovelRuntimeEventSnapshot { pub event_kind: String, pub client_event_id: Option, pub history_entry_id: Option, - pub payload: JsonValue, + pub payload: VisualNovelJsonValue, pub occurred_at_micros: i64, } @@ -556,12 +625,12 @@ pub fn list_visual_novel_works( match ctx.try_with_tx(|tx| list_visual_novel_works_tx(tx, input.clone())) { Ok(items) => VisualNovelWorksProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => VisualNovelWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -586,12 +655,12 @@ pub fn delete_visual_novel_work( match ctx.try_with_tx(|tx| delete_visual_novel_work_tx(tx, input.clone())) { Ok(items) => VisualNovelWorksProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => VisualNovelWorksProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -638,12 +707,12 @@ pub fn append_visual_novel_runtime_history_entry( match ctx.try_with_tx(|tx| append_visual_novel_runtime_history_entry_tx(tx, input.clone())) { Ok(items) => VisualNovelHistoryProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => VisualNovelHistoryProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -657,12 +726,12 @@ pub fn list_visual_novel_runtime_history( match ctx.try_with_tx(|tx| list_visual_novel_runtime_history_tx(tx, input.clone())) { Ok(items) => VisualNovelHistoryProcedureResult { ok: true, - items_json: Some(to_json_string(&items)), + items, error_message: None, }, Err(message) => VisualNovelHistoryProcedureResult { ok: false, - items_json: None, + items: Vec::new(), error_message: Some(message), }, } @@ -676,12 +745,12 @@ pub fn record_visual_novel_runtime_event( match ctx.try_with_tx(|tx| record_visual_novel_runtime_event_tx(tx, input.clone())) { Ok(event) => VisualNovelRuntimeEventProcedureResult { ok: true, - event_json: Some(to_json_string(&event)), + event: Some(event), error_message: None, }, Err(message) => VisualNovelRuntimeEventProcedureResult { ok: false, - event_json: None, + event: None, error_message: Some(message), }, } @@ -1052,17 +1121,22 @@ fn list_visual_novel_works_tx( ctx: &ReducerContext, input: VisualNovelWorksListInput, ) -> Result, String> { - let mut items = ctx - .db - .visual_novel_work_profile() + let rows = if input.published_only { + ctx.db + .visual_novel_work_profile() + .by_visual_novel_work_publication_status() + .filter(&VISUAL_NOVEL_PUBLICATION_PUBLISHED.to_string()) + .collect::>() + } else { + require_non_empty(&input.owner_user_id, "visual_novel owner_user_id")?; + ctx.db + .visual_novel_work_profile() + .by_visual_novel_work_owner_user_id() + .filter(&input.owner_user_id) + .collect::>() + }; + let mut items = rows .iter() - .filter(|row| { - if input.published_only { - row.publication_status == VISUAL_NOVEL_PUBLICATION_PUBLISHED - } else { - row.owner_user_id == input.owner_user_id - } - }) .map(|row| build_work_snapshot(&row)) .collect::, _>>()?; items.sort_by(|left, right| { @@ -1103,10 +1177,9 @@ fn delete_visual_novel_work_tx( for run in ctx .db .visual_novel_runtime_run() - .iter() - .filter(|row| { - row.profile_id == input.profile_id && row.owner_user_id == input.owner_user_id - }) + .by_visual_novel_run_profile_id() + .filter(&input.profile_id) + .filter(|row| row.owner_user_id == input.owner_user_id) .collect::>() { delete_run_children(ctx, &run.run_id, &input.owner_user_id); @@ -1385,8 +1458,8 @@ fn build_session_snapshot( let mut messages = ctx .db .visual_novel_agent_message() - .iter() - .filter(|message| message.session_id == row.session_id) + .by_visual_novel_agent_message_session_id() + .filter(&row.session_id) .map(|message| VisualNovelAgentMessageSnapshot { message_id: message.message_id, session_id: message.session_id, @@ -1412,8 +1485,9 @@ fn build_session_snapshot( current_turn: row.current_turn, progress_percent: row.progress_percent, messages, - draft: parse_optional_json_value(&row.draft_json)?, - pending_action: parse_optional_json_value(&row.pending_action_json)?, + draft: parse_optional_json_value(&row.draft_json)?.map(visual_novel_json_from_serde), + pending_action: parse_optional_json_value(&row.pending_action_json)? + .map(visual_novel_json_from_serde), last_assistant_reply: empty_to_none(&row.last_assistant_reply), published_profile_id: empty_to_none(&row.published_profile_id), created_at_micros: row.created_at.to_micros_since_unix_epoch(), @@ -1433,7 +1507,32 @@ fn build_work_snapshot(row: &VisualNovelWorkProfileRow) -> Result Result { + Ok(VisualNovelGalleryViewRow { + work_id: row.work_id.clone(), + profile_id: row.profile_id.clone(), + owner_user_id: row.owner_user_id.clone(), + source_session_id: empty_to_none(&row.source_session_id), + author_display_name: row.author_display_name.clone(), + work_title: row.work_title.clone(), + work_description: row.work_description.clone(), + tags: parse_string_vec_or_empty(&row.tags_json)?, + cover_image_src: empty_to_none(&row.cover_image_src), + source_asset_ids: parse_string_vec_or_empty(&row.source_asset_ids_json)?, publication_status: row.publication_status.clone(), publish_ready: row.publish_ready, play_count: row.play_count, @@ -1458,10 +1557,12 @@ fn build_run_snapshot( current_scene_id: empty_to_none(&row.current_scene_id), current_phase_id: empty_to_none(&row.current_phase_id), visible_character_ids: parse_string_vec_or_empty(&row.visible_character_ids_json)?, - flags: parse_json_value_or_object(&row.flags_json)?, - metrics: parse_json_value_or_object(&row.metrics_json)?, + flags: visual_novel_json_from_serde(parse_json_value_or_object(&row.flags_json)?), + metrics: visual_novel_json_from_serde(parse_json_value_or_object(&row.metrics_json)?), history: build_history_snapshots(ctx, &row.run_id, &row.owner_user_id)?, - available_choices: parse_json_value_or_array(&row.available_choices_json)?, + available_choices: visual_novel_json_from_serde(parse_json_value_or_array( + &row.available_choices_json, + )?), text_mode_enabled: row.text_mode_enabled, created_at_micros: row.created_at.to_micros_since_unix_epoch(), updated_at_micros: row.updated_at.to_micros_since_unix_epoch(), @@ -1476,8 +1577,9 @@ fn build_history_snapshots( let mut items = ctx .db .visual_novel_runtime_history_entry() - .iter() - .filter(|row| row.run_id == run_id && row.owner_user_id == owner_user_id) + .by_visual_novel_history_run_id() + .filter(&run_id.to_string()) + .filter(|row| row.owner_user_id == owner_user_id) .map(|row| build_history_snapshot(&row)) .collect::, _>>()?; items.sort_by(|left, right| { @@ -1500,7 +1602,7 @@ fn build_history_snapshot( turn_index: row.turn_index, source: row.source.clone(), action_text: empty_to_none(&row.action_text), - steps: parse_json_value_or_array(&row.steps_json)?, + steps: visual_novel_json_from_serde(parse_json_value_or_array(&row.steps_json)?), snapshot_before_hash: empty_to_none(&row.snapshot_before_hash), snapshot_after_hash: empty_to_none(&row.snapshot_after_hash), created_at_micros: row.created_at.to_micros_since_unix_epoch(), @@ -1518,7 +1620,7 @@ fn build_event_snapshot( event_kind: row.event_kind.clone(), client_event_id: empty_to_none(&row.client_event_id), history_entry_id: empty_to_none(&row.history_entry_id), - payload: parse_json_value_or_object(&row.payload_json)?, + payload: visual_novel_json_from_serde(parse_json_value_or_object(&row.payload_json)?), occurred_at_micros: row.occurred_at.to_micros_since_unix_epoch(), }) } @@ -1579,8 +1681,9 @@ fn delete_run_children(ctx: &ReducerContext, run_id: &str, owner_user_id: &str) for history in ctx .db .visual_novel_runtime_history_entry() - .iter() - .filter(|row| row.run_id == run_id && row.owner_user_id == owner_user_id) + .by_visual_novel_history_run_id() + .filter(&run_id.to_string()) + .filter(|row| row.owner_user_id == owner_user_id) .collect::>() { ctx.db @@ -1758,6 +1861,30 @@ fn parse_json_value_or_array(value: &str) -> Result { parse_json_value(value) } +fn visual_novel_json_from_serde(value: JsonValue) -> VisualNovelJsonValue { + match value { + JsonValue::Null => VisualNovelJsonValue::Null, + JsonValue::Bool(value) => VisualNovelJsonValue::Bool(value), + JsonValue::Number(value) => VisualNovelJsonValue::Number(value.as_f64().unwrap_or(0.0)), + JsonValue::String(value) => VisualNovelJsonValue::String(value), + JsonValue::Array(items) => VisualNovelJsonValue::Array( + items + .into_iter() + .map(visual_novel_json_from_serde) + .collect(), + ), + JsonValue::Object(object) => VisualNovelJsonValue::Object( + object + .into_iter() + .map(|(key, value)| VisualNovelJsonField { + key, + value: visual_novel_json_from_serde(value), + }) + .collect(), + ), + } +} + fn draft_string_field(draft: &JsonValue, key: &str) -> Option { draft .get(key) @@ -1853,7 +1980,7 @@ fn session_result( ) -> VisualNovelAgentSessionProcedureResult { VisualNovelAgentSessionProcedureResult { ok: true, - session_json: Some(to_json_string(&session)), + session: Some(session), error_message: None, } } @@ -1861,7 +1988,7 @@ fn session_result( fn session_error(message: String) -> VisualNovelAgentSessionProcedureResult { VisualNovelAgentSessionProcedureResult { ok: false, - session_json: None, + session: None, error_message: Some(message), } } @@ -1869,7 +1996,7 @@ fn session_error(message: String) -> VisualNovelAgentSessionProcedureResult { fn work_result(work: VisualNovelWorkSnapshot) -> VisualNovelWorkProcedureResult { VisualNovelWorkProcedureResult { ok: true, - work_json: Some(to_json_string(&work)), + work: Some(work), error_message: None, } } @@ -1877,7 +2004,7 @@ fn work_result(work: VisualNovelWorkSnapshot) -> VisualNovelWorkProcedureResult fn work_error(message: String) -> VisualNovelWorkProcedureResult { VisualNovelWorkProcedureResult { ok: false, - work_json: None, + work: None, error_message: Some(message), } } @@ -1885,7 +2012,7 @@ fn work_error(message: String) -> VisualNovelWorkProcedureResult { fn run_result(run: VisualNovelRunSnapshot) -> VisualNovelRunProcedureResult { VisualNovelRunProcedureResult { ok: true, - run_json: Some(to_json_string(&run)), + run: Some(run), error_message: None, } } @@ -1893,7 +2020,7 @@ fn run_result(run: VisualNovelRunSnapshot) -> VisualNovelRunProcedureResult { fn run_error(message: String) -> VisualNovelRunProcedureResult { VisualNovelRunProcedureResult { ok: false, - run_json: None, + run: None, error_message: Some(message), } } diff --git a/src/services/puzzle-gallery/puzzleGalleryClient.ts b/src/services/puzzle-gallery/puzzleGalleryClient.ts index d57ec95a..2ee76b62 100644 --- a/src/services/puzzle-gallery/puzzleGalleryClient.ts +++ b/src/services/puzzle-gallery/puzzleGalleryClient.ts @@ -2,6 +2,7 @@ import type { PuzzleAgentSessionSnapshot, } from '../../../packages/shared/src/contracts/puzzleAgentSession'; import type { + PuzzleGalleryResponse, PuzzleWorksResponse, PuzzleWorkSummary, } from '../../../packages/shared/src/contracts/puzzleWorkSummary'; @@ -18,7 +19,7 @@ const PUZZLE_GALLERY_READ_RETRY: ApiRetryOptions = { * 读取拼图广场列表。 */ export async function listPuzzleGallery() { - return requestJson( + return requestJson( PUZZLE_GALLERY_API_BASE, { method: 'GET',