添加maincloud发布
This commit is contained in:
@@ -203,6 +203,8 @@ spacetime server ping <server>
|
||||
```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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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。
|
||||
|
||||
@@ -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 进度。
|
||||
59
docs/technical/SPACETIMEDB_MAINCLOUD_PUBLISH_2026-04-24.md
Normal file
59
docs/technical/SPACETIMEDB_MAINCLOUD_PUBLISH_2026-04-24.md
Normal file
@@ -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 <database> --server maincloud --bin-path <wasm> --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` 连接数据库,不在前端增加逻辑。
|
||||
@@ -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",
|
||||
|
||||
86
scripts/api-server-maincloud.mjs
Normal file
86
scripts/api-server-maincloud.mjs
Normal file
@@ -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);
|
||||
});
|
||||
40
scripts/dev-web-rust.mjs
Normal file
40
scripts/dev-web-rust.mjs
Normal file
@@ -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);
|
||||
});
|
||||
125
scripts/spacetime-publish-maincloud.sh
Normal file
125
scripts/spacetime-publish-maincloud.sh
Normal file
@@ -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 <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 <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 <<EOF
|
||||
[spacetime:maincloud] 发布完成。api-server 可使用以下环境:
|
||||
GENARRATIVE_SPACETIME_SERVER_URL=${SPACETIME_SERVER_URL}
|
||||
GENARRATIVE_SPACETIME_DATABASE=${SPACETIME_DATABASE}
|
||||
GENARRATIVE_SPACETIME_TOKEN=
|
||||
EOF
|
||||
@@ -394,19 +394,26 @@ impl AppConfig {
|
||||
config.oss_success_action_status = oss_success_action_status;
|
||||
}
|
||||
|
||||
if let Some(spacetime_server_url) =
|
||||
read_first_non_empty_env(&["GENARRATIVE_SPACETIME_SERVER_URL"])
|
||||
if let Some(spacetime_server_url) = read_first_non_empty_env(&[
|
||||
"GENARRATIVE_SPACETIME_SERVER_URL",
|
||||
"GENARRATIVE_SPACETIME_MAINCLOUD_SERVER_URL",
|
||||
])
|
||||
{
|
||||
config.spacetime_server_url = spacetime_server_url;
|
||||
}
|
||||
|
||||
if let Some(spacetime_database) =
|
||||
read_first_non_empty_env(&["GENARRATIVE_SPACETIME_DATABASE"])
|
||||
if let Some(spacetime_database) = read_first_non_empty_env(&[
|
||||
"GENARRATIVE_SPACETIME_DATABASE",
|
||||
"GENARRATIVE_SPACETIME_MAINCLOUD_DATABASE",
|
||||
])
|
||||
{
|
||||
config.spacetime_database = spacetime_database;
|
||||
}
|
||||
|
||||
config.spacetime_token = read_first_non_empty_env(&["GENARRATIVE_SPACETIME_TOKEN"]);
|
||||
config.spacetime_token = read_first_non_empty_env(&[
|
||||
"GENARRATIVE_SPACETIME_TOKEN",
|
||||
"GENARRATIVE_SPACETIME_MAINCLOUD_TOKEN",
|
||||
]);
|
||||
if let Some(spacetime_pool_size) =
|
||||
read_first_positive_u32_env(&["GENARRATIVE_SPACETIME_POOL_SIZE"])
|
||||
{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use platform_llm::{LlmClient, LlmMessage, LlmTextRequest};
|
||||
use serde_json::{Map as JsonMap, Value as JsonValue, json};
|
||||
use serde_json::{Map as JsonMap, Value as JsonValue};
|
||||
use shared_contracts::runtime::ExecuteCustomWorldAgentActionRequest;
|
||||
use spacetime_client::CustomWorldAgentSessionRecord;
|
||||
|
||||
@@ -488,6 +488,7 @@ fn create_stable_id(prefix: &str, name: &str, index: usize) -> String {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn character_expansion_prompt_keeps_node_contract_text() {
|
||||
|
||||
@@ -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<u8>,
|
||||
}
|
||||
|
||||
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,
|
||||
|
||||
@@ -436,6 +436,41 @@ impl SpacetimeClient {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn upsert_custom_world_agent_operation_progress(
|
||||
&self,
|
||||
input: CustomWorldAgentOperationProgressRecordInput,
|
||||
) -> Result<CustomWorldAgentOperationRecord, SpacetimeClientError> {
|
||||
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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -2737,6 +2737,41 @@ pub(crate) fn format_rpg_agent_operation_status(
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn parse_rpg_agent_operation_type_record(
|
||||
value: &str,
|
||||
) -> Result<crate::module_bindings::RpgAgentOperationType, SpacetimeClientError> {
|
||||
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<crate::module_bindings::RpgAgentOperationStatus, SpacetimeClientError> {
|
||||
@@ -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<String>,
|
||||
pub updated_at_micros: i64,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct CustomWorldAgentActionExecuteRecordInput {
|
||||
pub session_id: String,
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user