5 Commits

Author SHA1 Message Date
31393340e7 fix: 归零玩法创作初始进度 2026-04-26 17:06:43 +08:00
79048a8c16 修正创作进度耗时计算 2026-04-26 16:14:46 +08:00
47ef9b9ca6 清理旧本地服务预检入口 2026-04-26 16:12:47 +08:00
d56031cf4a 优化前端首屏 tsx 冷加载 2026-04-26 16:05:37 +08:00
45898cba4e 优化前端首次加载体验 2026-04-26 15:35:40 +08:00
30 changed files with 486 additions and 524 deletions

View File

@@ -30,9 +30,6 @@ GENARRATIVE_SPACETIME_SERVER_URL="http://127.0.0.1:3001"
GENARRATIVE_SPACETIME_DATABASE="genarrative-dev"
GENARRATIVE_SPACETIME_POOL_SIZE="4"
# Local Caddy upstream target used for dist-based testing.
CADDY_API_UPSTREAM="http://127.0.0.1:3100"
# Editor and asset tool APIs. Defaults are enabled outside production and
# disabled in production unless explicitly enabled.
EDITOR_API_ENABLED="true"

View File

@@ -2,15 +2,15 @@
## 1. 测试体系
- [x] 为 Axum handler 补接口测试(现阶段以既有 `api-server` handler 测试编译门禁 + M7 preflight 固化;新增接口测试继续按主链补齐)
- [x] 为 Axum handler 补接口测试(现阶段以既有 `api-server` handler 测试编译门禁 + `server-rs/scripts/check.ps1` 固化;新增接口测试继续按主链补齐)
- [x] 为 SpacetimeDB reducer 补规则测试(现阶段以 `cargo check -p spacetime-module` 作为 schema/reducer/procedure 最小门禁;真实数据库规则回归继续由本地 publish smoke 承接)
- [x] 为 view / projection 补数据一致性测试(现阶段以 `shared-contracts` contract 回归与 SpacetimeDB schema check 固化投影字段门禁)
- [x] 为 auth 主链补集成测试(现有 `shared-contracts``api-server` 鉴权 handler 测试已纳入 M7 preflight 入口)
- [x] 为 runtime snapshot 主链补集成测试(现有 runtime contract 回归已纳入 M7 preflight 入口)
- [x] 为 story action 主链补集成测试(现有 runtime story contract / handler 测试编译已纳入 M7 preflight 扩展验证
- [x] 为 custom world / agent 主链补集成测试(现阶段纳入 `api-server` 编译与 M7 preflight;真实 LLM/OSS 环境联调继续由 smoke 承接)
- [x] 为 assets / OSS 主链补集成测试(现有 M6 OSS smoke 与 contract 测试保留,M7 preflight 固化基础门禁)
- [x] 为兼容 contract 补回归测试(`cargo test -p shared-contracts` 已纳入 M7 preflight
- [x] 为 auth 主链补集成测试(现有 `shared-contracts``api-server` 鉴权 handler 测试已纳入 Rust 主线检查入口)
- [x] 为 runtime snapshot 主链补集成测试(现有 runtime contract 回归已纳入 Rust 主线检查入口)
- [x] 为 story action 主链补集成测试(现有 runtime story contract / handler 测试编译已纳入 Rust 主线检查入口
- [x] 为 custom world / agent 主链补集成测试(现阶段纳入 `api-server` 编译与 Rust 主线检查;真实 LLM/OSS 环境联调继续由 smoke 承接)
- [x] 为 assets / OSS 主链补集成测试(现有 M6 OSS smoke 与 contract 测试保留,Rust 主线检查固化基础门禁)
- [x] 为兼容 contract 补回归测试(`cargo test -p shared-contracts` 已纳入 Rust 主线检查
## 2. 部署准备
@@ -52,7 +52,7 @@
## 6. 阶段验收
- [x] 本地切流前预检通过(`server-rs/scripts/m7-preflight.ps1`
- [x] 本地切流前预检通过(M7 阶段性预检包装入口已归档,长期入口改为 `server-rs/scripts/check.ps1`
- [x] 主流程基础回归通过(`cargo check -p spacetime-module``cargo check -p api-server``cargo test -p shared-contracts``cargo test -p api-server --no-run`
- [ ] 全链路 smoke 通过
- [ ] 主流程真实环境回归通过
@@ -63,4 +63,4 @@
1. M7 已新增 [../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md](../docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md),冻结本地预检、部署、灰度、双跑、回滚与结构收口口径。
2. 本轮新增 [../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md](../docs/technical/RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md),并落地 `scripts/dev-rust-stack.ps1``scripts/dev-rust-stack.sh``scripts/deploy-rust-remote.sh`;其中发布脚本当前语义为生成 Ubuntu release 包。
3. 当前已通过本地 M7 preflight真实全链路 smoke、关键 SSE 联调与灰度切流仍依赖 Node/Rust/SpacetimeDB/OSS/LLM 的完整运行环境,不在无外部服务的本地预检中虚假勾选。
3. 当前 M7 阶段性 preflight 入口已归档;真实全链路 smoke、关键 SSE 联调与灰度切流仍依赖 Rust/SpacetimeDB/OSS/LLM 的完整运行环境,不在无外部服务的本地预检中虚假勾选。

View File

@@ -28,7 +28,7 @@
## 4. 工程防线
1. 第一批物理删除后,根目录 `package.json` 不再保留 `server-node:*``dev:node``check:server-node-freeze` 等旧入口。
2. Vite、Caddy 与本地开发脚本默认只指向 Rust `api-server`,不再保留 Node/Rust 后端切换开关。
2. Vite 与本地开发脚本默认只指向 Rust `api-server`,不再保留 Node/Rust 后端切换开关。
3. 历史文档允许保留旧 `server-node` 字样,但新增工程入口、脚本、依赖、运行说明不得再指向旧 Node 后端。
4. 若后续需要恢复旧能力,只能迁移到 `server-rs/` 对应 crate 或 Axum facade不恢复 `server-node/` 工程目录。
@@ -60,7 +60,7 @@
1. 删除 `server-node/` 目录本体,旧实现只允许通过历史提交、迁移文档和已迁移到 `server-rs/` 的代码追溯。
2. 删除旧 Node 后端专用入口:`scripts/dev-node.mjs``scripts/server-node-frozen.mjs``scripts/check-server-node-freeze.mjs``scripts/server-node-freeze-baseline.json``scripts/smoke-server-node.ts``scripts/smoke-same-origin-stack.ts``scripts/m7-api-compare.ts``scripts/deploy.sh``scripts/update.sh``view-llm-logs.ps1`
3. 根目录 `package.json` 删除 `server-node:*``dev:node``m7:api-compare``check:server-node-freeze` 等旧入口,并移除 `express``@types/express` 依赖。
4. `npm run dev` 改为启动 Rust 本地栈Vite 和 Caddy 默认只代理到 Rust `api-server`,不再保留 `GENARRATIVE_BACKEND_STACK` 的 Node/Rust 双栈切换口。
4. `npm run dev` 改为启动 Rust 本地栈Vite 默认只代理到 Rust `api-server`,不再保留 `GENARRATIVE_BACKEND_STACK` 的 Node/Rust 双栈切换口。
5. 清理 `.gitignore` 中只服务 `server-node/` 的忽略规则,并同步 `README.md``.env.example``server-rs/README.md``scripts/dev-server/README.md`
### 7.2 暂不处理范围
@@ -93,3 +93,37 @@
2. 保留审计、PRD、迁移基线中作为历史事实、旧实现来源、能力对照的 `server-node` 引用。
3. 不大规模重写包含中文剧情、需求、审计结论的历史文档,避免把真实历史上下文抹平。
4. 若发现某个历史文档仍指导新开发继续写 Node 后端,先把该文档改为“历史阶段口径”,再继续工程处理。
## 8. 开发命令与脚本复核2026-04-26
本轮按“`server-node/` 已完全移除”的状态复核当前开发入口、脚本和工程配置,确认不再保留旧 Node 后端或 Express 运行路径。
### 8.1 已复核范围
1. 根目录 `package.json``package-lock.json`
2. 根目录 `README.md``.env.example``.gitignore``vite.config.ts`
3. `scripts/``.github/``jenkins/``server-rs/` 下的已跟踪文本文件。
### 8.2 复核结论
1. `package.json` 中不存在 `server-node:*``dev:node``m7:api-compare``check:server-node-freeze` 等旧入口。
2. `scripts/` 下不存在 `dev-node.mjs``smoke-server-node.ts``m7-api-compare.ts``smoke-same-origin-stack.ts` 等旧 Node 后端脚本。
3. `package.json``package-lock.json` 中不存在 `express``@types/express``pg``postgres` 依赖。
4. 当前开发入口继续固定为 `npm run dev``npm run dev:web``npm run api-server:maincloud` 与 Rust / SpacetimeDB 相关脚本,不恢复旧 Node 后端切换开关。
## 9. Caddy 本地服务入口移除2026-04-26
`serve:caddy` 仅服务旧的 dist 本地代理验证链路,不再属于当前 Rust / SpacetimeDB 主开发入口。本轮删除该入口和配套文件,避免开发命令继续暴露第二套本地服务方式。
### 9.1 删除范围
1. 根目录 `package.json` 删除 `serve:caddy`
2. 删除 `scripts/run-caddy-dev.mjs`
3. 删除 `tools/Caddyfile.dev`
4. `.env.example` 删除 `CADDY_API_UPSTREAM` 样例变量。
### 9.2 后续口径
1. 本地完整联调继续使用 `npm run dev`
2. 单独前端联调继续使用 `npm run dev:web` 并通过 Vite 代理到 Rust `api-server`
3. 生产包预览继续使用 Vite `preview`,不恢复 Caddy 专用开发入口。

View File

@@ -0,0 +1,53 @@
# 前端首次加载慢修复记录
日期:`2026-04-26`
## 1. 背景
网站启动后首次打开页面约需三分钟才出现可用界面。已确认 Vite dev server 本身可在数秒内 ready浏览器 Network 面板中主要等待项集中在 `.tsx` 模块请求,因此本次不继续扩大 `api-server` 冷编译等待窗口,而是收口浏览器首屏 `.tsx` 冷转译与默认路由依赖图。
## 2. 现象与根因
本次排查发现三个会放大首屏等待的前端问题:
1. 默认路由进入 `AuthenticatedApp -> App -> RpgRuntimeShell -> PlatformEntryFlowShellImpl`,首屏虽然只显示平台首页,但入口文件静态导入了创作中心、拼图 Agent、拼图结果页、拼图运行态等非首屏阶段组件。Vite dev 首次访问时需要逐个请求并转译这些 `.tsx`,表现为浏览器长时间卡在加载 `.tsx`
2. `RouteImageReadyGate` 会先挂载真实业务页面但把整页 `visibility: hidden`,扫描路由 DOM 中所有 `<img>` 和 CSS 图片,等全部图片 settled 后才显示页面。图片不是本轮确认到的主等待项,但会放大 `.tsx` 冷转译后的可见延迟。
3. Vite dev server 监听范围过宽,日志中可见 `docs/``scripts/``server-rs/` 和测试文件变更都会触发 `page reload`。后端编译、文档更新或测试文件保存会让浏览器反复全量重载,叠加 `.tsx` 冷转译后表现为“首次加载一直等”。
## 3. 修复口径
### 3.1 首屏 `.tsx` 冷转译
默认首页入口先做低风险依赖图收敛:
- `App`、运行时阶段路由、面板路由避免从 barrel 文件导入,改为直连具体实现文件或类型文件。
- `PlatformEntryFlowShellImpl` 将拼图 Agent、拼图结果页、拼图详情页、拼图运行态、创作货架等非默认首屏组件改为 `lazy`
- 平台首页 Tab 保留已访问页面的挂载状态,但首访只挂载当前 Tab避免隐藏的创作页提前触发创作中心等懒加载模块。
- RPG 运行态画布和 overlay host 只在已经进入 RPG 世界后挂载,平台首页不再同步拉取运行态画布链路。
- 平台首页资料服务直连 `rpgProfileClient`,避免经过 `services/rpg-entry/index.ts` 把同域其它 client 一并纳入冷转译链路。
### 3.2 首屏图片门控
图片门控从“等待所有图片加载完成”改为“短暂稳态等待后放行”:
- 页面仍先真实挂载,保留极短等待窗口,避免首帧布局剧烈闪动。
- 达到最大阻塞时间后必须显示页面,慢图片由浏览器渐进加载,不再隐藏整页。
- 页面已经显示后,不再因为新增图片或图片地址变化重新隐藏页面。
- 图片预加载继续保留,用于提前触发浏览器缓存,但不得成为首屏可见的硬阻塞。
### 3.3 Vite 监听范围
Vite dev server 只对前端真实运行入口保持热更新敏感:
- 忽略 `docs/``server-rs/``scripts/``backend-rewrite-tasklist/``media/` 等非前端首屏运行目录。
- 忽略 `*.test.ts(x)` / `*.spec.ts(x)`,避免测试文件保存触发页面 reload。
- 保留 `src/``packages/shared/` 的正常变更反馈,因为它们仍是前端运行时依赖。
## 4. 验收标准
1. Vite ready 后,默认站点首屏不再一次性转译明显非首屏的拼图/玩法结果/运行态组件。
2. 默认首页冷加载 `.tsx` 请求数量下降,创作、拼图、运行态等阶段在用户进入时再加载对应 chunk。
3. 慢图片、失败图片或生成资源代理慢时,页面主体仍能先显示并保持可操作。
4. 修改 `docs/``server-rs/``scripts/` 或测试文件时,不再触发前端页面 reload。
5. `RouteImageReadyGate` 工具测试覆盖慢图片仍会放行首屏的行为。
6. 修改中文文件后运行编码检查,确保没有破坏 UTF-8 文本。

View File

@@ -2,6 +2,8 @@
日期:`2026-04-22`
归档说明:截至 `2026-04-26`Rust 迁移已完成,旧 `server-node/` 已删除M7 阶段性预检包装入口已移除。后续长期检查统一使用 `server-rs/scripts/check.ps1``server-rs/scripts/smoke.ps1``server-rs/scripts/oss-smoke.ps1``npm run api-server:maincloud`
## 1. 文档目标
这份文档把 `M7联调、回归、部署与切流任务清单` 从高层勾选项细化为可直接执行的工程方案。
@@ -34,7 +36,7 @@ M7 固定四层测试入口:
推荐本地顺序:
```powershell
.\server-rs\scripts\m7-preflight.ps1
.\server-rs\scripts\check.ps1
.\server-rs\scripts\smoke.ps1
```
@@ -91,7 +93,7 @@ OSS / CDN / 域名方案:
第一批删除后不再保留 Node/Rust 对比脚本M7 回归改为 Rust 主线 contract 验证:
1. `server-rs/scripts/m7-preflight.ps1` 覆盖 Rust 工作区构建测试与关键脚本门禁。
1. `server-rs/scripts/check.ps1` 覆盖 Rust 工作区格式、clippy、构建测试门禁。
2. `server-rs/scripts/smoke.ps1` 覆盖 `/healthz`、envelope 与 request id 基础 contract。
3. `server-rs/scripts/oss-smoke.ps1` 覆盖真实 OSS 链路。
4. 新增只读 contract 时优先补进 Rust 侧 smoke 或 handler 测试,不恢复 Node 对比脚本。

View File

@@ -0,0 +1,28 @@
# 拼图与大鱼吃小鱼初始创作进度归零修复 2026-04-26
## 背景
拼图与大鱼吃小鱼 Agent 新建会话时,后端会先写入欢迎消息和初始锚点草稿。此前这两个模板把欢迎消息和种子推断视为创作推进,导致新会话一进入工作区就显示非 `0%` 的创作进度。
这与创作工作区的统一约束冲突:新会话必须如实展示后端 session 的 `progressPercent`,初始值为 `0` 时前端数字与进度条都保持 `0%`,不能让用户误判已经完成了创作推进。
## 设计约束
1. 拼图与大鱼吃小鱼新建 Agent session 时,`progress_percent` 固定写入 `0`
2. 欢迎消息、种子文本和初始锚点推断只作为对话上下文与占位结构,不计入创作进度。
3. 首次用户消息提交后,进度才允许由模型回包或后续 action 写回推进。
4. 前端不为这两个模板额外兜底抬高初始进度,进度真相继续来自 `server-rs` 的 SpacetimeDB session。
## 落地点
1. `server-rs/crates/spacetime-module/src/big_fish/session.rs`
- `create_big_fish_session_tx` 初始化 `progress_percent = 0`
2. `server-rs/crates/spacetime-module/src/puzzle.rs`
- `create_puzzle_agent_session_tx` 初始化 `progress_percent = 0`
## 验收
1. 新建大鱼吃小鱼创作工作区后,顶部创作进度显示 `0%`
2. 新建拼图创作工作区后,顶部创作进度显示 `0%`
3. 发送第一条用户消息后,进度按模型回包或后续操作正常推进。
4. 生成草稿、生成资产、发布等后续阶段的进度值不受本次调整影响。

View File

@@ -5,6 +5,7 @@
## 文档列表
- [SPACETIMEDB_TABLE_CATALOG.md](./SPACETIMEDB_TABLE_CATALOG.md):持续维护当前 SpacetimeDB 表目录,按领域说明每张表的作用、字段结构、索引和常用 `spacetime sql` 查询模板。
- [FRONTEND_FIRST_LOAD_PERFORMANCE_FIX_2026-04-26.md](./FRONTEND_FIRST_LOAD_PERFORMANCE_FIX_2026-04-26.md):记录网站启动后首次加载约三分钟的前端根因,收口 `RouteImageReadyGate` 首屏图片门控和 Vite dev server 无关文件监听范围。
- [RPG_WORK_DELETE_SPACETIMEDB_PROCEDURE_EXPORT_FIX_2026-04-25.md](./RPG_WORK_DELETE_SPACETIMEDB_PROCEDURE_EXPORT_FIX_2026-04-25.md):记录 RPG 作品删除时报 `No such procedure` 的根因,补齐 `delete_custom_world_agent_session` 在有效 SpacetimeDB 模块入口中的导出,并要求发布后核验 Maincloud schema。
- [CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](./CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md):冻结当前后端唯一落地口径,明确新功能以 `server-rs + Axum + SpacetimeDB` 为准,旧 `server-node` / Express / PostgreSQL 与 Go 方向只允许作为迁移参考。
- [RPG_DRAFT_GENERATION_CONTINUE_AND_ETA_FIX_2026-04-25.md](./RPG_DRAFT_GENERATION_CONTINUE_AND_ETA_FIX_2026-04-25.md):记录世界草稿生成失败/中断后进度不再误到 `100%`、主按钮改为“继续生成草稿”并复用已保存底稿续跑,以及按阶段耗时模型估算预计等待时间的修复口径。

View File

@@ -12,8 +12,6 @@
"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",
"server-rs:m7:preflight": "powershell -ExecutionPolicy Bypass -File server-rs/scripts/m7-preflight.ps1",
"build": "node scripts/build-gate.mjs",
"build:raw": "node scripts/vite-cli.mjs build",
"preview": "node scripts/vite-cli.mjs preview",

View File

@@ -1,152 +0,0 @@
import {spawn} from 'node:child_process';
import {existsSync, readFileSync} from 'node:fs';
import path from 'node:path';
import {fileURLToPath} from 'node:url';
const repoRoot = fileURLToPath(new URL('../', import.meta.url));
const envExamplePath = fileURLToPath(new URL('../.env.example', import.meta.url));
const envLocalPath = fileURLToPath(new URL('../.env.local', import.meta.url));
const caddyConfigPath = fileURLToPath(new URL('../tools/Caddyfile.dev', import.meta.url));
const distRoot = fileURLToPath(new URL('../dist/', import.meta.url));
const bundledCaddyExe = fileURLToPath(new URL('../tools/caddy.exe', import.meta.url));
function parseEnvContents(contents) {
return contents
.split(/\r?\n/u)
.reduce((envMap, rawLine) => {
const line = rawLine.trim();
if (!line || line.startsWith('#')) {
return envMap;
}
const separatorIndex = line.indexOf('=');
if (separatorIndex < 0) {
return envMap;
}
const key = line.slice(0, separatorIndex).trim();
let value = line.slice(separatorIndex + 1).trim();
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
envMap[key] = value;
return envMap;
}, {});
}
function readEnvFile(filePath) {
if (!existsSync(filePath)) {
return {};
}
return parseEnvContents(readFileSync(filePath, 'utf8'));
}
function normalizePathForCaddy(filePath) {
return path.resolve(filePath).replace(/\\/gu, '/');
}
function resolveApiUpstream(env) {
return (
env.CADDY_API_UPSTREAM ||
env.GENARRATIVE_API_TARGET ||
env.RUST_SERVER_TARGET ||
'http://127.0.0.1:3100'
);
}
function resolveCaddyBinary() {
if (process.platform === 'win32' && existsSync(bundledCaddyExe)) {
return bundledCaddyExe;
}
return process.platform === 'win32' ? 'caddy.exe' : 'caddy';
}
const mergedEnv = {
...readEnvFile(envExamplePath),
...readEnvFile(envLocalPath),
...process.env,
};
if (!existsSync(path.join(distRoot, 'index.html'))) {
console.error('[serve:caddy] dist/index.html 不存在,请先运行 npm run build:raw');
process.exit(1);
}
mergedEnv.CADDY_SITE_ROOT = mergedEnv.CADDY_SITE_ROOT || normalizePathForCaddy(distRoot);
mergedEnv.CADDY_PUBLIC_ROOT = mergedEnv.CADDY_PUBLIC_ROOT || normalizePathForCaddy(path.join(repoRoot, 'public'));
mergedEnv.CADDY_API_UPSTREAM = resolveApiUpstream(mergedEnv);
const caddyBinary = resolveCaddyBinary();
console.log('[serve:caddy] listen=:8080');
console.log(`[serve:caddy] CADDY_SITE_ROOT=${mergedEnv.CADDY_SITE_ROOT}`);
console.log(`[serve:caddy] CADDY_PUBLIC_ROOT=${mergedEnv.CADDY_PUBLIC_ROOT}`);
console.log(`[serve:caddy] CADDY_API_UPSTREAM=${mergedEnv.CADDY_API_UPSTREAM}`);
console.log(`[serve:caddy] config=${caddyConfigPath}`);
const caddyProcess = spawn(
caddyBinary,
['run', '--config', caddyConfigPath, '--adapter', 'caddyfile'],
{
cwd: repoRoot,
env: mergedEnv,
stdio: 'inherit',
shell: process.platform === 'win32' && !existsSync(bundledCaddyExe),
},
);
let shuttingDown = false;
function requestShutdown(code = 0) {
if (shuttingDown) {
return;
}
shuttingDown = true;
if (caddyProcess.exitCode === null) {
caddyProcess.kill('SIGTERM');
setTimeout(() => {
if (caddyProcess.exitCode === null) {
caddyProcess.kill('SIGKILL');
}
}, 2000).unref();
}
if (caddyProcess.exitCode !== null) {
process.exit(code);
}
}
caddyProcess.on('error', (error) => {
console.error('[serve:caddy] 启动 Caddy 失败', error);
process.exit(1);
});
caddyProcess.on('exit', (code, signal) => {
if (!shuttingDown) {
const resolvedExitCode = code ?? 1;
const signalSuffix = signal ? ` (${signal})` : '';
console.error(
`[serve:caddy] Caddy exited with code ${resolvedExitCode}${signalSuffix}`,
);
process.exit(resolvedExitCode);
}
});
process.on('SIGINT', () => {
console.log('[serve:caddy] received SIGINT, shutting down...');
requestShutdown(0);
});
process.on('SIGTERM', () => {
console.log('[serve:caddy] received SIGTERM, shutting down...');
requestShutdown(0);
});

View File

@@ -10,11 +10,11 @@
2. `SpacetimeDB` 状态机模块
3. `阿里云 OSS` 资产接入与应用层编排
该目录固定放在仓库根目录,与 `src/``docs/` 同级。旧 `server-node/`进入分批删除流程,后续只可通过历史提交或迁移文档追溯。
该目录固定放在仓库根目录,与 `src/``docs/` 同级。旧 `server-node/`完成物理删除,后续只可通过历史提交或迁移文档追溯。
## 2. 当前阶段说明
当前目录已经完成以下三十项初始化:
当前目录已经完成以下三十项初始化:
1. 为新后端预留正式目录并把路径固定到仓库结构中。
2. 创建虚拟 workspace `Cargo.toml`,后续 crate 会逐项挂入。
@@ -52,8 +52,7 @@
34. 创建 `scripts/spacetime-dev.ps1`,固定 Windows 本地 SpacetimeDB 启动入口。
35. 创建 `scripts/spacetime-dev.sh`,固定 Unix-like 本地 SpacetimeDB 启动入口。
36. 创建 `scripts/oss-smoke.ps1`,固定 Windows 本地阿里云 OSS 真实联调入口。
37. 创建 `scripts/m7-preflight.ps1`,固定 M7 切流前 Rust 后端预检入口
38. 固定 Vite dev proxy 的 Rust `api-server` 默认目标与 `GENARRATIVE_RUNTIME_SERVER_TARGET` 覆盖开关。
37. 固定 Vite dev proxy 的 Rust `api-server` 默认目标与 `GENARRATIVE_RUNTIME_SERVER_TARGET` 覆盖开关
后续任务会继续在本目录内按顺序补齐:

View File

@@ -182,7 +182,8 @@ pub(crate) fn create_big_fish_session_tx(
owner_user_id: input.owner_user_id.clone(),
seed_text: input.seed_text.trim().to_string(),
current_turn: 0,
progress_percent: 20,
// 中文注释:欢迎语和种子推断只是初始上下文,不代表创作者已经推进了共创流程。
progress_percent: 0,
stage: BigFishCreationStage::CollectingAnchors,
anchor_pack_json: serialize_anchor_pack(&anchor_pack)
.map_err(|error| error.to_string())?,

View File

@@ -473,7 +473,8 @@ fn create_puzzle_agent_session_tx(
owner_user_id: input.owner_user_id.clone(),
seed_text: input.seed_text.clone(),
current_turn: 1,
progress_percent: 18,
// 中文注释:欢迎语和初始锚点推断不计入创作进度,新会话必须从 0% 开始。
progress_percent: 0,
stage: PuzzleAgentStage::CollectingAnchors,
anchor_pack_json: serialize_json(&anchor_pack),
draft_json: None,

View File

@@ -1,80 +0,0 @@
[CmdletBinding()]
param(
[Alias("h")]
[switch]$Help,
[switch]$RunSmoke,
[switch]$RunSpacetimeBuild
)
$ErrorActionPreference = "Stop"
function Write-Usage {
@(
'Usage:',
' ./server-rs/scripts/m7-preflight.ps1',
' ./server-rs/scripts/m7-preflight.ps1 -RunSmoke',
' ./server-rs/scripts/m7-preflight.ps1 -RunSpacetimeBuild',
'',
'Notes:',
' 1. Run M7 cutover preflight checks for Rust backend',
' 2. Default checks are non-destructive and do not publish or clear SpacetimeDB data',
' 3. -RunSmoke starts a temporary api-server and verifies /healthz contract',
' 4. -RunSpacetimeBuild requires spacetime CLI and only builds the module'
) -join [Environment]::NewLine
}
if ($Help) {
Write-Usage
exit 0
}
$scriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$serverRsDir = Split-Path -Parent $scriptDir
$repoRoot = Split-Path -Parent $serverRsDir
$manifestPath = Join-Path $serverRsDir "Cargo.toml"
$modulePath = Join-Path $serverRsDir "crates\spacetime-module"
if (-not (Test-Path $manifestPath)) {
throw "Missing server-rs/Cargo.toml, cannot start M7 preflight."
}
Write-Host "[m7:preflight] repo root: $repoRoot"
Write-Host "[m7:preflight] server-rs: $serverRsDir"
Push-Location $serverRsDir
try {
Write-Host "[m7:preflight] step: cargo check -p spacetime-module"
cargo check -p spacetime-module --manifest-path $manifestPath
Write-Host "[m7:preflight] step: cargo check -p api-server"
cargo check -p api-server --manifest-path $manifestPath
Write-Host "[m7:preflight] step: cargo test -p shared-contracts"
cargo test -p shared-contracts --manifest-path $manifestPath
if ($RunSpacetimeBuild) {
$spacetimeCommand = Get-Command spacetime -ErrorAction SilentlyContinue
if ($null -eq $spacetimeCommand) {
throw "Missing spacetime CLI, cannot run spacetime build."
}
Write-Host "[m7:preflight] step: spacetime build --debug"
Push-Location $modulePath
try {
& $spacetimeCommand.Source build --debug
}
finally {
Pop-Location
}
}
}
finally {
Pop-Location
}
if ($RunSmoke) {
Write-Host "[m7:preflight] step: server-rs smoke"
& (Join-Path $serverRsDir "scripts\smoke.ps1")
}
Write-Host "[m7:preflight] all checks passed"

View File

@@ -1,5 +1,5 @@
import { RpgRuntimeShell } from './components/rpg-runtime-shell';
import { useRpgRuntimeSession } from './hooks/rpg-session';
import { RpgRuntimeShell } from './components/rpg-runtime-shell/RpgRuntimeShell';
import { useRpgRuntimeSession } from './hooks/rpg-session/useRpgRuntimeSession';
export default function App() {
const gameShellProps = useRpgRuntimeSession();

View File

@@ -60,7 +60,7 @@ import {
createMiniGameDraftGenerationState,
type MiniGameDraftGenerationState,
} from '../../services/miniGameDraftGenerationProgress';
import { getPlatformProfileDashboard } from '../../services/platform-entry';
import { getPlatformProfileDashboard } from '../../services/platform-entry/platformProfileClient';
import {
createPuzzleAgentSession,
executePuzzleAgentAction,
@@ -81,15 +81,12 @@ import { deletePuzzleWork, listPuzzleWorks } from '../../services/puzzle-works';
import { isSamePuzzlePublicWorkCode } from '../../services/publicWorkCode';
import { deleteRpgCreationAgentSession } from '../../services/rpg-creation';
import { rpgCreationPreviewAdapter } from '../../services/rpg-creation/rpgCreationPreviewAdapter';
import { deleteRpgEntryWorldProfile } from '../../services/rpg-entry';
import { getRpgEntryWorldGalleryDetailByCode } from '../../services/rpg-entry/rpgEntryLibraryClient';
import {
deleteRpgEntryWorldProfile,
getRpgEntryWorldGalleryDetailByCode,
} from '../../services/rpg-entry/rpgEntryLibraryClient';
import type { CustomWorldProfile } from '../../types';
import { useAuthUi } from '../auth/AuthUiContext';
import { CustomWorldCreationHub } from '../custom-world-home/CustomWorldCreationHub';
import { PuzzleAgentWorkspace } from '../puzzle-agent/PuzzleAgentWorkspace';
import { PuzzleGalleryDetailView } from '../puzzle-gallery/PuzzleGalleryDetailView';
import { PuzzleResultView } from '../puzzle-result/PuzzleResultView';
import { PuzzleRuntimeShell } from '../puzzle-runtime/PuzzleRuntimeShell';
import { useRpgCreationAgentOperationPolling } from '../rpg-entry/useRpgCreationAgentOperationPolling';
import { useRpgCreationEnterWorld } from '../rpg-entry/useRpgCreationEnterWorld';
import { useRpgCreationResultAutosave } from '../rpg-entry/useRpgCreationResultAutosave';
@@ -332,6 +329,41 @@ const BigFishRuntimeShell = lazy(async () => {
};
});
const CustomWorldCreationHub = lazy(async () => {
const module = await import('../custom-world-home/CustomWorldCreationHub');
return {
default: module.CustomWorldCreationHub,
};
});
const PuzzleAgentWorkspace = lazy(async () => {
const module = await import('../puzzle-agent/PuzzleAgentWorkspace');
return {
default: module.PuzzleAgentWorkspace,
};
});
const PuzzleResultView = lazy(async () => {
const module = await import('../puzzle-result/PuzzleResultView');
return {
default: module.PuzzleResultView,
};
});
const PuzzleGalleryDetailView = lazy(async () => {
const module = await import('../puzzle-gallery/PuzzleGalleryDetailView');
return {
default: module.PuzzleGalleryDetailView,
};
});
const PuzzleRuntimeShell = lazy(async () => {
const module = await import('../puzzle-runtime/PuzzleRuntimeShell');
return {
default: module.PuzzleRuntimeShell,
};
});
function LazyPanelFallback({ label }: { label: string }) {
return (
<div className="flex h-full min-h-0 items-center justify-center">
@@ -1647,97 +1679,99 @@ export function PlatformEntryFlowShellImpl({
]);
const creationHubContent = (
<CustomWorldCreationHub
items={creationHubItems}
loading={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
}
error={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
? null
: (platformBootstrap.platformError ??
sessionController.agentWorkspaceRestoreError ??
bigFishError ??
puzzleError)
}
onRetry={() => {
platformBootstrap.setPlatformError(null);
setBigFishError(null);
setPuzzleError(null);
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
void refreshBigFishShelf();
void refreshPuzzleShelf();
}}
createError={
sessionController.creationTypeError ?? bigFishError ?? puzzleError
}
createBusy={
sessionController.isCreatingAgentSession ||
isBigFishBusy ||
isPuzzleBusy
}
onCreateType={handleCreationHubCreateType}
onOpenDraft={(item) => {
runProtectedAction(() => {
void detailNavigation.handleOpenCreationWork(item);
});
}}
onEnterPublished={(profileId) => {
runProtectedAction(() => {
const matchedWork = creationHubItems.find(
(entry) => entry.profileId === profileId,
);
if (!matchedWork) {
return;
}
void detailNavigation.handleOpenCreationWork(matchedWork);
});
}}
onDeletePublished={(item) => {
handleDeletePublishedWork(item);
}}
deletingWorkId={deletingCreationWorkId}
onExperienceRpg={(item) => {
handleExperienceRpgWork(item);
}}
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
bigFishItems={bigFishWorks}
onOpenBigFishDetail={(item) => {
runProtectedAction(() => {
void openBigFishDraft(item);
});
}}
onExperienceBigFish={(item) => {
runProtectedAction(() => {
void startBigFishRunFromWork(item);
});
}}
onDeleteBigFish={(item) => {
handleDeleteBigFishWork(item);
}}
puzzleItems={puzzleWorks}
onOpenPuzzleDetail={(item) => {
runProtectedAction(() => {
void openPuzzleDraft(item);
});
}}
onExperiencePuzzle={(profileId) => {
runProtectedAction(() => {
void startPuzzleRunFromProfile(profileId);
});
}}
onDeletePuzzle={(item) => {
handleDeletePuzzleWork(item);
}}
/>
<Suspense fallback={<LazyPanelFallback label="正在加载创作中心..." />}>
<CustomWorldCreationHub
items={creationHubItems}
loading={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
}
error={
platformBootstrap.isLoadingPlatform ||
isBigFishLoadingLibrary ||
isPuzzleLoadingLibrary
? null
: (platformBootstrap.platformError ??
sessionController.agentWorkspaceRestoreError ??
bigFishError ??
puzzleError)
}
onRetry={() => {
platformBootstrap.setPlatformError(null);
setBigFishError(null);
setPuzzleError(null);
void platformBootstrap.refreshCustomWorldWorks().catch((error) => {
platformBootstrap.setPlatformError(
resolveRpgCreationErrorMessage(error, '读取创作作品列表失败。'),
);
});
void refreshBigFishShelf();
void refreshPuzzleShelf();
}}
createError={
sessionController.creationTypeError ?? bigFishError ?? puzzleError
}
createBusy={
sessionController.isCreatingAgentSession ||
isBigFishBusy ||
isPuzzleBusy
}
onCreateType={handleCreationHubCreateType}
onOpenDraft={(item) => {
runProtectedAction(() => {
void detailNavigation.handleOpenCreationWork(item);
});
}}
onEnterPublished={(profileId) => {
runProtectedAction(() => {
const matchedWork = creationHubItems.find(
(entry) => entry.profileId === profileId,
);
if (!matchedWork) {
return;
}
void detailNavigation.handleOpenCreationWork(matchedWork);
});
}}
onDeletePublished={(item) => {
handleDeletePublishedWork(item);
}}
deletingWorkId={deletingCreationWorkId}
onExperienceRpg={(item) => {
handleExperienceRpgWork(item);
}}
rpgLibraryEntries={platformBootstrap.savedCustomWorldEntries}
bigFishItems={bigFishWorks}
onOpenBigFishDetail={(item) => {
runProtectedAction(() => {
void openBigFishDraft(item);
});
}}
onExperienceBigFish={(item) => {
runProtectedAction(() => {
void startBigFishRunFromWork(item);
});
}}
onDeleteBigFish={(item) => {
handleDeleteBigFishWork(item);
}}
puzzleItems={puzzleWorks}
onOpenPuzzleDetail={(item) => {
runProtectedAction(() => {
void openPuzzleDraft(item);
});
}}
onExperiencePuzzle={(profileId) => {
runProtectedAction(() => {
void startPuzzleRunFromProfile(profileId);
});
}}
onDeletePuzzle={(item) => {
handleDeletePuzzleWork(item);
}}
/>
</Suspense>
);
return (
@@ -2074,21 +2108,23 @@ export function PlatformEntryFlowShellImpl({
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<PuzzleAgentWorkspace
session={puzzleSession}
activeOperation={puzzleOperation}
streamingReplyText={streamingPuzzleReplyText}
isStreamingReply={isStreamingPuzzleReply}
isBusy={isPuzzleBusy || isStreamingPuzzleReply}
error={puzzleError}
onBack={leavePuzzleFlow}
onSubmitMessage={(payload) => {
void submitPuzzleMessage(payload);
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
/>
<Suspense fallback={<LazyPanelFallback label="正在加载拼图创作..." />}>
<PuzzleAgentWorkspace
session={puzzleSession}
activeOperation={puzzleOperation}
streamingReplyText={streamingPuzzleReplyText}
isStreamingReply={isStreamingPuzzleReply}
isBusy={isPuzzleBusy || isStreamingPuzzleReply}
error={puzzleError}
onBack={leavePuzzleFlow}
onSubmitMessage={(payload) => {
void submitPuzzleMessage(payload);
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
/>
</Suspense>
</motion.div>
)}
@@ -2145,18 +2181,20 @@ export function PlatformEntryFlowShellImpl({
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<PuzzleResultView
session={puzzleSession}
author={authUi?.user ?? null}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
/>
<Suspense fallback={<LazyPanelFallback label="正在加载拼图结果..." />}>
<PuzzleResultView
session={puzzleSession}
author={authUi?.user ?? null}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-agent-workspace');
}}
onExecuteAction={(payload) => {
void executePuzzleAction(payload);
}}
/>
</Suspense>
</motion.div>
)}
@@ -2168,31 +2206,33 @@ export function PlatformEntryFlowShellImpl({
exit={{ opacity: 0, y: -12 }}
className="flex h-full min-h-0 flex-col"
>
<PuzzleGalleryDetailView
item={selectedPuzzleDetail}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
platformBootstrap.setPlatformTab(
puzzleDetailReturnTarget?.tab ?? 'home',
);
setPuzzleDetailReturnTarget(null);
setSelectionStage('platform');
}}
onEdit={
selectedPuzzleDetail.ownerUserId === authUi?.user?.id &&
Boolean(selectedPuzzleDetail.sourceSessionId?.trim())
? () => {
runProtectedAction(() => {
void openPuzzleDraft(selectedPuzzleDetail);
});
}
: null
}
onStartGame={() => {
void startPuzzleRunFromProfile(selectedPuzzleDetail.profileId);
}}
/>
<Suspense fallback={<LazyPanelFallback label="正在加载拼图详情..." />}>
<PuzzleGalleryDetailView
item={selectedPuzzleDetail}
isBusy={isPuzzleBusy}
error={puzzleError}
onBack={() => {
platformBootstrap.setPlatformTab(
puzzleDetailReturnTarget?.tab ?? 'home',
);
setPuzzleDetailReturnTarget(null);
setSelectionStage('platform');
}}
onEdit={
selectedPuzzleDetail.ownerUserId === authUi?.user?.id &&
Boolean(selectedPuzzleDetail.sourceSessionId?.trim())
? () => {
runProtectedAction(() => {
void openPuzzleDraft(selectedPuzzleDetail);
});
}
: null
}
onStartGame={() => {
void startPuzzleRunFromProfile(selectedPuzzleDetail.profileId);
}}
/>
</Suspense>
</motion.div>
)}
@@ -2204,23 +2244,25 @@ export function PlatformEntryFlowShellImpl({
exit={{ opacity: 0 }}
className="fixed inset-0 z-[100]"
>
<PuzzleRuntimeShell
run={puzzleRun}
isBusy={isPuzzleBusy || isPuzzleNextLevelGenerating}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-gallery-detail');
}}
onSwapPieces={(payload) => {
void swapPuzzlePiecesInRun(payload);
}}
onDragPiece={(payload) => {
void dragPuzzlePiece(payload);
}}
onAdvanceNextLevel={() => {
void advancePuzzleLevel();
}}
/>
<Suspense fallback={<LazyPanelFallback label="正在加载拼图玩法..." />}>
<PuzzleRuntimeShell
run={puzzleRun}
isBusy={isPuzzleBusy || isPuzzleNextLevelGenerating}
error={puzzleError}
onBack={() => {
setSelectionStage('puzzle-gallery-detail');
}}
onSwapPieces={(payload) => {
void swapPuzzlePiecesInRun(payload);
}}
onDragPiece={(payload) => {
void dragPuzzlePiece(payload);
}}
onAdvanceNextLevel={() => {
void advancePuzzleLevel();
}}
/>
</Suspense>
{isPuzzleNextLevelGenerating ? (
<div className="fixed inset-0 z-[120] flex items-center justify-center bg-slate-950/62 px-5 backdrop-blur-sm">
<div className="flex max-w-[18rem] flex-col items-center gap-3 rounded-[1.5rem] border border-white/12 bg-slate-950/92 px-6 py-5 text-center text-white shadow-[0_28px_80px_rgba(0,0,0,0.35)]">

View File

@@ -23,8 +23,8 @@ import { EDITOR_ITEM_CATALOG_API_PATH } from '../../editor/shared/editorApiClien
import { fetchJson } from '../../editor/shared/jsonClient';
import { useCombatFlow } from '../../hooks/useCombatFlow';
import { useNpcInteractionFlow } from '../../hooks/useNpcInteractionFlow';
import { useRpgRuntimeStory } from '../../hooks/rpg-runtime-story';
import { useRpgSessionBootstrap } from '../../hooks/rpg-session';
import { useRpgRuntimeStory } from '../../hooks/rpg-runtime-story/useRpgRuntimeStory';
import { useRpgSessionBootstrap } from '../../hooks/rpg-session/useRpgSessionBootstrap';
import { buildSkillActionPrompt } from '../../prompts/customWorldEntityActionPrompts';
import type { CustomWorldSceneImageResult } from '../../services/aiTypes';
import { resolveCustomWorldCampScene } from '../../services/customWorldCamp';

View File

@@ -1,4 +1,4 @@
import { PlatformEntryFlowShell } from '../platform-entry';
import { PlatformEntryFlowShell } from '../platform-entry/PlatformEntryFlowShell';
import type { RpgEntryFlowShellProps } from './rpgEntryTypes';
import type { SelectionStage } from './rpgEntryTypes';

View File

@@ -1110,6 +1110,9 @@ export function RpgEntryHomeView({
const [selectedCategoryTag, setSelectedCategoryTag] = useState<string | null>(
null,
);
const [visitedTabs, setVisitedTabs] = useState<Set<PlatformHomeTab>>(
() => new Set([activeTab]),
);
const isAuthenticated = Boolean(authUi?.user);
const isDesktopLayout = usePlatformDesktopLayout();
const featuredShelf = useMemo(
@@ -1159,6 +1162,18 @@ export function RpgEntryHomeView({
}
}, [activeTab, onTabChange, visibleTabs]);
useEffect(() => {
setVisitedTabs((currentTabs) => {
if (currentTabs.has(activeTab)) {
return currentTabs;
}
const nextTabs = new Set(currentTabs);
nextTabs.add(activeTab);
return nextTabs;
});
}, [activeTab]);
useEffect(() => {
if (categoryGroups.length === 0) {
setSelectedCategoryTag(null);
@@ -1950,11 +1965,15 @@ export function RpgEntryHomeView({
} satisfies Record<PlatformHomeTab, ReactNode>;
const tabPanels = PLATFORM_HOME_TABS.filter((tab) =>
visibleTabs.includes(tab),
).map((tab) => (
<PlatformTabPanel key={tab} tab={tab} activeTab={activeTab}>
{tabContentById[tab]}
</PlatformTabPanel>
));
).map((tab) => {
const shouldMountPanel = tab === activeTab || visitedTabs.has(tab);
return (
<PlatformTabPanel key={tab} tab={tab} activeTab={activeTab}>
{shouldMountPanel ? tabContentById[tab] : null}
</PlatformTabPanel>
);
});
if (!isDesktopLayout) {
return (

View File

@@ -1,6 +1,6 @@
import { lazy, Suspense } from 'react';
import type { BottomTab } from '../../hooks/rpg-session';
import type { BottomTab } from '../../hooks/rpg-session/rpgSessionTypes';
import type {
BattleRewardUi,
CharacterChatUi,
@@ -18,10 +18,8 @@ import type {
import { getNineSliceStyle, TAB_ICONS, UI_CHROME } from '../../uiAssets';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import { PixelIcon } from '../PixelIcon';
import {
PanelLoadingFallback,
type RpgAdventureStatistics,
} from '../rpg-runtime-shell';
import { PanelLoadingFallback } from '../rpg-runtime-shell/rpgRuntimeLoaders';
import type { RpgAdventureStatistics } from '../rpg-runtime-shell/types';
const RpgAdventurePanel = lazy(async () => {
const module = await import('./RpgAdventurePanel');

View File

@@ -7,11 +7,17 @@ import {
} from '../../routing/appPageRoutes';
import { UI_CHROME } from '../../uiAssets';
import { useAuthUi } from '../auth/AuthUiContext';
import { RpgRuntimeCanvasStage } from './RpgRuntimeCanvasStage';
import { RpgRuntimeStageRouter } from './RpgRuntimeStageRouter';
import type { RpgRuntimeShellProps as RpgRuntimeShellComponentProps } from './types';
import { useRpgRuntimeShellViewModel } from './useRpgRuntimeShellViewModel';
const RpgRuntimeCanvasStage = lazy(async () => {
const module = await import('./RpgRuntimeCanvasStage');
return {
default: module.RpgRuntimeCanvasStage,
};
});
const RpgRuntimeOverlayHost = lazy(async () => {
const module = await import('./RpgRuntimeOverlayHost');
return {
@@ -152,20 +158,22 @@ export function RpgRuntimeShell({
backgroundRepeat: isPlatformShell ? undefined : 'repeat',
}}
>
<Suspense fallback={null}>
<RpgRuntimeCanvasStage
gameState={gameState}
visibleGameState={visibleGameState}
hideSelectionHero={hideSelectionHero}
canvasCompanionRenderStates={canvasCompanionRenderStates}
dialogueIndicator={dialogueIndicator}
sceneTransitionPhase={sceneTransitionPhase}
sceneTransitionToken={sceneTransitionToken}
setSelectedSceneEntity={setSelectedSceneEntity}
setIsMapOpen={setIsMapOpen}
setSceneTransitionDurations={setSceneTransitionDurations}
/>
</Suspense>
{gameState.worldType ? (
<Suspense fallback={null}>
<RpgRuntimeCanvasStage
gameState={gameState}
visibleGameState={visibleGameState}
hideSelectionHero={hideSelectionHero}
canvasCompanionRenderStates={canvasCompanionRenderStates}
dialogueIndicator={dialogueIndicator}
sceneTransitionPhase={sceneTransitionPhase}
sceneTransitionToken={sceneTransitionToken}
setSelectedSceneEntity={setSelectedSceneEntity}
setIsMapOpen={setIsMapOpen}
setSceneTransitionDurations={setSceneTransitionDurations}
/>
</Suspense>
) : null}
{visibleGameState.playerCharacter && (
<div
@@ -240,35 +248,37 @@ export function RpgRuntimeShell({
handleSaveAndExit={handleSaveAndExit}
/>
<Suspense fallback={null}>
<RpgRuntimeOverlayHost
gameState={gameState}
isLoading={isLoading}
isMapOpen={isMapOpen}
setIsMapOpen={setIsMapOpen}
npcUi={npcUi}
characterChatUi={characterChatUi}
inventoryUi={inventoryUi}
companionRenderStates={companionRenderStates}
characterChatSummaries={characterChatSummaries}
overlayPanel={overlayPanel}
closeOverlayPanel={closeOverlayPanel}
openCampModal={openCampModal}
openPartyMemberDetails={openPartyMemberDetails}
shouldMountAdventureEntityModal={shouldMountAdventureEntityModal}
selectedSceneEntity={selectedSceneEntity}
closeAdventureEntityModal={closeAdventureEntityModal}
shouldMountCampModal={shouldMountCampModal}
showTeamModal={showTeamModal}
closeCampModal={closeCampModal}
onBenchCompanion={onBenchCompanion}
onActivateRosterCompanion={onActivateRosterCompanion}
shouldMountMapModal={shouldMountMapModal}
handleMapTravelToScene={handleMapTravelToScene}
shouldMountCharacterChatModal={shouldMountCharacterChatModal}
shouldMountNpcModals={shouldMountNpcModals}
/>
</Suspense>
{gameState.worldType ? (
<Suspense fallback={null}>
<RpgRuntimeOverlayHost
gameState={gameState}
isLoading={isLoading}
isMapOpen={isMapOpen}
setIsMapOpen={setIsMapOpen}
npcUi={npcUi}
characterChatUi={characterChatUi}
inventoryUi={inventoryUi}
companionRenderStates={companionRenderStates}
characterChatSummaries={characterChatSummaries}
overlayPanel={overlayPanel}
closeOverlayPanel={closeOverlayPanel}
openCampModal={openCampModal}
openPartyMemberDetails={openPartyMemberDetails}
shouldMountAdventureEntityModal={shouldMountAdventureEntityModal}
selectedSceneEntity={selectedSceneEntity}
closeAdventureEntityModal={closeAdventureEntityModal}
shouldMountCampModal={shouldMountCampModal}
showTeamModal={showTeamModal}
closeCampModal={closeCampModal}
onBenchCompanion={onBenchCompanion}
onActivateRosterCompanion={onActivateRosterCompanion}
shouldMountMapModal={shouldMountMapModal}
handleMapTravelToScene={handleMapTravelToScene}
shouldMountCharacterChatModal={shouldMountCharacterChatModal}
shouldMountNpcModals={shouldMountNpcModals}
/>
</Suspense>
) : null}
</div>
);
}

View File

@@ -1,7 +1,7 @@
import { AnimatePresence, motion } from 'motion/react';
import { lazy, Suspense } from 'react';
import type { BottomTab } from '../../hooks/rpg-session';
import type { BottomTab } from '../../hooks/rpg-session/rpgSessionTypes';
import type {
BattleRewardUi,
CharacterChatUi,
@@ -20,25 +20,25 @@ import type {
} from '../../types';
import { UI_CHROME } from '../../uiAssets';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import type { SelectionStage } from '../platform-entry';
import type { SelectionStage } from '../platform-entry/platformEntryTypes';
import type { RpgAdventureStatistics } from './types';
const RpgEntryCharacterSelectView = lazy(async () => {
const module = await import('../rpg-entry');
const module = await import('../rpg-entry/RpgEntryCharacterSelectView');
return {
default: module.RpgEntryCharacterSelectView,
};
});
const PlatformEntryFlowShell = lazy(async () => {
const module = await import('../platform-entry');
const module = await import('../platform-entry/PlatformEntryFlowShell');
return {
default: module.PlatformEntryFlowShell,
};
});
const RpgRuntimePanelRouter = lazy(async () => {
const module = await import('../rpg-runtime-panels');
const module = await import('../rpg-runtime-panels/RpgRuntimePanelRouter');
return {
default: module.RpgRuntimePanelRouter,
};

View File

@@ -1,4 +1,4 @@
import type { BottomTab } from '../../hooks/rpg-session';
import type { BottomTab } from '../../hooks/rpg-session/rpgSessionTypes';
import type {
BattleRewardUi,
CharacterChatUi,

View File

@@ -7,7 +7,7 @@ import {
} from '../../routing/appPageRoutes';
import type { GameState } from '../../types';
import type { GameCanvasEntitySelection } from '../GameCanvas';
import type { SelectionStage } from '../platform-entry';
import type { SelectionStage } from '../platform-entry/platformEntryTypes';
type OverlayPanel = 'character' | 'inventory' | null;

View File

@@ -2,7 +2,7 @@ import { useEffect } from 'react';
import { DEFAULT_MUSIC_VOLUME } from '../../../packages/shared/src/contracts/runtime';
import { useAuthUi } from '../../components/auth/AuthUiContext';
import type { RpgRuntimeShellProps } from '../../components/rpg-runtime-shell';
import type { RpgRuntimeShellProps } from '../../components/rpg-runtime-shell/types';
import { activateRosterCompanion, benchActiveCompanion } from '../../data/companionRoster';
import { syncGameStatePlayTime } from '../../data/runtimeStats';
import type { HydratedSavedGameSnapshot } from '../../persistence/runtimeSnapshotTypes';

View File

@@ -128,6 +128,19 @@ test('maps running draft_foundation operation to refined generation progress ste
expect(isDraftFoundationOperationRunning(baseOperation)).toBe(true);
});
test('calculates elapsed time from operation startedAt before local fallback', () => {
const progress = buildAgentDraftFoundationGenerationProgress(
{
...baseOperation,
startedAt: '1970-01-01T00:00:01.000Z',
},
4_000,
6_000,
);
expect(progress?.elapsedMs).toBe(5_000);
});
test('maps auto asset phases to refined generation progress steps', () => {
const progress = buildAgentDraftFoundationGenerationProgress(
{

View File

@@ -380,6 +380,18 @@ function parseOperationUpdatedAtMs(
return Number.isFinite(parsedMs) ? parsedMs : null;
}
function parseOperationStartedAtMs(
operation: CustomWorldAgentOperationRecord,
) {
const rawStartedAt = operation.startedAt?.trim();
if (!rawStartedAt) {
return null;
}
const parsedMs = Date.parse(rawStartedAt);
return Number.isFinite(parsedMs) ? parsedMs : null;
}
function resolveAgentDraftFoundationStepIndex(
operation: CustomWorldAgentOperationRecord,
) {
@@ -517,7 +529,7 @@ export function isDraftFoundationOperationRunning(
export function buildAgentDraftFoundationGenerationProgress(
operation: CustomWorldAgentOperationRecord | null | undefined,
startedAtMs: number | null,
fallbackStartedAtMs: number | null,
nowMs = Date.now(),
): CustomWorldGenerationProgress | null {
if (!isDraftFoundationOperation(operation)) {
@@ -526,6 +538,8 @@ export function buildAgentDraftFoundationGenerationProgress(
const activeStepIndex = resolveAgentDraftFoundationStepIndex(operation);
const overallProgress = resolveFailedProgress(operation, activeStepIndex);
// 中文注释:总耗时必须绑定服务端 operation 创建时间,避免刷新或前端重挂载后重新计时。
const startedAtMs = parseOperationStartedAtMs(operation) ?? fallbackStartedAtMs;
const elapsedMs = startedAtMs ? Math.max(0, nowMs - startedAtMs) : 0;
const estimatedRemainingMs = resolveEstimatedRemainingMs(
overallProgress,

View File

@@ -1,5 +1 @@
/**
* 平台入口服务通用封装。
* 先复用既有资料看板读取逻辑,但对 `platform-entry` 暴露通用命名。
*/
export { getRpgProfileDashboard as getPlatformProfileDashboard } from '../rpg-entry';
export { getPlatformProfileDashboard } from './platformProfileClient';

View File

@@ -0,0 +1,5 @@
/**
* 平台首页资料读取入口。
* 直连 RPG profile client避免默认首页首访经过服务桶入口触发额外模块转译。
*/
export { getRpgProfileDashboard as getPlatformProfileDashboard } from '../rpg-entry/rpgProfileClient';

View File

@@ -1,27 +0,0 @@
{
auto_https off
}
:8080 {
root * {$CADDY_SITE_ROOT}
handle /api/* {
reverse_proxy {$CADDY_API_UPSTREAM}
}
@public_assets path /branding/* /character/* /generated-character-drafts/* /generated-characters/* /generated-custom-world-scenes/* /generated-qwen-sprites/* /Icons/* /Pixel* /scene_bg/* /UI/*
handle @public_assets {
root * {$CADDY_PUBLIC_ROOT}
file_server
}
handle /healthz {
respond "ok" 200
}
handle {
encode gzip zstd
try_files {path} /index.html
file_server
}
}

View File

@@ -16,6 +16,16 @@ export default defineConfig(({mode}) => {
'**/public/generated-characters/**',
'**/public/generated-custom-world-scenes/**',
'**/public/generated-qwen-sprites/**',
'**/backend-rewrite-tasklist/**',
'**/docs/**',
'**/jenkins/**',
'**/media/**',
'**/scripts/**',
'**/server-rs/**',
'**/*.test.ts',
'**/*.test.tsx',
'**/*.spec.ts',
'**/*.spec.tsx',
];
const rustServerTarget =
env.RUST_SERVER_TARGET ||