From f6604ea3b46f65d846a9fa0deef1704781edf1c7 Mon Sep 17 00:00:00 2001 From: kdletters Date: Fri, 24 Apr 2026 22:07:02 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0maincloud=E5=8F=91=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .codex/skills/spacetimedb-cli/SKILL.md | 2 + .env.local | 4 + ...N_RS_NODE_WORKFLOW_ALIGNMENT_2026-04-24.md | 24 ++++ ...E_PLAYER_AND_REAL_IMAGE_PLAN_2026-04-24.md | 68 ++++++++++ ...PACETIMEDB_MAINCLOUD_PUBLISH_2026-04-24.md | 59 +++++++++ package.json | 4 +- scripts/api-server-maincloud.mjs | 86 ++++++++++++ scripts/dev-web-rust.mjs | 40 ++++++ scripts/spacetime-publish-maincloud.sh | 125 ++++++++++++++++++ server-rs/crates/api-server/src/config.rs | 17 ++- .../src/custom_world_agent_entities.rs | 3 +- server-rs/crates/api-server/src/puzzle.rs | 31 ++--- .../spacetime-client/src/custom_world.rs | 35 +++++ server-rs/crates/spacetime-client/src/lib.rs | 35 ++++- .../crates/spacetime-client/src/mapper.rs | 49 +++++++ .../src/module_bindings/mod.rs | 4 + 16 files changed, 563 insertions(+), 23 deletions(-) create mode 100644 docs/technical/PUZZLE_SINGLE_PLAYER_AND_REAL_IMAGE_PLAN_2026-04-24.md create mode 100644 docs/technical/SPACETIMEDB_MAINCLOUD_PUBLISH_2026-04-24.md create mode 100644 scripts/api-server-maincloud.mjs create mode 100644 scripts/dev-web-rust.mjs create mode 100644 scripts/spacetime-publish-maincloud.sh diff --git a/.codex/skills/spacetimedb-cli/SKILL.md b/.codex/skills/spacetimedb-cli/SKILL.md index 0aa0d2ac..3bd0d454 100644 --- a/.codex/skills/spacetimedb-cli/SKILL.md +++ b/.codex/skills/spacetimedb-cli/SKILL.md @@ -203,6 +203,8 @@ spacetime server ping ```bash # Clear data and republish spacetime publish my-db --clear-database --yes +# Clear data and republish only when conflict +spacetime publish my-db --clear-database=on-conflict --yes ``` ### "Build failed" diff --git a/.env.local b/.env.local index 593661a6..fe798ce3 100644 --- a/.env.local +++ b/.env.local @@ -52,3 +52,7 @@ ALIYUN_OSS_ACCESS_KEY_SECRET="XblWGE6CO1WLnSBdMRVpL6lut4GSoS" GENARRATIVE_BACKEND_STACK="rust" RUST_SERVER_TARGET="http://127.0.0.1:3100" GENARRATIVE_API_TARGET="http://127.0.0.1:3100" + +GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL="https://maincloud.spacetimedb.com" +GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE="xushi-p4wfr" +GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN="eyJhbGciOiJSUzI1NiJ9.eyJzdWIiOiIwMUsyN05YUjBaQkRUVEVCNlFQQjFXNzU2MiIsImlzcyI6Imh0dHBzOi8vYXV0aC5zcGFjZXRpbWVkYi5jb20iLCJhdWQiOiJzcGFjZXRpbWVkYiIsImlhdCI6MTc3NzAzNjI2MSwiZXhwIjoxODQwMTA4MjYxfQ.XosLKR-y85dv4yRN-INJMNSWhz4VtXaDvypvyzNAwFdmLMC3IKG6HfmSBHwLOjO3JVkQBTKodivYe6_sDOFNsCMGdP5nwMubYlmxWaOk41WBldd3JFA7ag8OpikYBkWp-4n59c8wLn-LWiOUWBw_g5vaCbzZs3pP51amw9o-DUEog53fGjoS3ij8oVIg_8AZDxoSmqVvT6K-2wIpstj7bM674nks-qbhMuAjdM9l1HURw_uip5iWEIB4hQZtzlOtHe49wvhN3lvgoM9r4YJS7emDDBwFTopQF-cSPKyh_tFfpH7jUIb3RiqGutQV37c3veNnUVxmYNvqB561eR4mQw" diff --git a/docs/technical/CUSTOM_WORLD_DRAFT_FOUNDATION_RS_NODE_WORKFLOW_ALIGNMENT_2026-04-24.md b/docs/technical/CUSTOM_WORLD_DRAFT_FOUNDATION_RS_NODE_WORKFLOW_ALIGNMENT_2026-04-24.md index 18c58825..0186d5bc 100644 --- a/docs/technical/CUSTOM_WORLD_DRAFT_FOUNDATION_RS_NODE_WORKFLOW_ALIGNMENT_2026-04-24.md +++ b/docs/technical/CUSTOM_WORLD_DRAFT_FOUNDATION_RS_NODE_WORKFLOW_ALIGNMENT_2026-04-24.md @@ -83,3 +83,27 @@ cargo test -p api-server custom_world_foundation_draft -- --nocapture ``` 结果:后端检查通过;`custom_world_foundation_draft` 相关测试 `3 passed`。 + +## 2026-04-24 `spacetime-client` facade 补齐 + +合并 `draft_foundation` 进度链路后,`spacetime-module` 和生成绑定中已经存在 `upsert_custom_world_agent_operation_progress` procedure,但手写 `spacetime-client` facade 尚未导出对应 record input 与调用方法,导致 `api-server` 编译时报: + +1. `CustomWorldAgentOperationProgressRecordInput` 未导出。 +2. `SpacetimeClient::upsert_custom_world_agent_operation_progress` 不存在。 + +本次补齐边界: + +1. `spacetime-client::mapper` 新增 `CustomWorldAgentOperationProgressRecordInput`。 +2. `spacetime-client::custom_world` 新增 `upsert_custom_world_agent_operation_progress(...)`,负责把字符串形式的 operation type/status 翻译为 SpacetimeDB 生成枚举后调用 procedure。 +3. `spacetime-client::module_bindings::mod` 补入已生成的 progress input/procedure 索引,避免 procedure 文件存在但 `RemoteProcedures` 扩展 trait 未进入作用域。 +4. `api-server` 只依赖 facade,不直接碰生成绑定,保持 HTTP 层与 SpacetimeDB 生成类型隔离。 + +补充验证: + +```bash +cargo fmt -p spacetime-client -p api-server +cargo check -p api-server --bin api-server +npm run check:encoding +``` + +结果:`api-server` 编译通过,编码检查通过;剩余 warning 为既有 dead code。 diff --git a/docs/technical/PUZZLE_SINGLE_PLAYER_AND_REAL_IMAGE_PLAN_2026-04-24.md b/docs/technical/PUZZLE_SINGLE_PLAYER_AND_REAL_IMAGE_PLAN_2026-04-24.md new file mode 100644 index 00000000..c2a78538 --- /dev/null +++ b/docs/technical/PUZZLE_SINGLE_PLAYER_AND_REAL_IMAGE_PLAN_2026-04-24.md @@ -0,0 +1,68 @@ +# 拼图玩法单机运行态与真实图片生成方案 2026-04-24 + +## 1. 本次收口目标 + +这次收口只做两件事: + +1. 拼图结果页中的候选图生成不再返回本地 SVG 占位图,而是接入 Rust `api-server` 现有的真实外部生图链。 +2. 拼图第一版运行态改为单机本地运行,不再把交换、拖动、通关进度和下一关状态保存到后端。 + +## 2. 第一版单机范围 + +第一版“单机版本”的准确定义如下: + +1. 玩家从拼图广场或作品详情进入玩法时,前端基于作品详情在本地构造一次 `PuzzleRunSnapshot`。 +2. 交换拼图块、拖动拼图块、关卡是否拼完,全部由前端本地计算。 +3. 本地运行态不调用 `/api/runtime/puzzle/runs/*` 写回当前过程状态。 +4. 关闭玩法后,这次运行态直接失效,不做断点续玩,不做跨端同步。 +5. 后端仍然负责: + - Agent 会话 + - 结果页草稿编译 + - 正式候选图生成 + - 封面确认 + - 作品发布 + - 作品列表 / 详情 / 广场读取 + +这意味着第一版拼图玩法是“创作后端化、游玩本地化”的结构,而不是“所有状态都走后端”的结构。 + +## 3. 真实图片生成链 + +拼图正式候选图统一复用当前仓库已经跑通的 Rust 资产主链: + +1. `api-server` 根据拼图草稿的关卡名和结果页 prompt 组装正式文生图 prompt。 +2. 调用 DashScope 文生图接口创建异步任务。 +3. 轮询任务直到拿到正式图片地址。 +4. 下载图片二进制。 +5. 上传到私有 OSS。 +6. 在 `module-assets` / `spacetime-client` 的资产真相链中确认对象并绑定到拼图实体。 +7. 对前端返回 `/generated-puzzle-assets/*` 兼容路径,而不是本地 `svg` 占位路径。 + +## 4. 路径与边界 + +### 4.1 候选图输出路径 + +拼图正式候选图统一使用: + +`/generated-puzzle-assets/*` + +不能继续写到仓库本地 `public/generated-puzzle-covers/*`。 + +### 4.2 运行态边界 + +第一版单机运行态保留现有 DTO 结构,目的是不重做界面层。 + +但 DTO 的来源变化为: + +1. 进入玩法时从作品详情构造本地 `run` +2. 交换 / 拖动 / 通关时由前端工具函数返回新的 `run` +3. 当前不依赖后端 `start/swap/drag/next-level` 接口完成主链 + +## 5. 当前实现判断标准 + +当下面结果成立时,视为这一轮目标达成: + +1. `generate_puzzle_images` 返回的 `imageSrc` 不再是本地 `svg` 占位图。 +2. 返回路径切到 `/generated-puzzle-assets/*`。 +3. 未配置 DashScope 或 OSS 时,接口明确返回 provider 级错误,而不是静默回退占位图。 +4. 玩家进入拼图玩法后,即使后端运行态接口不可用,也能在本地完成交换与拖动。 +5. 关闭玩法后不保留当前 run 进度。 diff --git a/docs/technical/SPACETIMEDB_MAINCLOUD_PUBLISH_2026-04-24.md b/docs/technical/SPACETIMEDB_MAINCLOUD_PUBLISH_2026-04-24.md new file mode 100644 index 00000000..2abfcf22 --- /dev/null +++ b/docs/technical/SPACETIMEDB_MAINCLOUD_PUBLISH_2026-04-24.md @@ -0,0 +1,59 @@ +# SpacetimeDB Maincloud 发布与 api-server 适配方案 + +## 目标 + +新增一条明确的 npm 命令链,用于把 `server-rs/crates/spacetime-module` 发布到 SpacetimeDB Maincloud,并让 `api-server` 可以使用同一套 Maincloud 数据库配置启动。 + +## 环境变量约定 + +Maincloud 发布不复用本地 `spacetime.local.json`,避免误把本地开发库名发布到云端。需要显式提供: + +| 变量 | 用途 | +| --- | --- | +| `GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE` | Maincloud 数据库名,发布脚本优先读取 | +| `GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL` | Maincloud 服务地址,默认 `https://maincloud.spacetimedb.com` | +| `GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN` | `api-server` 连接 Maincloud 时使用的 token | + +兼容 `api-server` 现有变量: + +| 变量 | 用途 | +| --- | --- | +| `GENARRATIVE_SPACETIME_SERVER_URL` | `api-server` 实际连接地址 | +| `GENARRATIVE_SPACETIME_DATABASE` | `api-server` 实际连接数据库 | +| `GENARRATIVE_SPACETIME_TOKEN` | `api-server` 实际连接 token | + +## npm 命令 + +```bash +npm run spacetime:publish:maincloud +``` + +执行内容: + +1. 使用 `cargo build -p spacetime-module --target wasm32-unknown-unknown --release` 构建 wasm。 +2. 使用 `spacetime publish --server maincloud --bin-path --yes` 发布到 Maincloud。 +3. 输出 `api-server` 需要的 Maincloud 环境变量,便于部署进程复用。 + +如需 schema 冲突时清库发布: + +```bash +npm run spacetime:publish:maincloud -- --clear-database +``` + +## api-server 启动 + +```bash +npm run api-server:maincloud +``` + +执行内容: + +1. 从 `.env` 与 `.env.local` 读取默认环境。 +2. 将 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 映射为 `api-server` 已支持的 `GENARRATIVE_SPACETIME_*`。 +3. 启动 `cargo run -p api-server --manifest-path server-rs/Cargo.toml`。 + +## 设计约束 + +- Maincloud 数据库名必须显式配置,不能默认读取本地 `spacetime.local.json`。 +- 发布脚本只处理 SpacetimeDB 模块发布,不启动本地 SpacetimeDB。 +- `api-server` 继续通过 `SpacetimeClientConfig` 的 `server_url / database / token` 连接数据库,不在前端增加逻辑。 diff --git a/package.json b/package.json index 59469601..a4d6c994 100644 --- a/package.json +++ b/package.json @@ -7,8 +7,10 @@ "dev": "node scripts/dev-node.mjs", "dev:rust": "node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh", "dev:rust:logs": "node scripts/run-bash-script.mjs scripts/spacetime-logs-local.sh", - "dev:web": "node scripts/vite-cli.mjs --port=3000 --host=0.0.0.0", + "dev:web": "node scripts/dev-web-rust.mjs", "dev:node": "node scripts/dev-node.mjs", + "spacetime:publish:maincloud": "node scripts/run-bash-script.mjs scripts/spacetime-publish-maincloud.sh", + "api-server:maincloud": "node scripts/api-server-maincloud.mjs", "deploy:rust:remote": "node scripts/run-bash-script.mjs scripts/deploy-rust-remote.sh", "build:rust:ubuntu": "node scripts/run-bash-script.mjs scripts/deploy-rust-remote.sh", "serve:caddy": "node scripts/run-caddy-dev.mjs", diff --git a/scripts/api-server-maincloud.mjs b/scripts/api-server-maincloud.mjs new file mode 100644 index 00000000..09688655 --- /dev/null +++ b/scripts/api-server-maincloud.mjs @@ -0,0 +1,86 @@ +import {spawn} from 'node:child_process'; +import {existsSync, readFileSync} from 'node:fs'; +import {resolve} from 'node:path'; + +const repoRoot = process.cwd(); + +function loadEnvFile(path, target) { + if (!existsSync(path)) { + return; + } + + const rawText = readFileSync(path, 'utf8'); + for (const rawLine of rawText.split(/\r?\n/u)) { + const line = rawLine.trim(); + if (!line || line.startsWith('#')) { + continue; + } + + const match = line.match(/^([A-Za-z_][A-Za-z0-9_]*)=(.*)$/u); + if (!match) { + continue; + } + + const [, key, rawValue] = match; + if (target[key] !== undefined) { + continue; + } + + target[key] = rawValue.replace(/^['"]|['"]$/gu, ''); + } +} + +const mergedEnv = {...process.env}; +loadEnvFile(resolve(repoRoot, '.env'), mergedEnv); +loadEnvFile(resolve(repoRoot, '.env.local'), mergedEnv); + +mergedEnv.GENARRATIVE_API_HOST = mergedEnv.GENARRATIVE_API_HOST || '127.0.0.1'; +mergedEnv.GENARRATIVE_API_PORT = mergedEnv.GENARRATIVE_API_PORT || '3100'; +mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL = + mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL || + mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL || + 'https://maincloud.spacetimedb.com'; +mergedEnv.GENARRATIVE_SPACETIME_DATABASE = + mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE || + mergedEnv.GENARRATIVE_SPACETIME_DATABASE || + ''; +mergedEnv.GENARRATIVE_SPACETIME_TOKEN = + mergedEnv.GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN || + mergedEnv.GENARRATIVE_SPACETIME_TOKEN || + ''; + +if (!mergedEnv.GENARRATIVE_SPACETIME_DATABASE) { + console.error( + '[api-server:maincloud] 缺少 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE 或 GENARRATIVE_SPACETIME_DATABASE。', + ); + process.exit(1); +} + +console.log( + `[api-server:maincloud] SpacetimeDB ${mergedEnv.GENARRATIVE_SPACETIME_DATABASE} @ ${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`, +); + +const child = spawn( + 'cargo', + ['run', '-p', 'api-server', '--manifest-path', 'server-rs/Cargo.toml'], + { + cwd: repoRoot, + env: mergedEnv, + stdio: 'inherit', + shell: process.platform === 'win32', + }, +); + +child.on('error', (error) => { + console.error(`[api-server:maincloud] 启动 cargo 失败: ${error.message}`); + process.exit(1); +}); + +child.on('exit', (code, signal) => { + if (signal) { + console.error(`[api-server:maincloud] api-server 被信号终止: ${signal}`); + process.exit(1); + } + + process.exit(code ?? 0); +}); diff --git a/scripts/dev-web-rust.mjs b/scripts/dev-web-rust.mjs new file mode 100644 index 00000000..5c76eb2b --- /dev/null +++ b/scripts/dev-web-rust.mjs @@ -0,0 +1,40 @@ +import {spawn} from 'node:child_process'; + +const mergedEnv = { + ...process.env, + GENARRATIVE_BACKEND_STACK: process.env.GENARRATIVE_BACKEND_STACK || 'rust', + RUST_SERVER_TARGET: + process.env.RUST_SERVER_TARGET || + process.env.GENARRATIVE_API_TARGET || + `http://127.0.0.1:${process.env.GENARRATIVE_API_PORT || '3100'}`, +}; + +mergedEnv.GENARRATIVE_RUNTIME_SERVER_TARGET = + process.env.GENARRATIVE_RUNTIME_SERVER_TARGET || mergedEnv.RUST_SERVER_TARGET; + +console.log(`[dev:web] backend=rust target=${mergedEnv.GENARRATIVE_RUNTIME_SERVER_TARGET}`); + +const child = spawn( + 'node', + ['scripts/vite-cli.mjs', '--port=3000', '--host=0.0.0.0'], + { + cwd: process.cwd(), + env: mergedEnv, + stdio: 'inherit', + shell: process.platform === 'win32', + }, +); + +child.on('error', (error) => { + console.error(`[dev:web] 启动 Vite 失败: ${error.message}`); + process.exit(1); +}); + +child.on('exit', (code, signal) => { + if (signal) { + console.error(`[dev:web] Vite 被信号终止: ${signal}`); + process.exit(1); + } + + process.exit(code ?? 0); +}); diff --git a/scripts/spacetime-publish-maincloud.sh b/scripts/spacetime-publish-maincloud.sh new file mode 100644 index 00000000..805f67d9 --- /dev/null +++ b/scripts/spacetime-publish-maincloud.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +SERVER_RS_DIR="${REPO_ROOT}/server-rs" +MODULE_PATH="${SERVER_RS_DIR}/target/wasm32-unknown-unknown/release/spacetime_module.wasm" +SPACETIME_SERVER_ALIAS="maincloud" +CLEAR_DATABASE=0 + +load_env_file() { + local env_file="$1" + local line key value + + if [[ ! -f "${env_file}" ]]; then + return + fi + + while IFS= read -r line || [[ -n "${line}" ]]; do + line="${line%$'\r'}" + line="${line#$'\xef\xbb\xbf'}" + [[ -z "${line}" || "${line}" == \#* ]] && continue + [[ "${line}" =~ ^([A-Za-z_][A-Za-z0-9_]*)=(.*)$ ]] || continue + key="${BASH_REMATCH[1]}" + value="${BASH_REMATCH[2]}" + value="${value%\"}" + value="${value#\"}" + value="${value%\'}" + value="${value#\'}" + if [[ -z "${!key+x}" ]]; then + export "${key}=${value}" + fi + done <"${env_file}" +} + +usage() { + cat <<'EOF' +用法: + npm run spacetime:publish:maincloud + npm run spacetime:publish:maincloud -- --database + npm run spacetime:publish:maincloud -- --clear-database + +说明: + 发布 server-rs/crates/spacetime-module 到 SpacetimeDB Maincloud。 + 数据库名优先读取 --database,其次读取 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE。 +EOF +} + +load_env_file "${REPO_ROOT}/.env" +load_env_file "${REPO_ROOT}/.env.local" + +SPACETIME_DATABASE="${GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE:-}" +SPACETIME_SERVER_URL="${GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL:-https://maincloud.spacetimedb.com}" + +while [[ $# -gt 0 ]]; do + case "$1" in + -h|--help) + usage + exit 0 + ;; + --database) + SPACETIME_DATABASE="${2:?缺少 --database 的值}" + shift 2 + ;; + --server-url) + SPACETIME_SERVER_URL="${2:?缺少 --server-url 的值}" + shift 2 + ;; + --clear-database) + CLEAR_DATABASE=1 + shift + ;; + *) + echo "[spacetime:maincloud] 未知参数: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +if [[ -z "${SPACETIME_DATABASE}" ]]; then + echo "[spacetime:maincloud] 缺少 GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE。" >&2 + echo "[spacetime:maincloud] 请在 .env.local 中配置,或通过 --database 传入。" >&2 + exit 1 +fi + +if ! command -v cargo >/dev/null 2>&1; then + echo "[spacetime:maincloud] 缺少 cargo 命令。" >&2 + exit 1 +fi + +if ! command -v spacetime >/dev/null 2>&1; then + echo "[spacetime:maincloud] 缺少 spacetime CLI,请先安装并登录 Maincloud。" >&2 + exit 1 +fi + +echo "[spacetime:maincloud] 构建 spacetime-module wasm" +cargo build \ + --manifest-path "${SERVER_RS_DIR}/Cargo.toml" \ + -p spacetime-module \ + --target wasm32-unknown-unknown \ + --release + +PUBLISH_ARGS=( + publish + "${SPACETIME_DATABASE}" + --server "${SPACETIME_SERVER_ALIAS}" + --bin-path "${MODULE_PATH}" + --yes +) + +if [[ "${CLEAR_DATABASE}" -eq 1 ]]; then + # Maincloud 清库只在 schema 冲突时触发,避免无冲突升级误删线上数据。 + PUBLISH_ARGS+=(-c=on-conflict) +fi + +echo "[spacetime:maincloud] 发布 SpacetimeDB wasm: ${SPACETIME_DATABASE} -> ${SPACETIME_SERVER_ALIAS}" +spacetime "${PUBLISH_ARGS[@]}" + +cat < String { #[cfg(test)] mod tests { use super::*; + use serde_json::json; #[test] fn character_expansion_prompt_keeps_node_contract_text() { diff --git a/server-rs/crates/api-server/src/puzzle.rs b/server-rs/crates/api-server/src/puzzle.rs index 9d356d17..a327bfc7 100644 --- a/server-rs/crates/api-server/src/puzzle.rs +++ b/server-rs/crates/api-server/src/puzzle.rs @@ -16,6 +16,7 @@ use module_assets::{ AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input, build_asset_object_upsert_input, generate_asset_binding_id, generate_asset_object_id, }; +use module_puzzle::PuzzleGeneratedImageCandidate; use platform_oss::{LegacyAssetPrefix, OssHeadObjectRequest, OssObjectAccess, OssPutObjectRequest}; use serde_json::{Map, Value, json}; use shared_contracts::{ @@ -1463,21 +1464,6 @@ struct PuzzleDownloadedImage { bytes: Vec, } -fn to_puzzle_generated_image_candidate( - candidate: &PuzzleGeneratedImageCandidateRecord, -) -> PuzzleGeneratedImageCandidate { - // SpacetimeDB ???????? module-puzzle ??????????? snake_case ????HTTP ????????? camelCase? - PuzzleGeneratedImageCandidate { - candidate_id: candidate.candidate_id.clone(), - image_src: candidate.image_src.clone(), - asset_id: candidate.asset_id.clone(), - prompt: candidate.prompt.clone(), - actual_prompt: candidate.actual_prompt.clone(), - source_type: candidate.source_type.clone(), - selected: candidate.selected, - } -} - struct GeneratedPuzzleAssetResponse { image_src: String, asset_id: String, @@ -1526,6 +1512,21 @@ fn build_puzzle_dashscope_http_client( }) } +fn to_puzzle_generated_image_candidate( + candidate: &PuzzleGeneratedImageCandidateRecord, +) -> PuzzleGeneratedImageCandidate { + // SpacetimeDB 模块反序列化的是 module-puzzle 的持久化结构,必须保留 snake_case 字段名;HTTP 响应层再单独映射为 camelCase。 + PuzzleGeneratedImageCandidate { + candidate_id: candidate.candidate_id.clone(), + image_src: candidate.image_src.clone(), + asset_id: candidate.asset_id.clone(), + prompt: candidate.prompt.clone(), + actual_prompt: candidate.actual_prompt.clone(), + source_type: candidate.source_type.clone(), + selected: candidate.selected, + } +} + async fn create_puzzle_text_to_image_generation( http_client: &reqwest::Client, settings: &PuzzleDashScopeSettings, diff --git a/server-rs/crates/spacetime-client/src/custom_world.rs b/server-rs/crates/spacetime-client/src/custom_world.rs index 3f651ed5..c718ea90 100644 --- a/server-rs/crates/spacetime-client/src/custom_world.rs +++ b/server-rs/crates/spacetime-client/src/custom_world.rs @@ -436,6 +436,41 @@ impl SpacetimeClient { .await } + pub async fn upsert_custom_world_agent_operation_progress( + &self, + input: CustomWorldAgentOperationProgressRecordInput, + ) -> Result { + let procedure_input = CustomWorldAgentOperationProgressInput { + session_id: input.session_id, + owner_user_id: input.owner_user_id, + operation_id: input.operation_id, + operation_type: parse_rpg_agent_operation_type_record(input.operation_type.as_str())?, + operation_status: parse_rpg_agent_operation_status_record( + input.operation_status.as_str(), + )?, + phase_label: input.phase_label, + phase_detail: input.phase_detail, + operation_progress: input.operation_progress, + error_message: input.error_message, + updated_at_micros: input.updated_at_micros, + }; + + self.call_after_connect(move |connection, sender| { + connection + .procedures() + .upsert_custom_world_agent_operation_progress_then( + procedure_input, + move |_, result| { + let mapped = result + .map_err(|error| SpacetimeClientError::Procedure(error.to_string())) + .and_then(map_custom_world_agent_operation_procedure_result); + send_once(&sender, mapped); + }, + ); + }) + .await + } + pub async fn get_custom_world_agent_operation( &self, session_id: String, diff --git a/server-rs/crates/spacetime-client/src/lib.rs b/server-rs/crates/spacetime-client/src/lib.rs index f8451a57..7c2b50d6 100644 --- a/server-rs/crates/spacetime-client/src/lib.rs +++ b/server-rs/crates/spacetime-client/src/lib.rs @@ -4,7 +4,40 @@ pub mod module_bindings; mod mapper; pub(crate) use mapper::*; -pub use mapper::{BattleStateRecord, ResolveCombatActionRecord, CustomWorldLibraryEntryRecord, CustomWorldGalleryEntryRecord, CustomWorldLibraryMutationRecord, CustomWorldPublishedProfileCompileRecord, CustomWorldPublishWorldRecord, CustomWorldAgentMessageRecord, CustomWorldAgentOperationRecord, CustomWorldDraftCardRecord, CustomWorldSupportedActionRecord, CustomWorldCheckpointRecord, CustomWorldAgentCheckpointRecord, CustomWorldResultPreviewBlockerRecord, CustomWorldPublishGateRecord, CustomWorldWorkSummaryRecord, CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardDetailRecord, CustomWorldAgentSessionRecord, CustomWorldProfileUpsertRecordInput, CustomWorldPublishWorldRecordInput, CustomWorldAgentSessionCreateRecordInput, CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentActionExecuteRecord, PuzzleAgentSessionCreateRecordInput, PuzzleAgentMessageSubmitRecordInput, PuzzleAgentMessageFinalizeRecordInput, PuzzleGeneratedImagesSaveRecordInput, PuzzleSelectCoverImageRecordInput, PuzzlePublishRecordInput, PuzzleWorkUpsertRecordInput, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, PuzzleCreatorIntentRecord, PuzzleGeneratedImageCandidateRecord, PuzzleResultDraftRecord, PuzzleAgentMessageRecord, PuzzleAgentSuggestedActionRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, PuzzleResultPreviewRecord, PuzzleAgentSessionRecord, PuzzleWorkProfileRecord, PuzzleCellPositionRecord, PuzzlePieceStateRecord, PuzzleMergedGroupRecord, PuzzleBoardRecord, PuzzleRuntimeLevelRecord, PuzzleRunRecord, BigFishSessionCreateRecordInput, BigFishMessageSubmitRecordInput, BigFishMessageFinalizeRecordInput, BigFishAssetGenerateRecordInput, BigFishRunStartRecordInput, BigFishRunInputSubmitRecordInput, BigFishAnchorItemRecord, BigFishAnchorPackRecord, BigFishLevelBlueprintRecord, BigFishBackgroundBlueprintRecord, BigFishRuntimeParamsRecord, BigFishGameDraftRecord, BigFishAgentMessageRecord, BigFishAssetSlotRecord, BigFishAssetCoverageRecord, BigFishSessionRecord, BigFishWorkSummaryRecord, BigFishVector2Record, BigFishRuntimeEntityRecord, BigFishRuntimeRecord, ResolveNpcBattleInteractionInput, AiTaskStageRecord, AiResultReferenceRecord, AiTextChunkRecord, AiTaskRecord, AiTaskMutationRecord, NpcStateRecord, NpcInteractionRecord, NpcBattleInteractionRecord}; +pub use mapper::{ + AiResultReferenceRecord, AiTaskMutationRecord, AiTaskRecord, AiTaskStageRecord, + AiTextChunkRecord, BattleStateRecord, BigFishAgentMessageRecord, BigFishAnchorItemRecord, + BigFishAnchorPackRecord, BigFishAssetCoverageRecord, BigFishAssetGenerateRecordInput, + BigFishAssetSlotRecord, BigFishBackgroundBlueprintRecord, BigFishGameDraftRecord, + BigFishLevelBlueprintRecord, BigFishMessageFinalizeRecordInput, + BigFishMessageSubmitRecordInput, BigFishRunInputSubmitRecordInput, BigFishRunStartRecordInput, + BigFishRuntimeEntityRecord, BigFishRuntimeParamsRecord, BigFishRuntimeRecord, + BigFishSessionCreateRecordInput, BigFishSessionRecord, BigFishVector2Record, + BigFishWorkSummaryRecord, CustomWorldAgentActionExecuteRecord, + CustomWorldAgentActionExecuteRecordInput, CustomWorldAgentCheckpointRecord, + CustomWorldAgentMessageFinalizeRecordInput, CustomWorldAgentMessageRecord, + CustomWorldAgentMessageSubmitRecordInput, CustomWorldAgentOperationProgressRecordInput, + CustomWorldAgentOperationRecord, CustomWorldAgentSessionCreateRecordInput, + CustomWorldAgentSessionRecord, CustomWorldCheckpointRecord, CustomWorldDraftCardDetailRecord, + CustomWorldDraftCardDetailSectionRecord, CustomWorldDraftCardRecord, + CustomWorldGalleryEntryRecord, CustomWorldLibraryEntryRecord, CustomWorldLibraryMutationRecord, + CustomWorldProfileUpsertRecordInput, CustomWorldPublishGateRecord, + CustomWorldPublishWorldRecord, CustomWorldPublishWorldRecordInput, + CustomWorldPublishedProfileCompileRecord, CustomWorldResultPreviewBlockerRecord, + CustomWorldSupportedActionRecord, CustomWorldWorkSummaryRecord, NpcBattleInteractionRecord, + NpcInteractionRecord, NpcStateRecord, PuzzleAgentMessageFinalizeRecordInput, + PuzzleAgentMessageRecord, PuzzleAgentMessageSubmitRecordInput, + PuzzleAgentSessionCreateRecordInput, PuzzleAgentSessionRecord, + PuzzleAgentSuggestedActionRecord, PuzzleAnchorItemRecord, PuzzleAnchorPackRecord, + PuzzleBoardRecord, PuzzleCellPositionRecord, PuzzleCreatorIntentRecord, + PuzzleGeneratedImageCandidateRecord, PuzzleGeneratedImagesSaveRecordInput, + PuzzleMergedGroupRecord, PuzzlePieceStateRecord, PuzzlePublishRecordInput, + PuzzleResultDraftRecord, PuzzleResultPreviewBlockerRecord, PuzzleResultPreviewFindingRecord, + PuzzleResultPreviewRecord, PuzzleRunDragRecordInput, PuzzleRunNextLevelRecordInput, + PuzzleRunRecord, PuzzleRunStartRecordInput, PuzzleRunSwapRecordInput, PuzzleRuntimeLevelRecord, + PuzzleSelectCoverImageRecordInput, PuzzleWorkProfileRecord, PuzzleWorkUpsertRecordInput, + ResolveCombatActionRecord, ResolveNpcBattleInteractionInput, +}; pub mod ai; pub mod assets; diff --git a/server-rs/crates/spacetime-client/src/mapper.rs b/server-rs/crates/spacetime-client/src/mapper.rs index 96cb0c0b..33cfaf68 100644 --- a/server-rs/crates/spacetime-client/src/mapper.rs +++ b/server-rs/crates/spacetime-client/src/mapper.rs @@ -2737,6 +2737,41 @@ pub(crate) fn format_rpg_agent_operation_status( } } +pub(crate) fn parse_rpg_agent_operation_type_record( + value: &str, +) -> Result { + match value.trim() { + "process_message" => Ok(crate::module_bindings::RpgAgentOperationType::ProcessMessage), + "draft_foundation" => Ok(crate::module_bindings::RpgAgentOperationType::DraftFoundation), + "update_draft_card" => Ok(crate::module_bindings::RpgAgentOperationType::UpdateDraftCard), + "sync_result_profile" => { + Ok(crate::module_bindings::RpgAgentOperationType::SyncResultProfile) + } + "generate_characters" => { + Ok(crate::module_bindings::RpgAgentOperationType::GenerateCharacters) + } + "generate_landmarks" => { + Ok(crate::module_bindings::RpgAgentOperationType::GenerateLandmarks) + } + "generate_role_assets" => { + Ok(crate::module_bindings::RpgAgentOperationType::GenerateRoleAssets) + } + "sync_role_assets" => Ok(crate::module_bindings::RpgAgentOperationType::SyncRoleAssets), + "generate_scene_assets" => { + Ok(crate::module_bindings::RpgAgentOperationType::GenerateSceneAssets) + } + "sync_scene_assets" => Ok(crate::module_bindings::RpgAgentOperationType::SyncSceneAssets), + "expand_long_tail" => Ok(crate::module_bindings::RpgAgentOperationType::ExpandLongTail), + "publish_world" => Ok(crate::module_bindings::RpgAgentOperationType::PublishWorld), + "revert_checkpoint" => Ok(crate::module_bindings::RpgAgentOperationType::RevertCheckpoint), + "delete_characters" => Ok(crate::module_bindings::RpgAgentOperationType::DeleteCharacters), + "delete_landmarks" => Ok(crate::module_bindings::RpgAgentOperationType::DeleteLandmarks), + other => Err(SpacetimeClientError::Runtime(format!( + "未知 rpg agent operation type: {other}" + ))), + } +} + pub(crate) fn parse_rpg_agent_operation_status_record( value: &str, ) -> Result { @@ -3686,6 +3721,20 @@ pub struct CustomWorldAgentMessageFinalizeRecordInput { pub updated_at_micros: i64, } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct CustomWorldAgentOperationProgressRecordInput { + pub session_id: String, + pub owner_user_id: String, + pub operation_id: String, + pub operation_type: String, + pub operation_status: String, + pub phase_label: String, + pub phase_detail: String, + pub operation_progress: u32, + pub error_message: Option, + pub updated_at_micros: i64, +} + #[derive(Clone, Debug, PartialEq, Eq)] pub struct CustomWorldAgentActionExecuteRecordInput { pub session_id: String, 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 a26b146b..20d9f94f 100644 --- a/server-rs/crates/spacetime-client/src/module_bindings/mod.rs +++ b/server-rs/crates/spacetime-client/src/module_bindings/mod.rs @@ -114,6 +114,7 @@ pub mod custom_world_agent_message_snapshot_type; pub mod custom_world_agent_message_submit_input_type; pub mod custom_world_agent_operation_type; pub mod custom_world_agent_operation_get_input_type; +pub mod custom_world_agent_operation_progress_input_type; pub mod custom_world_agent_operation_procedure_result_type; pub mod custom_world_agent_operation_snapshot_type; pub mod custom_world_agent_session_type; @@ -339,6 +340,7 @@ pub mod start_ai_task_reducer; pub mod start_ai_task_stage_reducer; pub mod turn_in_quest_reducer; pub mod unpublish_custom_world_profile_reducer; +pub mod upsert_custom_world_agent_operation_progress_procedure; pub mod upsert_chapter_progression_reducer; pub mod upsert_custom_world_profile_reducer; pub mod upsert_npc_state_reducer; @@ -579,6 +581,7 @@ pub use custom_world_agent_message_snapshot_type::CustomWorldAgentMessageSnapsho pub use custom_world_agent_message_submit_input_type::CustomWorldAgentMessageSubmitInput; pub use custom_world_agent_operation_type::CustomWorldAgentOperation; pub use custom_world_agent_operation_get_input_type::CustomWorldAgentOperationGetInput; +pub use custom_world_agent_operation_progress_input_type::CustomWorldAgentOperationProgressInput; pub use custom_world_agent_operation_procedure_result_type::CustomWorldAgentOperationProcedureResult; pub use custom_world_agent_operation_snapshot_type::CustomWorldAgentOperationSnapshot; pub use custom_world_agent_session_type::CustomWorldAgentSession; @@ -846,6 +849,7 @@ pub use start_ai_task_reducer::start_ai_task; pub use start_ai_task_stage_reducer::start_ai_task_stage; pub use turn_in_quest_reducer::turn_in_quest; pub use unpublish_custom_world_profile_reducer::unpublish_custom_world_profile; +pub use upsert_custom_world_agent_operation_progress_procedure::upsert_custom_world_agent_operation_progress; pub use upsert_chapter_progression_reducer::upsert_chapter_progression; pub use upsert_custom_world_profile_reducer::upsert_custom_world_profile; pub use upsert_npc_state_reducer::upsert_npc_state;