1
CI / verify (push) Has been cancelled

This commit is contained in:
2026-04-26 21:07:55 +08:00
609 changed files with 4601 additions and 14501 deletions
@@ -87,7 +87,7 @@
4. 重新执行:
```powershell
spacetime generate --no-config --lang rust --out-dir D:\Genarrative\server-rs\crates\spacetime-client\src\module_bindings --module-path D:\Genarrative\server-rs\crates\spacetime-module --include-private --yes
npm run spacetime:generate
```
说明:
@@ -107,7 +107,7 @@
```bash
spacetime start --listen-addr 127.0.0.1:3000
spacetime publish genarrative-dev --server local --yes --module-path server-rs/crates/spacetime-module
spacetime generate --lang rust --out-dir server-rs/crates/spacetime-client/src/module_bindings --module-path server-rs/crates/spacetime-module --include-private --yes
npm run spacetime:generate
```
## 5. 为什么当前阶段不用 CLI 文本解析
@@ -195,7 +195,7 @@
1. `cargo check -p module-story --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`
2. `cargo check -p spacetime-module --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`
3. `cargo check -p spacetime-module --target wasm32-unknown-unknown --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`
4. `spacetime generate --no-config --lang rust --out-dir D:\\Genarrative\\server-rs\\crates\\spacetime-client\\src\\module_bindings --module-path D:\\Genarrative\\server-rs\\crates\\spacetime-module --include-private --yes`
4. `npm run spacetime:generate`
5. `cargo check -p spacetime-client --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`
6. `cargo check -p api-server --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml`
7. `npm run check:encoding`
@@ -234,7 +234,7 @@
同时,本轮还完成了以下工程收口:
1. 已重新执行 `spacetime generate --no-config --lang rust --out-dir D:\\Genarrative\\server-rs\\crates\\spacetime-client\\src\\module_bindings --module-path D:\\Genarrative\\server-rs\\crates\\spacetime-module --include-private --yes`
1. 已重新执行 `npm run spacetime:generate`
2. 已把 `spacetime-client` 中 battle query 的占位实现替换为真实 procedure 调用。
3. 已再次执行 `cargo check -p spacetime-client --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml``cargo check -p api-server --manifest-path D:\\Genarrative\\server-rs\\Cargo.toml` 并通过。
@@ -340,7 +340,7 @@ finalScore = tagSimilarityScore * 0.7 + sameAuthorScore * 0.3;
4. `cargo check -p module-puzzle`
5. `cargo check -p shared-contracts`
6. `cargo check -p spacetime-module`
7. `spacetime generate --no-config --lang rust --out-dir server-rs/crates/spacetime-client/src/module_bindings --module-path server-rs/crates/spacetime-module --include-private --yes`
7. `npm run spacetime:generate`
8. `cargo check -p spacetime-client`
9. `cargo check -p api-server`
+1
View File
@@ -4,6 +4,7 @@
## 文档列表
- [RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md](./RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md):记录 RPG 运行时 NPC 聊天、RPG/自定义世界 Agent 与大鱼 Agent 从“拼完整 SSE 字符串后一次性返回”改为 `mpsc + Sse<Event>` 真流式输出的后端落地口径。
- [RPG_BATTLE_HEALTHBAR_AND_ACTION_PRESENTATION_FIX_2026-04-26.md](./RPG_BATTLE_HEALTHBAR_AND_ACTION_PRESENTATION_FIX_2026-04-26.md):记录 RPG 战斗血条安全锚点、服务端战斗回包前端短表现,以及 `battle_use_skill` 指定技能兜底结算的修复口径。
- [SPACETIMEDB_TABLE_CATALOG.md](./SPACETIMEDB_TABLE_CATALOG.md):持续维护当前 SpacetimeDB 表目录,按领域说明每张表的作用、字段结构、索引和常用 `spacetime sql` 查询模板。
- [RPG_OPENING_SCENE_ACT_IMAGE_PRESENTATION_SYNC_2026-04-26.md](./RPG_OPENING_SCENE_ACT_IMAGE_PRESENTATION_SYNC_2026-04-26.md):记录开局场景与普通场景复用同一场景展示解析服务,修复列表幕缩略图和详情幕背景预览图片不一致的问题。
@@ -0,0 +1,51 @@
# RPG 与 Agent 聊天真流式 SSE 修复(2026-04-26
## 背景
RPG 运行时 NPC 聊天和创作 Agent 聊天此前都使用 `platform-llm.stream_text(...)`,但部分 HTTP handler 会把 `reply_delta` 先拼进内存字符串,等模型完整返回、建议生成和会话落库完成后才一次性响应。前端虽然消费的是 SSE 协议,实际体验仍接近非流式。
本次落地把这类路由改成真正边生成边输出:
1. RPG 运行时 NPC 聊天:`POST /api/runtime/chat/npc/turn/stream`
2. RPG/自定义世界创作 Agent`POST /api/runtime/custom-world/agent/sessions/:sessionId/messages/stream`
3. 大鱼吃小鱼创作 Agent`POST /api/runtime/big-fish/agent/sessions/:sessionId/messages/stream`
拼图 Agent 已经使用同类 `mpsc + Sse<Event>` 结构,本轮只作为参照,不重复改动。
## 设计约束
1. 前端 SSE contract 不变,继续消费 `reply_delta / complete / session / done / error`
2. `reply_delta` 必须在模型生成过程中实时发送,不再等待完整 handler 结束。
3. Agent 的最终 `session` 仍必须等 SpacetimeDB `finalize_*` 成功后再发送,保证前端真相态与表内状态一致。
4. RPG NPC 聊天的建议、好感变化、敌对终止判定仍在完整回复后计算,最终通过 `complete` 一次性返回。
5. LLM 不可用或失败时,RPG NPC 聊天继续走确定性兜底,仍发送至少一条 `reply_delta``complete`
## 落地方案
### Axum SSE 输出
相关 handler 改为返回 `Sse::new(async_stream::stream! { ... })`。模型回调不能直接 `yield`,因此使用:
1. `tokio::sync::mpsc::unbounded_channel::<String>()`
2. LLM turn 回调把最新可见回复写入 `reply_tx`
3. 外层 `tokio::select!` 同时等待 LLM 完成和 `reply_rx.recv()`
4. 每次收到文本立即 `yield Event::default().event("reply_delta")`
### Agent 写回顺序
Agent 路由保持原有 submit/finalize 分工:
1. `submit_*_message` 先写入用户消息与 operation。
2. `run_*_agent_turn` 运行模型,同时把 `replyText` 增量推给 SSE。
3. turn 完成后构造 `finalize_*_agent_message` 输入并写回 SpacetimeDB。
4. finalize 成功后再读取或映射最新 session,发送 `session``done`
5. finalize 失败时发送 `error` 并结束 SSE。
## 验收
1. `cargo fmt -p api-server`
2. `cargo check -p api-server`
3. `cargo test -p api-server runtime_chat`
4. `cargo test -p api-server creation_agent_llm_turn`
5. `node scripts/check-encoding.mjs docs/technical/RPG_AND_AGENT_CHAT_TRUE_SSE_STREAMING_2026-04-26.md server-rs/crates/api-server/src/runtime_chat.rs server-rs/crates/api-server/src/custom_world.rs server-rs/crates/api-server/src/big_fish.rs docs/technical/README.md`
6. 修改后端代码后,使用 `npm run api-server:maincloud` 重启后端。
@@ -47,6 +47,7 @@
- 优先 `LlmClient.stream_text(...)` 生成 `reply_delta`
- 再调用 `request_text(...)` 生成建议。
- 计算 `affinityDelta / affinityText / chatDirective` 后输出 `complete`
- SSE stream 返回给 Axum 时要求 `'static`,进入 stream 的玩家输入必须先转成 owned `String`,不能把 handler 栈上的 `&str` 借入 stream。
3. 修改 `server-rs/crates/api-server/src/auth.rs`
- `allows_internal_forwarded_auth(...)` 允许 `/api/runtime/chat/`,与 big-fish、puzzle 的内部转发鉴权策略保持一致。
- 单测覆盖 `/api/runtime/chat/npc/turn/stream`,防止后续新增 runtime 路由时再次遗漏内部转发白名单。
@@ -34,7 +34,7 @@ npm run dev:rust
1. 检查 `cargo``node``spacetime` CLI。
2. Windows Git Bash 下如 `server-rs/.spacetimedb/local/bin/current/spacetimedb-cli.exe` 不存在,先把本机 `spacetime` 所在安装目录的 `bin/``spacetime.exe` 同步到 `server-rs/.spacetimedb/local/`
3. 启动 `spacetime --root-dir=server-rs/.spacetimedb/local start --edition standalone --listen-addr 127.0.0.1:3101`,确保本地数据库与 SpacetimeDB 内部日志不会落到开发者全局目录。
4. 等待 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 可用。
4. 等待 `spacetime --root-dir=server-rs/.spacetimedb/local server ping http://127.0.0.1:3101` 可用;判定标准必须包含输出中的 `Server is online:`,不能只依赖 CLI 退出码,因为 SpacetimeDB CLI `2.1.0``502 Bad Gateway` 时也可能返回退出码 `0`
5. 执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`,确保 publish 的签名身份与 standalone 的本地控制库一致,并在当前开发阶段允许新版模块表结构变化且发生 schema 冲突时清除旧模块数据。
6. 注入 `GENARRATIVE_API_*``GENARRATIVE_SPACETIME_*` 后启动 `cargo run -p api-server`;直接运行 `api-server` 时,如未显式设置 `GENARRATIVE_SPACETIME_DATABASE`,服务端也会向上查找 `spacetime.local.json` 作为本地默认库名。
7. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite,避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`
@@ -65,6 +65,23 @@ npm run dev:rust
./scripts/dev-rust-stack.sh --preserve-database
```
bindings 生成:
```bash
npm run spacetime:generate
npm run spacetime:generate -- --rust-only
```
生成规则:
1. `npm run spacetime:generate` 是本仓库刷新 SpacetimeDB Rust client bindings 的唯一推荐入口。
2. 前端不直连 SpacetimeDB,不生成 TypeScript bindings;前端只通过 Rust `api-server` 暴露的 HTTP / SSE contract 访问后端。
3. Rust bindings 不生成私有表绑定,不追加 `--include-private`,生成到短临时目录后同步到 `server-rs/crates/spacetime-client/src/module_bindings`
4. Windows 下 SpacetimeDB CLI `2.1.0` 会在生成结束后把所有生成文件路径一次性传给 formatterRust bindings 文件较多,若直接输出到仓库深目录,可能触发 `Could not format generated files: 文件名或扩展名太长。`。该错误不是单个最长文件路径超过限制,而是 formatter 子进程参数总长超过 Windows `CreateProcess` 限制。
5. CLI 在上述 formatter 失败时仍可能返回退出码 `0`;脚本会捕获输出,只要出现 `Could not format generated files` 就视为失败。
6. 根目录 `spacetime.json` 不配置 `generate` 目标,避免裸 `spacetime generate` 在 Windows 上直接碰到 Rust formatter 路径总长限制,也避免误生成前端 bindings。
7. 不直接手写或局部补 `server-rs/crates/spacetime-client/src/module_bindings` 下的生成文件;schema 变化后重新执行本脚本,并补跑编码检查与对应类型检查。
日志提取:
```bash
@@ -86,6 +103,7 @@ npm run dev:rust:logs -- --follow
2. `spacetime --root-dir=server-rs/.spacetimedb/local list --server http://127.0.0.1:3101` 应能看到 `spacetime.local.json` 中的库名;若没有,执行 `spacetime --root-dir=server-rs/.spacetimedb/local publish <本地数据库名> --server http://127.0.0.1:3101 --module-path server-rs/crates/spacetime-module -c=on-conflict --yes`
3. 发布库名与 `GENARRATIVE_SPACETIME_DATABASE` 不一致时,`/api/runtime/custom-world-gallery` 会从 Rust `api-server` 返回 `502`,前端首页只能展示空态或错误提示,无法自行修复。
4. 如果 Vite 输出 `/api/auth/refresh``/api/auth/login-options``/api/runtime/custom-world-gallery``ECONNREFUSED`,先确认当前脚本是否已经打印 `等待 api-server 就绪` 并通过;正常情况下 Vite 只会在 `/healthz` 可访问后启动,不应再因为 Rust 监听未完成而代理失败。
5. 如果 `spacetime server ping` 打印 `Server could not be reached (502 Bad Gateway)`,即使命令退出码为 `0` 也必须视为未就绪;脚本会继续等待新启动实例,或在 root-dir 已被其他实例占用时输出占用进程。
编译警告治理:
@@ -93,6 +111,12 @@ npm run dev:rust:logs -- --follow
2. 仅供测试断言使用的辅助函数使用 `#[cfg(test)]` 限定,避免进入 `cargo run -p api-server` 的普通二进制编译。
3. 已无调用入口且无迁移价值的映射函数直接删除;如果后续新增同类 SpacetimeDB 记录映射,再按实际调用路径补回,避免提前保留死代码。
Maincloud API 重启补充:
1. `npm run api-server:maincloud` 会先读取 `.env``.env.local`,把 `GENARRATIVE_SPACETIME_MAINCLOUD_*` 映射为 `api-server` 使用的 `GENARRATIVE_SPACETIME_*`,再运行 `cargo run -p api-server --manifest-path server-rs/Cargo.toml`
2. Windows 下脚本会尽力停止本仓库 `server-rs/target/debug/api-server.exe` 对应的旧进程,避免 cargo 重新编译时 exe 被占用。
3. 旧进程已经退出或清理过程中出现瞬时等待失败时,不应阻断新的 `api-server` 启动;脚本只记录清理失败并继续启动。
## 3. Ubuntu 发布包脚本
入口:
@@ -48,7 +48,7 @@ spacetime publish xushi-p4wfr --server http://127.0.0.1:3101 --module-path D:\Ge
publish 成功后,继续执行:
```bash
spacetime generate --no-config --lang rust --out-dir D:\Genarrative\server-rs\crates\spacetime-client\src\module_bindings --module-path D:\Genarrative\server-rs\crates\spacetime-module --include-private --yes
npm run spacetime:generate
```
要求:
@@ -164,19 +164,18 @@ Stage 5 已接入 agent `publish_world` action,但调用方仍需要显式提
本仓默认刷新命令:
```bash
spacetime generate --no-config --lang rust --out-dir server-rs/crates/spacetime-client/src/module_bindings --module-path server-rs/crates/spacetime-module --include-private --yes
npm run spacetime:generate
```
如果当前工作树里存在其他并行 `cargo build/test/check` 导致 `spacetime generate` 内部构建长期卡在锁竞争,则先在独立目标目录编译 wasm,再使用 `--bin-path` 生成:
如果当前工作树里存在其他并行 `cargo build/test/check` 导致 `spacetime generate` 内部构建长期卡在锁竞争,则先结束占锁任务后重新执行仓库脚本;脚本内部会使用短临时目录生成公开 Rust bindings,不生成私有表绑定。
```bash
CARGO_TARGET_DIR=server-rs/target-spacetime-bindgen cargo build --manifest-path server-rs/crates/spacetime-module/Cargo.toml --target wasm32-unknown-unknown --release
spacetime generate --no-config --lang rust --out-dir server-rs/crates/spacetime-client/src/module_bindings --bin-path server-rs/target-spacetime-bindgen/wasm32-unknown-unknown/release/spacetime_module.wasm --include-private --yes
npm run spacetime:generate
```
这样做的目的固定如下:
1. 避开根目录 `spacetime.json` 多 generate target 对单次 Rust 生成的参数冲突
1. 避开根目录 `spacetime.json` 误触发前端 bindings 或私有表 bindings 生成
2. 避开共享 `target/` 目录被其他任务占锁时的构建阻塞。
3. 保证 `create_custom_world_agent_session` / `get_custom_world_agent_session` 这类新 procedure 与输入输出类型能同步进入 Rust bindings。
+1
View File
@@ -9,6 +9,7 @@
"dev:rust:logs": "node scripts/run-bash-script.mjs scripts/spacetime-logs-local.sh",
"dev:web": "node scripts/dev-web-rust.mjs",
"spacetime:publish:maincloud": "node scripts/run-bash-script.mjs scripts/spacetime-publish-maincloud.sh",
"spacetime:generate": "node scripts/generate-spacetime-bindings.mjs",
"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",
+9 -3
View File
@@ -67,15 +67,21 @@ function stopExistingWindowsApiServer() {
// Windows 下 cargo 重新编译时无法覆盖仍在运行的 exe,只清理本仓库 target 内的旧进程。
const command = [
'$ErrorActionPreference = "Continue"',
'$target = [System.IO.Path]::GetFullPath($env:GENARRATIVE_API_SERVER_EXE_TARGET)',
'$processes = Get-Process -Name api-server -ErrorAction SilentlyContinue | Where-Object {',
' $_.Path -and ([System.IO.Path]::GetFullPath($_.Path) -ieq $target)',
'}',
'foreach ($process in $processes) {',
' Stop-Process -Id $process.Id -Force',
' Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue',
' Write-Output $process.Id',
' try {',
' Stop-Process -Id $process.Id -Force -ErrorAction Stop',
' Wait-Process -Id $process.Id -Timeout 5 -ErrorAction SilentlyContinue',
' Write-Output $process.Id',
' } catch {',
' Write-Error "[api-server:maincloud] 忽略旧进程清理瞬时失败 pid=$($process.Id): $($_.Exception.Message)"',
' }',
'}',
'exit 0',
].join('\n');
const output = execFileSync(
+8 -2
View File
@@ -78,7 +78,7 @@ wait_for_spacetime() {
exit 1
fi
if spacetime --root-dir="${root_dir}" server ping "${server}" >/dev/null 2>&1; then
if is_spacetime_ready "${server}" "${root_dir}"; then
return
fi
@@ -92,8 +92,14 @@ wait_for_spacetime() {
is_spacetime_ready() {
local server="$1"
local root_dir="$2"
local output
spacetime --root-dir="${root_dir}" server ping "${server}" >/dev/null 2>&1
if ! output="$(spacetime --root-dir="${root_dir}" server ping "${server}" 2>&1)"; then
return 1
fi
# SpacetimeDB CLI 2.1.0 在 502 Bad Gateway 时仍可能返回 0,不能只依赖退出码。
[[ "${output}" == *"Server is online:"* ]]
}
describe_spacetime_root_owner() {
+207
View File
@@ -0,0 +1,207 @@
import {spawn} from 'node:child_process';
import {existsSync} from 'node:fs';
import {cp, mkdir, readdir, rm, stat} from 'node:fs/promises';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
const SCRIPT_DIR = path.dirname(fileURLToPath(import.meta.url));
const REPO_ROOT = path.resolve(SCRIPT_DIR, '..');
const MODULE_PATH = path.join(REPO_ROOT, 'server-rs', 'crates', 'spacetime-module');
const TARGETS = [
{
name: 'Rust',
lang: 'rust',
tempName: 'rs',
outDir: path.join(
REPO_ROOT,
'server-rs',
'crates',
'spacetime-client',
'src',
'module_bindings',
),
},
];
const args = new Set(process.argv.slice(2));
const KNOWN_ARGS = new Set(['--rust-only']);
for (const arg of args) {
if (!KNOWN_ARGS.has(arg)) {
console.error(`[spacetime:generate] 未知参数: ${arg}`);
process.exit(1);
}
}
if (!existsSync(path.join(MODULE_PATH, 'Cargo.toml'))) {
console.error(`[spacetime:generate] 未找到模块: ${MODULE_PATH}`);
process.exit(1);
}
const tempRoot = resolveTempRoot();
assertSafeTempRoot(tempRoot);
const selectedTargets = TARGETS.filter((target) => shouldRunTarget(target.lang));
if (selectedTargets.length === 0) {
console.error('[spacetime:generate] 没有需要生成的目标。');
process.exit(1);
}
await mkdir(tempRoot, {recursive: true});
for (const target of selectedTargets) {
const tempOutDir = path.join(tempRoot, target.tempName);
await recreateTempDir(tempOutDir);
console.log(`[spacetime:generate] 生成 ${target.name} bindings 到短路径: ${tempOutDir}`);
await run('spacetime', buildGenerateArgs(target, tempOutDir));
const fileCount = await countFiles(tempOutDir);
if (fileCount === 0) {
throw new Error(`${target.name} bindings 未生成任何文件。`);
}
console.log(`[spacetime:generate] 同步 ${fileCount} 个文件到 ${target.outDir}`);
await replaceGeneratedDir(tempOutDir, target.outDir);
}
await rm(tempRoot, {recursive: true, force: true});
console.log('[spacetime:generate] bindings 生成完成。');
function shouldRunTarget(lang) {
if (args.has('--rust-only')) {
return lang === 'rust';
}
return true;
}
function resolveTempRoot() {
if (process.env.GENARRATIVE_BINDGEN_TEMP_ROOT) {
return path.resolve(process.env.GENARRATIVE_BINDGEN_TEMP_ROOT);
}
// Windows 下 SpacetimeDB CLI 2.1.0 会把所有生成文件路径一次性传给 formatter;
// Rust bindings 文件数较多,输出到仓库深目录时容易触发 CreateProcess 路径总长限制。
if (process.platform === 'win32') {
return path.join(path.parse(REPO_ROOT).root, '.genarrative-bindgen');
}
return path.join(REPO_ROOT, 'tmp', 'spacetime-bindgen');
}
async function recreateTempDir(dir) {
assertInside(dir, tempRoot, '临时生成目录');
await rm(dir, {recursive: true, force: true});
await mkdir(dir, {recursive: true});
}
async function replaceGeneratedDir(fromDir, toDir) {
assertInside(toDir, REPO_ROOT, '仓库生成目录');
await rm(toDir, {recursive: true, force: true});
await mkdir(path.dirname(toDir), {recursive: true});
await cp(fromDir, toDir, {recursive: true});
}
function assertInside(candidate, parent, label) {
const relative = path.relative(path.resolve(parent), path.resolve(candidate));
if (relative === '' || relative.startsWith('..') || path.isAbsolute(relative)) {
throw new Error(`${label} 不在预期目录内: ${candidate}`);
}
}
function assertSafeTempRoot(dir) {
const resolved = path.resolve(dir);
const parsed = path.parse(resolved);
const basename = path.basename(resolved).toLowerCase();
if (resolved === path.resolve(REPO_ROOT) || resolved === parsed.root) {
throw new Error(`临时根目录不允许指向仓库或磁盘根目录: ${resolved}`);
}
if (!basename.includes('bindgen')) {
throw new Error(`临时根目录必须是明确的 bindings 生成目录: ${resolved}`);
}
}
function buildGenerateArgs(target, outDir) {
const generateArgs = [
'generate',
'--no-config',
'--lang',
target.lang,
'--out-dir',
outDir,
'--module-path',
MODULE_PATH,
'--yes',
];
return generateArgs;
}
function run(command, commandArgs) {
return new Promise((resolve, reject) => {
const child = spawn(command, commandArgs, {
cwd: REPO_ROOT,
env: process.env,
shell: false,
stdio: ['ignore', 'pipe', 'pipe'],
});
let output = '';
child.stdout.on('data', (chunk) => {
const text = chunk.toString();
output += text;
process.stdout.write(text);
});
child.stderr.on('data', (chunk) => {
const text = chunk.toString();
output += text;
process.stderr.write(text);
});
child.on('error', reject);
child.on('exit', (code, signal) => {
if (signal) {
reject(new Error(`${command} 被信号中断: ${signal}`));
return;
}
if (output.includes('Could not format generated files')) {
reject(new Error(`${command} 生成后格式化失败。`));
return;
}
if (code === 0) {
resolve();
return;
}
reject(new Error(`${command} 退出码: ${code ?? 'unknown'}`));
});
});
}
async function countFiles(dir) {
let count = 0;
const entries = await readdir(dir, {withFileTypes: true});
for (const entry of entries) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
count += await countFiles(entryPath);
continue;
}
if (entry.isFile() || (await stat(entryPath)).isFile()) {
count += 1;
}
}
return count;
}
+116 -118
View File
@@ -1,13 +1,17 @@
use std::{
collections::BTreeMap,
convert::Infallible,
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
};
use axum::{
Json,
extract::{Extension, Path, State, rejection::JsonRejection},
http::{HeaderName, StatusCode, header},
response::{IntoResponse, Response},
http::StatusCode,
response::{
IntoResponse, Response,
sse::{Event, Sse},
},
};
use module_assets::{
AssetObjectAccessPolicy, AssetObjectFieldError, build_asset_entity_binding_input,
@@ -323,93 +327,107 @@ pub async fn stream_big_fish_message(
big_fish_error_response(&request_context, map_big_fish_client_error(error))
})?;
let quick_fill_requested = payload.quick_fill_requested.unwrap_or(false);
let mut draft_writer = AiGenerationDraftWriter::new(AiGenerationDraftContext::new(
"big_fish",
owner_user_id.as_str(),
session_id.as_str(),
payload.client_message_id.as_str(),
"大鱼吃小鱼模板生成草稿",
));
if let Err(error) = draft_writer.ensure_started(state.spacetime_client()).await {
tracing::warn!(error = %error, "大鱼吃小鱼模板生成草稿任务启动失败,主生成流程继续执行");
}
let draft_sink = AiGenerationDraftSink::new(
AiGenerationDraftContext::new(
let state = state.clone();
let session_id_for_stream = session_id.clone();
let owner_user_id_for_stream = owner_user_id.clone();
let client_message_id_for_stream = payload.client_message_id.clone();
let stream = async_stream::stream! {
let mut draft_writer = AiGenerationDraftWriter::new(AiGenerationDraftContext::new(
"big_fish",
owner_user_id.as_str(),
session_id.as_str(),
payload.client_message_id.as_str(),
owner_user_id_for_stream.as_str(),
session_id_for_stream.as_str(),
client_message_id_for_stream.as_str(),
"大鱼吃小鱼模板生成草稿",
),
state.spacetime_client().clone(),
);
let mut streamed_reply_text = String::new();
let turn_result = run_big_fish_agent_turn(
BigFishAgentTurnRequest {
llm_client: state.llm_client(),
session: &submitted_session,
quick_fill_requested,
},
|text| {
draft_sink.persist_visible_text_async(text);
streamed_reply_text = text.to_string();
},
)
.await;
if !streamed_reply_text.is_empty() {
draft_writer
.persist_visible_text(state.spacetime_client(), streamed_reply_text.as_str())
.await;
}
let reply_text = match &turn_result {
Ok(result) => result.assistant_reply_text.clone(),
Err(error) => error.to_string(),
};
let finalize_input = match turn_result {
Ok(turn_result) => build_finalize_record_input(
session_id.clone(),
owner_user_id.clone(),
build_prefixed_uuid_id("big-fish-message-"),
turn_result,
current_utc_micros(),
),
Err(error) => build_failed_finalize_record_input(
session_id.clone(),
owner_user_id.clone(),
&submitted_session,
error.to_string(),
current_utc_micros(),
),
};
let session = state
.spacetime_client()
.finalize_big_fish_agent_message(finalize_input)
.await
.map_err(|error| {
big_fish_error_response(&request_context, map_big_fish_client_error(error))
})?;
));
if let Err(error) = draft_writer.ensure_started(state.spacetime_client()).await {
tracing::warn!(error = %error, "大鱼吃小鱼模板生成草稿任务启动失败,主生成流程继续执行");
}
let (reply_tx, mut reply_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
// 与 RPG/拼图 Agent 保持同一语义:回复先流式展示,session 真相仍等 finalize 后下发。
let turn_result = {
let run_turn = run_big_fish_agent_turn(
BigFishAgentTurnRequest {
llm_client: state.llm_client(),
session: &submitted_session,
quick_fill_requested,
},
move |text| {
let _ = reply_tx.send(text.to_string());
},
);
tokio::pin!(run_turn);
let session_response = map_big_fish_session_response(session);
let mut sse_body = String::new();
append_sse_event(
&request_context,
&mut sse_body,
"reply_delta",
&json!({ "text": if streamed_reply_text.is_empty() { reply_text } else { streamed_reply_text } }),
)?;
append_sse_event(
&request_context,
&mut sse_body,
"session",
&json!({ "session": session_response }),
)?;
append_sse_event(
&request_context,
&mut sse_body,
"done",
&json!({ "ok": true }),
)?;
Ok(build_event_stream_response(sse_body))
loop {
// 每个 replyText 增量同时写草稿表并推给 SSE,避免前端等待完整模型响应。
tokio::select! {
result = &mut run_turn => break result,
maybe_text = reply_rx.recv() => {
if let Some(text) = maybe_text {
draft_writer
.persist_visible_text(state.spacetime_client(), text.as_str())
.await;
yield Ok::<Event, Infallible>(big_fish_sse_json_event_or_error(
"reply_delta",
json!({ "text": text }),
));
}
}
}
}
};
while let Some(text) = reply_rx.recv().await {
draft_writer
.persist_visible_text(state.spacetime_client(), text.as_str())
.await;
yield Ok::<Event, Infallible>(big_fish_sse_json_event_or_error(
"reply_delta",
json!({ "text": text }),
));
}
let finalize_input = match turn_result {
Ok(turn_result) => build_finalize_record_input(
session_id_for_stream.clone(),
owner_user_id_for_stream.clone(),
build_prefixed_uuid_id("big-fish-message-"),
turn_result,
current_utc_micros(),
),
Err(error) => build_failed_finalize_record_input(
session_id_for_stream.clone(),
owner_user_id_for_stream.clone(),
&submitted_session,
error.to_string(),
current_utc_micros(),
),
};
let session = match state
.spacetime_client()
.finalize_big_fish_agent_message(finalize_input)
.await
{
Ok(session) => session,
Err(error) => {
yield Ok::<Event, Infallible>(big_fish_sse_json_event_or_error(
"error",
json!({ "message": error.to_string() }),
));
return;
}
};
let session_response = map_big_fish_session_response(session);
yield Ok::<Event, Infallible>(big_fish_sse_json_event_or_error(
"session",
json!({ "session": session_response }),
));
yield Ok::<Event, Infallible>(big_fish_sse_json_event_or_error(
"done",
json!({ "ok": true }),
));
};
Ok(Sse::new(stream).into_response())
}
pub async fn execute_big_fish_action(
@@ -1706,40 +1724,20 @@ fn big_fish_bad_request(request_context: &RequestContext, message: &str) -> Resp
)
}
fn append_sse_event(
request_context: &RequestContext,
body: &mut String,
event: &str,
payload: &Value,
) -> Result<(), Response> {
let payload_text = serde_json::to_string(payload).map_err(|error| {
big_fish_error_response(
request_context,
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "big-fish",
"message": format!("SSE payload 序列化失败:{error}"),
})),
)
})?;
body.push_str("event: ");
body.push_str(event);
body.push('\n');
body.push_str("data: ");
body.push_str(&payload_text);
body.push_str("\n\n");
Ok(())
fn big_fish_sse_json_event_or_error(event_name: &str, payload: Value) -> Event {
match serde_json::to_string(&payload) {
Ok(payload_text) => Event::default().event(event_name).data(payload_text),
Err(_) => big_fish_sse_error_event_message("SSE payload 序列化失败".to_string()),
}
}
fn build_event_stream_response(body: String) -> Response {
(
[
(header::CONTENT_TYPE, "text/event-stream; charset=utf-8"),
(header::CACHE_CONTROL, "no-cache"),
(HeaderName::from_static("x-accel-buffering"), "no"),
],
body,
)
.into_response()
fn big_fish_sse_error_event_message(message: String) -> Event {
let payload = format!(
"{{\"message\":{}}}",
serde_json::to_string(&message)
.unwrap_or_else(|_| "\"SSE 错误事件序列化失败\"".to_string())
);
Event::default().event("error").data(payload)
}
fn map_big_fish_client_error(error: SpacetimeClientError) -> AppError {
+41 -26
View File
@@ -901,34 +901,49 @@ pub async fn stream_custom_world_agent_message(
if let Err(error) = draft_writer.ensure_started(state.spacetime_client()).await {
tracing::warn!(error = %error, "自定义世界模板生成草稿任务启动失败,主生成流程继续执行");
}
let draft_sink = AiGenerationDraftSink::new(
AiGenerationDraftContext::new(
"custom_world",
owner_user_id_for_stream.as_str(),
session_id_for_stream.as_str(),
operation_id.as_str(),
"自定义世界模板生成草稿",
),
state.spacetime_client().clone(),
);
// 聊天回复必须等本轮模型解析、进度与会话快照全部落库后,
// 再随最终 session 一次性返回,避免玩家先看到回复而进度仍停在旧状态。
let turn_result = run_custom_world_agent_turn(
CustomWorldAgentTurnRequest {
llm_client: state.llm_client(),
session: &session,
quick_fill_requested,
focus_card_id,
},
move |text| {
draft_sink.persist_visible_text_async(text);
},
)
.await;
if let Ok(result) = &turn_result {
let (reply_tx, mut reply_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
// Agent turn 仍负责完整 JSON 解析和最终写回;这里把 replyText 增量桥接成前端可见的 SSE 分片。
let turn_result = {
let run_turn = run_custom_world_agent_turn(
CustomWorldAgentTurnRequest {
llm_client: state.llm_client(),
session: &session,
quick_fill_requested,
focus_card_id,
},
move |text| {
let _ = reply_tx.send(text.to_string());
},
);
tokio::pin!(run_turn);
loop {
// 不等待最终 session 落库即可先推送回复进度;session/done 仍在 finalize 成功后发送。
tokio::select! {
result = &mut run_turn => break result,
maybe_text = reply_rx.recv() => {
if let Some(text) = maybe_text {
draft_writer
.persist_visible_text(state.spacetime_client(), text.as_str())
.await;
yield Ok::<Event, Infallible>(custom_world_sse_json_event_or_error(
"reply_delta",
json!({ "text": text }),
));
}
}
}
}
};
while let Some(text) = reply_rx.recv().await {
draft_writer
.persist_visible_text(state.spacetime_client(), result.assistant_reply_text.as_str())
.persist_visible_text(state.spacetime_client(), text.as_str())
.await;
yield Ok::<Event, Infallible>(custom_world_sse_json_event_or_error(
"reply_delta",
json!({ "text": text }),
));
}
let finalize_input = match turn_result {
+113 -109
View File
@@ -1,12 +1,16 @@
use axum::{
Json,
extract::{Extension, State},
http::{StatusCode, header},
response::{IntoResponse, Response},
http::StatusCode,
response::{
IntoResponse, Response,
sse::{Event, Sse},
},
};
use platform_llm::{LlmMessage, LlmTextRequest};
use serde::Deserialize;
use serde_json::{Value, json};
use std::convert::Infallible;
use crate::{
http_error::AppError,
@@ -58,7 +62,7 @@ pub async fn stream_runtime_npc_chat_turn(
let npc_name = read_string_field(&payload.encounter, "npcName")
.or_else(|| read_string_field(&payload.encounter, "name"))
.unwrap_or_else(|| "对方".to_string());
let player_message = payload.player_message.trim();
let player_message = payload.player_message.trim().to_string();
if player_message.is_empty() && !payload.npc_initiates_conversation {
return Err(runtime_chat_error_response(
&request_context,
@@ -69,75 +73,106 @@ pub async fn stream_runtime_npc_chat_turn(
));
}
let llm_result =
generate_llm_npc_chat_turn(&state, &request_context, &payload, &npc_name).await;
let (mut body, npc_reply, suggestions, function_suggestions, force_exit) = match llm_result {
Some(result) => result,
None => {
let npc_reply = build_deterministic_npc_reply(
npc_name.as_str(),
player_message,
payload.npc_initiates_conversation,
);
let force_exit = should_force_chat_exit(payload.chat_directive.as_ref())
|| should_hostile_chat_breakoff_deterministically(
player_message,
payload.chat_directive.as_ref(),
);
let suggestions = if force_exit {
Vec::new()
} else {
build_deterministic_chat_suggestions(npc_name.as_str(), player_message)
};
let function_suggestions = if force_exit {
Vec::new()
} else {
build_fallback_function_suggestions(payload.chat_directive.as_ref())
};
let mut body = String::new();
append_sse_event(
&request_context,
&mut body,
let stream = async_stream::stream! {
let (reply_tx, mut reply_rx) = tokio::sync::mpsc::unbounded_channel::<String>();
// `platform-llm` 在当前任务内持续回调增量文本;外层用 channel 把增量转成真正的 SSE 分片。
let llm_turn = generate_llm_npc_chat_turn(
&state,
&payload,
&npc_name,
move |text| {
let _ = reply_tx.send(text.to_string());
},
);
tokio::pin!(llm_turn);
let llm_result = loop {
// 模型尚未结束时优先把已收到的累计回复推出去,避免等完整建议生成后才一次性返回。
tokio::select! {
result = &mut llm_turn => break result,
maybe_text = reply_rx.recv() => {
if let Some(text) = maybe_text {
yield Ok::<Event, Infallible>(runtime_chat_sse_json_event_or_error(
"reply_delta",
json!({ "text": text }),
));
}
}
}
};
while let Some(text) = reply_rx.recv().await {
yield Ok::<Event, Infallible>(runtime_chat_sse_json_event_or_error(
"reply_delta",
&json!({ "text": npc_reply }),
)?;
(
body,
npc_reply,
suggestions,
function_suggestions,
force_exit,
)
json!({ "text": text }),
));
}
};
let chatted_count = read_number_field(&payload.npc_state, "chattedCount").unwrap_or(0.0);
let affinity_delta = if payload.npc_initiates_conversation {
0
} else {
compute_npc_chat_affinity_delta(player_message, npc_reply.as_str(), chatted_count)
};
let complete_payload = json!({
"npcReply": npc_reply,
"affinityDelta": affinity_delta,
"affinityText": describe_affinity_shift(affinity_delta),
"suggestions": suggestions,
"functionSuggestions": function_suggestions,
"pendingQuestOffer": null,
"chatDirective": build_completion_directive(payload.chat_directive.as_ref(), force_exit),
});
let (npc_reply, suggestions, function_suggestions, force_exit) = match llm_result {
Some(result) => result,
None => {
let npc_reply = build_deterministic_npc_reply(
npc_name.as_str(),
player_message.as_str(),
payload.npc_initiates_conversation,
);
let force_exit = should_force_chat_exit(payload.chat_directive.as_ref())
|| should_hostile_chat_breakoff_deterministically(
player_message.as_str(),
payload.chat_directive.as_ref(),
);
let suggestions = if force_exit {
Vec::new()
} else {
build_deterministic_chat_suggestions(npc_name.as_str(), player_message.as_str())
};
let function_suggestions = if force_exit {
Vec::new()
} else {
build_fallback_function_suggestions(payload.chat_directive.as_ref())
};
yield Ok::<Event, Infallible>(runtime_chat_sse_json_event_or_error(
"reply_delta",
json!({ "text": npc_reply }),
));
(npc_reply, suggestions, function_suggestions, force_exit)
}
};
append_sse_event(&request_context, &mut body, "complete", &complete_payload)?;
body.push_str("data: [DONE]\n\n");
Ok(build_event_stream_response(body))
let chatted_count = read_number_field(&payload.npc_state, "chattedCount").unwrap_or(0.0);
let affinity_delta = if payload.npc_initiates_conversation {
0
} else {
compute_npc_chat_affinity_delta(player_message.as_str(), npc_reply.as_str(), chatted_count)
};
let complete_payload = json!({
"npcReply": npc_reply,
"affinityDelta": affinity_delta,
"affinityText": describe_affinity_shift(affinity_delta),
"suggestions": suggestions,
"functionSuggestions": function_suggestions,
"pendingQuestOffer": null,
"chatDirective": build_completion_directive(payload.chat_directive.as_ref(), force_exit),
});
yield Ok::<Event, Infallible>(runtime_chat_sse_json_event_or_error(
"complete",
complete_payload,
));
yield Ok::<Event, Infallible>(Event::default().data("[DONE]"));
};
Ok(Sse::new(stream).into_response())
}
async fn generate_llm_npc_chat_turn(
async fn generate_llm_npc_chat_turn<F>(
state: &AppState,
request_context: &RequestContext,
payload: &NpcChatTurnRequest,
npc_name: &str,
) -> Option<(String, String, Vec<String>, Vec<Value>, bool)> {
mut on_reply_update: F,
) -> Option<(String, Vec<String>, Vec<Value>, bool)>
where
F: FnMut(&str),
{
let llm_client = state.llm_client()?;
let character = payload
.character
@@ -160,7 +195,6 @@ async fn generate_llm_npc_chat_turn(
chat_directive: payload.chat_directive.as_ref(),
};
let mut body = String::new();
let reply_prompt = build_npc_chat_turn_reply_prompt(&prompt_input);
let mut reply_request = LlmTextRequest::new(vec![
LlmMessage::system(NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT),
@@ -171,12 +205,7 @@ async fn generate_llm_npc_chat_turn(
let reply_response = llm_client
.stream_text(reply_request, |delta| {
let _ = append_sse_event(
request_context,
&mut body,
"reply_delta",
&json!({ "text": delta.accumulated_text }),
);
on_reply_update(delta.accumulated_text.as_str());
})
.await
.ok()?;
@@ -189,7 +218,7 @@ async fn generate_llm_npc_chat_turn(
});
if should_force_chat_exit(payload.chat_directive.as_ref()) {
return Some((body, npc_reply, Vec::new(), Vec::new(), true));
return Some((npc_reply, Vec::new(), Vec::new(), true));
}
let suggestion_prompt =
@@ -224,13 +253,7 @@ async fn generate_llm_npc_chat_turn(
suggestions = build_fallback_npc_chat_suggestions(payload.player_message.as_str());
}
Some((
body,
npc_reply,
suggestions,
function_suggestions,
force_exit,
))
Some((npc_reply, suggestions, function_suggestions, force_exit))
}
fn build_deterministic_npc_reply(
@@ -595,39 +618,20 @@ fn describe_affinity_shift(affinity_delta: i32) -> &'static str {
"这轮对话暂时没有带来明显关系变化。"
}
fn append_sse_event(
request_context: &RequestContext,
body: &mut String,
event: &str,
payload: &Value,
) -> Result<(), Response> {
let payload_text = serde_json::to_string(payload).map_err(|error| {
runtime_chat_error_response(
request_context,
AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({
"provider": "runtime-chat",
"message": format!("SSE payload 序列化失败:{error}"),
})),
)
})?;
body.push_str("event: ");
body.push_str(event);
body.push('\n');
body.push_str("data: ");
body.push_str(&payload_text);
body.push_str("\n\n");
Ok(())
fn runtime_chat_sse_json_event_or_error(event_name: &str, payload: Value) -> Event {
match serde_json::to_string(&payload) {
Ok(payload_text) => Event::default().event(event_name).data(payload_text),
Err(_) => runtime_chat_sse_error_event_message("SSE payload 序列化失败".to_string()),
}
}
fn build_event_stream_response(body: String) -> Response {
(
[
(header::CONTENT_TYPE, "text/event-stream; charset=utf-8"),
(header::CACHE_CONTROL, "no-cache"),
],
body,
)
.into_response()
fn runtime_chat_sse_error_event_message(message: String) -> Event {
let payload = format!(
"{{\"message\":{}}}",
serde_json::to_string(&message)
.unwrap_or_else(|_| "\"SSE 错误事件序列化失败\"".to_string())
);
Event::default().event("error").data(payload)
}
fn runtime_chat_error_response(request_context: &RequestContext, error: AppError) -> Response {
+6 -4
View File
@@ -14,7 +14,7 @@
当前目录已不再只是占位,当前阶段已经落下:
1. 通过 `spacetime generate --lang rust --include-private` 生成的最小 Rust bindings
1. 通过 `npm run spacetime:generate -- --rust-only` 生成的公开 Rust bindings
2. `DbConnection` 连接封装
3. `confirm_asset_object_and_return` procedure 的最小调用适配
4. `bind_asset_object_to_entity_and_return` procedure 的最小调用适配
@@ -33,9 +33,11 @@
`src/module_bindings` 目录下的 Rust 文件统一视为 SpacetimeDB CLI 生成产物,后续维护必须遵守:
1. 只允许通过 `spacetime generate --lang rust` 刷新,不允许手工修改。
2.允许对该目录执行 `rustfmt`,避免把 CLI 原始输出改写成额外格式化噪音
3. `src/lib.rs` 已通过 `#[rustfmt::skip] pub mod module_bindings;` 显式阻止 workspace 级 `cargo fmt` 继续递归格式化该目录
1. 只允许通过仓库根目录 `npm run spacetime:generate -- --rust-only` 刷新,不允许手工修改。
2.生成私有表绑定,不追加 `--include-private`;如后端需要读取私有表,应先在 `api-server` 或模块层补明确 contract,而不是让客户端 crate 直接依赖私有表结构
3. 不允许对该目录额外执行 `rustfmt`,生成物格式只接受 SpacetimeDB CLI 生成阶段自身输出
4. `src/lib.rs` 已通过 `#[rustfmt::skip] pub mod module_bindings;` 显式阻止 workspace 级 `cargo fmt` 继续递归格式化该目录。
5. Windows 下直接把 Rust bindings 输出到本目录时,SpacetimeDB CLI `2.1.0` 的生成后 formatter 可能因为路径参数总长触发 `文件名或扩展名太长`;仓库脚本会先输出到短临时目录,再同步回本目录。
### 2.1.1 绑定缺文件恢复流程
@@ -2,12 +2,7 @@
// 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 spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::quest_record_input_type::QuestRecordInput;
@@ -19,10 +14,8 @@ pub(super) struct AcceptQuestArgs {
impl From<AcceptQuestArgs> for super::Reducer {
fn from(args: AcceptQuestArgs) -> Self {
Self::AcceptQuest {
input: args.input,
}
}
Self::AcceptQuest { input: args.input }
}
}
impl __sdk::InModule for AcceptQuestArgs {
@@ -40,9 +33,8 @@ pub trait accept_quest {
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`accept_quest:accept_quest_then`] to run a callback after the reducer completes.
fn accept_quest(&self, input: QuestRecordInput,
) -> __sdk::Result<()> {
self.accept_quest_then(input, |_, _| {})
fn accept_quest(&self, input: QuestRecordInput) -> __sdk::Result<()> {
self.accept_quest_then(input, |_, _| {})
}
/// Request that the remote module invoke the reducer `accept_quest` to run as soon as possible,
@@ -70,7 +62,7 @@ impl accept_quest for super::RemoteReducers {
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(AcceptQuestArgs { input, }, callback)
self.imp
.invoke_reducer_with_callback(AcceptQuestArgs { input }, callback)
}
}
@@ -2,12 +2,7 @@
// 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 spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::quest_completion_ack_input_type::QuestCompletionAckInput;
@@ -19,10 +14,8 @@ pub(super) struct AcknowledgeQuestCompletionArgs {
impl From<AcknowledgeQuestCompletionArgs> for super::Reducer {
fn from(args: AcknowledgeQuestCompletionArgs) -> Self {
Self::AcknowledgeQuestCompletion {
input: args.input,
}
}
Self::AcknowledgeQuestCompletion { input: args.input }
}
}
impl __sdk::InModule for AcknowledgeQuestCompletionArgs {
@@ -40,9 +33,8 @@ pub trait acknowledge_quest_completion {
/// The reducer will run asynchronously in the future,
/// and this method provides no way to listen for its completion status.
/// /// Use [`acknowledge_quest_completion:acknowledge_quest_completion_then`] to run a callback after the reducer completes.
fn acknowledge_quest_completion(&self, input: QuestCompletionAckInput,
) -> __sdk::Result<()> {
self.acknowledge_quest_completion_then(input, |_, _| {})
fn acknowledge_quest_completion(&self, input: QuestCompletionAckInput) -> __sdk::Result<()> {
self.acknowledge_quest_completion_then(input, |_, _| {})
}
/// Request that the remote module invoke the reducer `acknowledge_quest_completion` to run as soon as possible,
@@ -70,7 +62,7 @@ impl acknowledge_quest_completion for super::RemoteReducers {
+ Send
+ 'static,
) -> __sdk::Result<()> {
self.imp.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input, }, callback)
self.imp
.invoke_reducer_with_callback(AcknowledgeQuestCompletionArgs { input }, callback)
}
}
@@ -2,23 +2,17 @@
// 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 spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::puzzle_run_next_level_input_type::PuzzleRunNextLevelInput;
use super::puzzle_run_procedure_result_type::PuzzleRunProcedureResult;
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
struct AdvancePuzzleNextLevelArgs {
struct AdvancePuzzleNextLevelArgs {
pub input: PuzzleRunNextLevelInput,
}
impl __sdk::InModule for AdvancePuzzleNextLevelArgs {
type Module = super::RemoteModule;
}
@@ -28,16 +22,19 @@ impl __sdk::InModule for AdvancePuzzleNextLevelArgs {
///
/// Implemented for [`super::RemoteProcedures`].
pub trait advance_puzzle_next_level {
fn advance_puzzle_next_level(&self, input: PuzzleRunNextLevelInput,
) {
self.advance_puzzle_next_level_then(input, |_, _| {});
fn advance_puzzle_next_level(&self, input: PuzzleRunNextLevelInput) {
self.advance_puzzle_next_level_then(input, |_, _| {});
}
fn advance_puzzle_next_level_then(
&self,
input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleRunProcedureResult, __sdk::InternalError>) + Send + 'static,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
);
}
@@ -46,13 +43,17 @@ impl advance_puzzle_next_level for super::RemoteProcedures {
&self,
input: PuzzleRunNextLevelInput,
__callback: impl FnOnce(&super::ProcedureEventContext, Result<PuzzleRunProcedureResult, __sdk::InternalError>) + Send + 'static,
__callback: impl FnOnce(
&super::ProcedureEventContext,
Result<PuzzleRunProcedureResult, __sdk::InternalError>,
) + Send
+ 'static,
) {
self.imp.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>(
"advance_puzzle_next_level",
AdvancePuzzleNextLevelArgs { input, },
__callback,
);
self.imp
.invoke_procedure_with_callback::<_, PuzzleRunProcedureResult>(
"advance_puzzle_next_level",
AdvancePuzzleNextLevelArgs { input },
__callback,
);
}
}
@@ -2,12 +2,7 @@
// 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 spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -17,12 +12,10 @@ pub struct AiResultReferenceInput {
pub task_id: String,
pub reference_kind: AiResultReferenceKind,
pub reference_id: String,
pub label: Option::<String>,
pub label: Option<String>,
pub created_at_micros: i64,
}
impl __sdk::InModule for AiResultReferenceInput {
type Module = super::RemoteModule;
}
@@ -2,12 +2,7 @@
// 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 spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
#[derive(__lib::ser::Serialize, __lib::de::Deserialize, Clone, PartialEq, Debug)]
#[sats(crate = __lib)]
@@ -24,12 +19,8 @@ pub enum AiResultReferenceKind {
RuntimeItemRecord,
AssetObject,
}
impl __sdk::InModule for AiResultReferenceKind {
type Module = super::RemoteModule;
}
@@ -2,12 +2,7 @@
// 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 spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -18,12 +13,10 @@ pub struct AiResultReferenceSnapshot {
pub task_id: String,
pub reference_kind: AiResultReferenceKind,
pub reference_id: String,
pub label: Option::<String>,
pub label: Option<String>,
pub created_at_micros: i64,
}
impl __sdk::InModule for AiResultReferenceSnapshot {
type Module = super::RemoteModule;
}
@@ -2,12 +2,7 @@
// 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 spacetimedb_sdk::__codegen::{self as __sdk, __lib, __sats, __ws};
use super::ai_result_reference_kind_type::AiResultReferenceKind;
@@ -19,16 +14,14 @@ pub struct AiResultReference {
pub task_id: String,
pub reference_kind: AiResultReferenceKind,
pub reference_id: String,
pub label: Option::<String>,
pub label: Option<String>,
pub created_at: __sdk::Timestamp,
}
impl __sdk::InModule for AiResultReference {
type Module = super::RemoteModule;
}
/// Column accessor struct for the table `AiResultReference`.
///
/// Provides typed access to columns for query building.
@@ -38,7 +31,7 @@ pub struct AiResultReferenceCols {
pub task_id: __sdk::__query_builder::Col<AiResultReference, String>,
pub reference_kind: __sdk::__query_builder::Col<AiResultReference, AiResultReferenceKind>,
pub reference_id: __sdk::__query_builder::Col<AiResultReference, String>,
pub label: __sdk::__query_builder::Col<AiResultReference, Option::<String>>,
pub label: __sdk::__query_builder::Col<AiResultReference, Option<String>>,
pub created_at: __sdk::__query_builder::Col<AiResultReference, __sdk::Timestamp>,
}
@@ -46,14 +39,16 @@ impl __sdk::__query_builder::HasCols for AiResultReference {
type Cols = AiResultReferenceCols;
fn cols(table_name: &'static str) -> Self::Cols {
AiResultReferenceCols {
result_reference_row_id: __sdk::__query_builder::Col::new(table_name, "result_reference_row_id"),
result_reference_row_id: __sdk::__query_builder::Col::new(
table_name,
"result_reference_row_id",
),
result_ref_id: __sdk::__query_builder::Col::new(table_name, "result_ref_id"),
task_id: __sdk::__query_builder::Col::new(table_name, "task_id"),
reference_kind: __sdk::__query_builder::Col::new(table_name, "reference_kind"),
reference_id: __sdk::__query_builder::Col::new(table_name, "reference_id"),
label: __sdk::__query_builder::Col::new(table_name, "label"),
created_at: __sdk::__query_builder::Col::new(table_name, "created_at"),
}
}
}
@@ -70,12 +65,13 @@ impl __sdk::__query_builder::HasIxCols for AiResultReference {
type IxCols = AiResultReferenceIxCols;
fn ix_cols(table_name: &'static str) -> Self::IxCols {
AiResultReferenceIxCols {
result_reference_row_id: __sdk::__query_builder::IxCol::new(table_name, "result_reference_row_id"),
result_reference_row_id: __sdk::__query_builder::IxCol::new(
table_name,
"result_reference_row_id",
),
task_id: __sdk::__query_builder::IxCol::new(table_name, "task_id"),
}
}
}
impl __sdk::__query_builder::CanBeLookupTable for AiResultReference {}

Some files were not shown because too many files have changed in this diff Show More