diff --git a/.env.example b/.env.example index 1be72344..1f06973c 100644 --- a/.env.example +++ b/.env.example @@ -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" diff --git a/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md b/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md index e6a7056b..c7d5019a 100644 --- a/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md +++ b/backend-rewrite-tasklist/06_M7_TEST_DEPLOY_CUTOVER.md @@ -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 的完整运行环境,不在无外部服务的本地预检中虚假勾选。 diff --git a/docs/audits/engineering/SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md b/docs/audits/engineering/SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md index 85a89750..ff65dd05 100644 --- a/docs/audits/engineering/SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md +++ b/docs/audits/engineering/SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md @@ -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 专用开发入口。 diff --git a/docs/design/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md b/docs/design/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md index 7e98f8e4..43a510da 100644 --- a/docs/design/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md +++ b/docs/design/COMPANION_FIRST_CONTACT_RELATIONSHIP_AND_PRIVATE_CHAT_DESIGN_2026-04-04.md @@ -240,6 +240,7 @@ function buildNpcFirstContactOptionCatalog( - 不能再用“某人看着你,像是在等你把话接下去”这类第三人称占位旁白充当可见对话历史首句,也不能在聊天 state 里本地硬编码一条替代台词。 - 当玩家在场景中第一次真正撞上角色型 NPC 并进入聊天时,应直接触发一轮由 NPC 主动开口的模型回复;这一轮只生成 NPC 自己的首句与后续可选回应,不得代替玩家补写未说过的话。 - 负好感或敌对关系不应跳过主动开口;如果玩家从 NPC 交互面板点击 `npc_chat`,且该角色尚未完成 `firstMeaningfulContactResolved`,仍要走同一条 NPC 主动开场链路。负好感只影响语气、敌对聊天指令与后续可选功能,不影响“由角色先发言”的首遇行为。 +- 好感度小于 `0` 的角色在聊天终止时不进入 `story_continue_adventure` 收束态。无论是玩家主动退出聊天,还是模型通过敌对聊天指令主动结束聊天,底部选项都固定收束为 `npc_fight` 与 `battle_escape_breakout`:按钮文案分别为“战斗”“逃跑”。点击“战斗”进入 NPC 战斗结算链路;点击“逃跑”执行现有 `battle_escape_breakout` function,完成脱离演出与后续状态更新。 4. 首遇状态下,不允许前两项直接变成: - 深背景追问 diff --git a/docs/experience/AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md b/docs/experience/AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md new file mode 100644 index 00000000..81d75bd2 --- /dev/null +++ b/docs/experience/AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md @@ -0,0 +1,18 @@ +# Agent 空会话草稿可见性修正 2026-04-26 + +用户从创作中心点进 RPG 或大鱼吃小鱼工作台时,后端会立即创建 Agent session,并写入一条助手欢迎消息。但在用户尚未发送任何消息、也没有传入种子文本时,这个 session 只是临时工作区,不应进入“我的创作”草稿列表。 + +本次规则: + +1. 只有存在用户消息、非空 seedText、真实草稿数据或已发布状态时,Agent session 才算作品草稿。 +2. 助手欢迎消息、默认 anchorPack、空 `{}` draftProfile 不算用户创作内容。 +3. 过滤必须落在后端 works 聚合层,前端创作中心只消费结果,不负责隐藏空草稿。 +4. RPG 仍保留已发布 profile 和孤立持久草稿 profile 的展示;未发布且仍有活跃 Agent session 的编译 profile 继续去重。 + +涉及入口: + +- `server-rs/crates/spacetime-module/src/lib.rs` +- `server-rs/crates/spacetime-module/src/custom_world/mod.rs` +- `server-rs/crates/spacetime-module/src/big_fish/session.rs` + +后续如果新增玩法创作 Agent,也必须复用同一判断:创建会话不等于创建草稿,作品列表只展示已经被用户实际开始编辑或已经生成结果的会话。 diff --git a/docs/experience/README.md b/docs/experience/README.md index 0380a42c..95f83d28 100644 --- a/docs/experience/README.md +++ b/docs/experience/README.md @@ -29,3 +29,4 @@ - [RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md](./RPG_DRAFT_IMAGE_PARALLEL_GENERATION_2026-04-24.md):记录 RPG 底稿阶段角色主形象与场景背景图并行生成约束。 - [PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md](./PLATFORM_HOME_BANNER_IMAGE_SIZE_FIX_2026-04-25.md):记录首页 banner 背景图不能进入普通布局流的修复经验。 - [RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md](./RPG_PUBLISH_GALLERY_REFRESH_FIX_2026-04-25.md):记录 RPG 发布后首页 / 分类页公开作品列表刷新链路。 +- [AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md](./AGENT_EMPTY_SESSION_DRAFT_VISIBILITY_2026-04-26.md):记录 Agent 空会话不应进入作品草稿列表的后端判定规则。 diff --git a/docs/technical/FRONTEND_FIRST_LOAD_PERFORMANCE_FIX_2026-04-26.md b/docs/technical/FRONTEND_FIRST_LOAD_PERFORMANCE_FIX_2026-04-26.md new file mode 100644 index 00000000..e2fed867 --- /dev/null +++ b/docs/technical/FRONTEND_FIRST_LOAD_PERFORMANCE_FIX_2026-04-26.md @@ -0,0 +1,56 @@ +# 前端首次加载慢修复记录 + +日期:`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 中所有 `` 和 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 世界后挂载,平台首页不再同步拉取运行态画布链路。 +- 默认 `App` 不再首屏调用 `useRpgRuntimeSession`。平台首页先挂载轻量 `PlatformEntryFlowShell`,用户选择世界、恢复存档或进入 RPG 运行态深链后,才懒加载完整 `RpgRuntimeApp` 和故事/战斗/NPC 交互 hooks。 +- 平台入口 props 移除未使用的 `gameState`,避免轻量首页为了兼容旧签名初始化完整 RPG `GameState`。 +- 平台首页资料服务直连 `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. 默认首页不再同步加载 RPG story / combat / NPC interaction 运行态 hooks;进入自定义世界或恢复存档后再加载完整运行态。 +4. 慢图片、失败图片或生成资源代理慢时,页面主体仍能先显示并保持可操作。 +5. 修改 `docs/`、`server-rs/`、`scripts/` 或测试文件时,不再触发前端页面 reload。 +6. `RouteImageReadyGate` 工具测试覆盖慢图片仍会放行首屏的行为。 +7. 修改中文文件后运行编码检查,确保没有破坏 UTF-8 文本。 diff --git a/docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md b/docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md index b1c8998c..e47ea575 100644 --- a/docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md +++ b/docs/technical/M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md @@ -51,6 +51,19 @@ 1. `content-type`:优先使用 OSS 响应头。 2. `cache-control`:`private, max-age=60`。 3. `x-genarrative-asset-object-key`:回写解析后的 OSS object key,方便调试。 +4. `content-length`:成功读取 OSS 对象后按二进制体长度显式回写,避免开发代理或浏览器把已成功读取的图片误判为空响应。 + +## 3.1 成功对象响应稳定性补充 + +日期:`2026-04-26` + +本次排查发现 `/generated-characters/storynpcs-0/visual/visual-storynpcs-0-aitask_65048e86d04f7/master.png` 在 OSS 签名 URL 下可正常返回 `200 image/png`,但 Rust 同源代理成功读取对象后曾对客户端返回空响应,Vite 开发代理进一步表现为 `500 Internal Server Error`。 + +修复口径: + +1. 成功分支不再依赖隐式 `error_for_status()` 后继续用 builder 拼装响应,而是先判断上游状态,再用明确的 `(status, headers, bytes)` 二进制响应返回。 +2. 非 `2xx` 上游状态统一映射为 `404` 或 `502` JSON 错误,禁止把 OSS 上游异常表现成连接中断。 +3. 成功响应必须保留 `content-type`,并显式回写 `content-length`、`cache-control` 与 `x-genarrative-asset-object-key`。 ## 4. 对象键约定 diff --git a/docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md b/docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md index 837b2fd7..3b4a3c59 100644 --- a/docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md +++ b/docs/technical/M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md @@ -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 对比脚本。 diff --git a/docs/technical/PUZZLE_BIG_FISH_INITIAL_CREATION_PROGRESS_ZERO_FIX_2026-04-26.md b/docs/technical/PUZZLE_BIG_FISH_INITIAL_CREATION_PROGRESS_ZERO_FIX_2026-04-26.md new file mode 100644 index 00000000..9d595594 --- /dev/null +++ b/docs/technical/PUZZLE_BIG_FISH_INITIAL_CREATION_PROGRESS_ZERO_FIX_2026-04-26.md @@ -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. 生成草稿、生成资产、发布等后续阶段的进度值不受本次调整影响。 diff --git a/docs/technical/README.md b/docs/technical/README.md index a3f7cf9b..bf65274e 100644 --- a/docs/technical/README.md +++ b/docs/technical/README.md @@ -7,6 +7,7 @@ - [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):记录开局场景与普通场景复用同一场景展示解析服务,修复列表幕缩略图和详情幕背景预览图片不一致的问题。 +- [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%`、主按钮改为“继续生成草稿”并复用已保存底稿续跑,以及按阶段耗时模型估算预计等待时间的修复口径。 diff --git a/package.json b/package.json index 531e2a9f..d7ef44df 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/scripts/run-caddy-dev.mjs b/scripts/run-caddy-dev.mjs deleted file mode 100644 index f8d78e72..00000000 --- a/scripts/run-caddy-dev.mjs +++ /dev/null @@ -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); -}); diff --git a/server-rs/README.md b/server-rs/README.md index 2da5588f..55fbec94 100644 --- a/server-rs/README.md +++ b/server-rs/README.md @@ -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` 覆盖开关。 后续任务会继续在本目录内按顺序补齐: diff --git a/server-rs/crates/api-server/src/legacy_generated_assets.rs b/server-rs/crates/api-server/src/legacy_generated_assets.rs index 1f910183..1a150c3c 100644 --- a/server-rs/crates/api-server/src/legacy_generated_assets.rs +++ b/server-rs/crates/api-server/src/legacy_generated_assets.rs @@ -1,7 +1,6 @@ use axum::{ - body::Body, extract::{Path, State}, - http::{HeaderName, HeaderValue, StatusCode, header}, + http::{HeaderMap, HeaderName, HeaderValue, StatusCode, header}, response::{IntoResponse, Response}, }; use platform_oss::{LegacyAssetPrefix, OssSignedGetObjectUrlRequest}; @@ -115,44 +114,47 @@ async fn read_legacy_generated_asset( .headers() .get(header::CONTENT_TYPE) .cloned(); - let bytes = upstream_response - .error_for_status() - .map_err(|error| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "aliyun-oss", - "message": format!("读取 OSS 旧 generated 资源失败:{error}"), - })) - })? - .bytes() - .await - .map_err(|error| { - AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ - "provider": "aliyun-oss", - "message": format!("读取 OSS 旧 generated 资源内容失败:{error}"), - })) - })?; - let mut response = Response::builder() - .status(status) - .header(header::CACHE_CONTROL, CACHE_CONTROL_VALUE) - .header( - HeaderName::from_static(ASSET_OBJECT_KEY_HEADER), - HeaderValue::from_str(object_key.as_str()).map_err(|error| { - AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ - "provider": "legacy-generated-assets", - "message": format!("构造资源响应头失败:{error}"), - })) - })?, - ); - if let Some(content_type) = content_type { - response = response.header(header::CONTENT_TYPE, content_type); + if !status.is_success() { + return Err(map_legacy_generated_upstream_status(status, object_key)); } - response.body(Body::from(bytes)).map_err(|error| { - AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR).with_details(json!({ - "provider": "legacy-generated-assets", - "message": format!("构造资源响应失败:{error}"), + let bytes = upstream_response.bytes().await.map_err(|error| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_details(json!({ + "provider": "aliyun-oss", + "message": format!("读取 OSS 旧 generated 资源内容失败:{error}"), })) - }) + })?; + + // 旧 generated 路径会被 /