Prune obsolete docs and update navigation

This commit is contained in:
2026-04-25 15:10:24 +08:00
parent 1c5f9303a2
commit 2ebfd1cf55
341 changed files with 1352 additions and 90709 deletions

View File

@@ -13,15 +13,9 @@ VITE_LLM_PROXY_BASE_URL="/api/llm"
# Optional frontend override for the local custom-world scene image proxy path. # Optional frontend override for the local custom-world scene image proxy path.
VITE_SCENE_IMAGE_PROXY_BASE_URL="/api/custom-world/scene-image" VITE_SCENE_IMAGE_PROXY_BASE_URL="/api/custom-world/scene-image"
# Legacy Node backend address. Do not use for new runtime routes. # Runtime API routes use Rust Axum api-server.
NODE_SERVER_ADDR=":8081"
NODE_SERVER_TARGET="http://127.0.0.1:8081"
# Backend switch for local dev proxy.
# Runtime API routes default to Rust Axum api-server.
GENARRATIVE_BACKEND_STACK="rust"
RUST_SERVER_TARGET="http://127.0.0.1:3100" RUST_SERVER_TARGET="http://127.0.0.1:3100"
# Optional hard override. When set, it wins over GENARRATIVE_BACKEND_STACK/NODE_SERVER_TARGET/RUST_SERVER_TARGET. # Optional hard override. When set, it wins over RUST_SERVER_TARGET.
GENARRATIVE_RUNTIME_SERVER_TARGET="" GENARRATIVE_RUNTIME_SERVER_TARGET=""
# Rust api-server local target used by the Big Fish / Puzzle compatibility gateways # Rust api-server local target used by the Big Fish / Puzzle compatibility gateways
@@ -37,18 +31,14 @@ GENARRATIVE_SPACETIME_DATABASE="genarrative-dev"
GENARRATIVE_SPACETIME_POOL_SIZE="4" GENARRATIVE_SPACETIME_POOL_SIZE="4"
# Local Caddy upstream target used for dist-based testing. # Local Caddy upstream target used for dist-based testing.
CADDY_API_UPSTREAM="http://127.0.0.1:8081" CADDY_API_UPSTREAM="http://127.0.0.1:3100"
# Editor and asset tool APIs. Defaults are enabled outside production and # Editor and asset tool APIs. Defaults are enabled outside production and
# disabled in production unless explicitly enabled. # disabled in production unless explicitly enabled.
EDITOR_API_ENABLED="true" EDITOR_API_ENABLED="true"
ASSETS_API_ENABLED="true" ASSETS_API_ENABLED="true"
# Node backend PostgreSQL connection string. # Rust api-server JWT settings.
# Runtime persistence now uses PostgreSQL as the only formal backend baseline.
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/genarrative"
# Node backend JWT settings.
JWT_SECRET="CHANGE_ME_FOR_PRODUCTION" JWT_SECRET="CHANGE_ME_FOR_PRODUCTION"
# Access token 有效期。 # Access token 有效期。
JWT_EXPIRES_IN="2h" JWT_EXPIRES_IN="2h"

View File

@@ -34,8 +34,6 @@ ALIYUN_SMS_RETURN_VERIFY_CODE="false"
VITE_AUTH_ALLOW_DEV_GUEST="false" VITE_AUTH_ALLOW_DEV_GUEST="false"
DATABASE_URL="postgresql://postgres:postgres@127.0.0.1:5432/genarrative"
# 启用服务端大模型调试日志(记录所有输入输出) # 启用服务端大模型调试日志(记录所有输入输出)
LLM_DEBUG_LOG="true" LLM_DEBUG_LOG="true"
@@ -49,7 +47,6 @@ ALIYUN_OSS_ACCESS_KEY_ID="LTAI5t7aiyw6uDFW4miJvU8f"
ALIYUN_OSS_ACCESS_KEY_SECRET="XblWGE6CO1WLnSBdMRVpL6lut4GSoS" ALIYUN_OSS_ACCESS_KEY_SECRET="XblWGE6CO1WLnSBdMRVpL6lut4GSoS"
# Local Rust backend target for Vite dev proxy. # Local Rust backend target for Vite dev proxy.
GENARRATIVE_BACKEND_STACK="rust"
RUST_SERVER_TARGET="http://127.0.0.1:3100" RUST_SERVER_TARGET="http://127.0.0.1:3100"
GENARRATIVE_API_TARGET="http://127.0.0.1:3100" GENARRATIVE_API_TARGET="http://127.0.0.1:3100"

5
.gitignore vendored
View File

@@ -20,11 +20,6 @@ temp-build-goal-check/
*.py[cod] *.py[cod]
/public/generated-custom-world-scenes /public/generated-custom-world-scenes
temp*build*/ temp*build*/
/server-node/dist/
/server-node/logs/*
!/server-node/logs/.gitkeep
/server-node/data/*
!/server-node/data/.gitkeep
/server-rs/target/ /server-rs/target/
/server-rs/.spacetimedb/ /server-rs/.spacetimedb/
/server-rs/.data/ /server-rs/.data/

View File

@@ -18,6 +18,8 @@
前置条件: 前置条件:
- Node.js - Node.js
- Rust / Cargo
- SpacetimeDB CLI
安装依赖: 安装依赖:
@@ -42,9 +44,8 @@ npm run dev
补充说明: 补充说明:
- `npm run dev`同时启动 Vite 与 Express 后端,适合完整联调。 - `npm run dev` 会启动 SpacetimeDB standalone、Rust `api-server` 与 Vite 前端,适合完整联调。
- 如果没有显式配置 `DATABASE_URL`,且本机 `PostgreSQL` 不可用,开发模式会自动回退到内存版 `pg-mem`,方便先跑通鉴权与存档主链 - 如果只想单独启动前端页面,可使用 `npm run dev:web`,默认代理到本地 Rust `api-server`
- 如果只想单独启动前端页面,可使用 `npm run dev:web`
构建生产包: 构建生产包:

28
bash.exe.stackdump Normal file
View File

@@ -0,0 +1,28 @@
Stack trace:
Frame Function Args
0007FFFFB520 00021005FE8E (000210285F68, 00021026AB6E, 000000000000, 0007FFFFA420) msys-2.0.dll+0x1FE8E
0007FFFFB520 0002100467F9 (000000000000, 000000000000, 000000000000, 0007FFFFB7F8) msys-2.0.dll+0x67F9
0007FFFFB520 000210046832 (000210286019, 0007FFFFB3D8, 000000000000, 000000000000) msys-2.0.dll+0x6832
0007FFFFB520 000210068CF6 (000000000000, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28CF6
0007FFFFB520 000210068E24 (0007FFFFB530, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x28E24
0007FFFFB800 00021006A225 (0007FFFFB530, 000000000000, 000000000000, 000000000000) msys-2.0.dll+0x2A225
End of stack trace
Loaded modules:
000100400000 bash.exe
7FFA3C060000 ntdll.dll
7FFA3B490000 KERNEL32.DLL
7FFA390F0000 KERNELBASE.dll
7FFA3BE50000 USER32.dll
7FFA38E90000 win32u.dll
7FFA3A230000 GDI32.dll
7FFA38D60000 gdi32full.dll
7FFA38EC0000 msvcp_win.dll
7FFA38930000 ucrtbase.dll
000210040000 msys-2.0.dll
7FFA39EB0000 advapi32.dll
7FFA3A180000 msvcrt.dll
7FFA3BCA0000 sechost.dll
7FFA3B5F0000 RPCRT4.dll
7FFA37D70000 CRYPTBASE.DLL
7FFA38B40000 bcryptPrimitives.dll
7FFA3A260000 IMM32.DLL

View File

@@ -1,6 +1,6 @@
# 文档总览 # 文档总览
`docs/` 现在按主题拆成了 6 类`docs/prd/` 保持独立,不参与本次整理改写 `docs/` 现在按主题拆成了 6 类;旧后端路线文档开始聚合和删除,后续实现以 Rust / SpacetimeDB 当前基线为准
## 快速入口 ## 快速入口
@@ -10,15 +10,15 @@
- [技术方案](./technical/README.md):动画、服务端、外部产品形态拆解。 - [技术方案](./technical/README.md):动画、服务端、外部产品形态拆解。
- [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级。 - [规划与优先级](./planning/README.md):当前阶段的迭代排序与落地优先级。
- [参考目录](./reference/README.md):脚本/Function 速查入口。 - [参考目录](./reference/README.md):脚本/Function 速查入口。
- [PRD](./prd/):产品需求与阶段计划,原样保留 - [PRD](./prd):产品需求与阶段计划。
## 推荐阅读顺序 ## 推荐阅读顺序
1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。 1. 先看 [经验沉淀](./experience/README.md),快速建立这个项目的开发共识。
2. 再看 [工程审查总览](./audits/engineering/README.md) 和 [文本审计总览](./audits/text/README.md),了解当前风险。 2. 再看 [工程审查总览](./audits/engineering/README.md) 和 [文本审计总览](./audits/text/README.md),了解当前风险。
3. 需要排期时看 [规划与优先级](./planning/README.md)。 3. 需要排期时看 [规划与优先级](./planning/README.md)。
4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md)。 4. 需要补方案时进入 [系统设计](./design/README.md) / [技术方案](./technical/README.md);涉及后端先看 [当前后端实现基线](./technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)
5. 需要对齐目标边界时再进入 [PRD](./prd/)。 5. 需要对齐目标边界时再进入 [PRD](./prd)。
## 分类规则 ## 分类规则

View File

@@ -7,7 +7,7 @@
- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` - `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md`
- `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` - `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` - `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md` - `docs/audits/engineering/README.md`
- `src/data/stateFunctions.ts` - `src/data/stateFunctions.ts`
- `src/data/npcInteractions.ts` - `src/data/npcInteractions.ts`
- `src/data/treasureInteractions.ts` - `src/data/treasureInteractions.ts`

View File

@@ -9,7 +9,7 @@
- `docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md` - `docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md`
- `docs/reference/FUNCTION_SCRIPT_CATALOG_2026-04-04.md` - `docs/reference/FUNCTION_SCRIPT_CATALOG_2026-04-04.md`
- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` - `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md`
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md` - `docs/audits/engineering/README.md`
本次实际核对了这些实现入口: 本次实际核对了这些实现入口:

View File

@@ -4,7 +4,7 @@
## 系列总览 ## 系列总览
- [engineering/README.md](./engineering/README.md):工程优化审查三轮记录的融合入口。 - [engineering/README.md](./engineering/README.md)当前工程优化审查与历史结论聚合入口。
- [text/README.md](./text/README.md):文本、英文残留、乱码审计系列的融合入口。 - [text/README.md](./text/README.md):文本、英文残留、乱码审计系列的融合入口。
## 专项审计 ## 专项审计

View File

@@ -349,7 +349,7 @@
文档依据: 文档依据:
1. `docs/audits/engineering/README.md` 1. `docs/audits/engineering/README.md`
2. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md` 2. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
3. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md` 3. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
4. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` 4. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`

View File

@@ -1,256 +0,0 @@
# 当前工程优化优先级汇总2026-04-10
## 结论先说
`2026-04-01` 那轮工程审查相比,当前仓库的主问题已经发生了明显迁移:
- 运行时主链拆分已经有进展,`useStoryGeneration.ts` 不再是最高复杂度热点。
- `typecheck`、前后端测试、内容校验、编码校验都已经回到可通过状态。
- 当前真正卡住工程节奏的,已经变成:
- 绿色门禁不可信
- 构建 warning 仍然会直接打断发布门禁
- 自定义世界 / 编辑器 / 资产链路出现了新的巨型模块热点
- 生成产物与旧工具链残留开始反向污染 lint、watch 和本地开发信号
一句话判断:
**现在最该优先做的,不是继续扩功能,而是先把门禁重新拉回可信状态,再拆 editor / custom world / assets 这批新的复杂度中心。**
---
## 2026-04-10 当前校验快照
本次汇总不是只复述旧文档,额外执行了当前仓库校验命令。
| 项目 | 结果 | 说明 |
| --- | --- | --- |
| `npm run check:encoding` | 通过 | 编码基线正常 |
| `npm run typecheck` | 通过 | 当前严格类型门禁可通过 |
| `npm run test` | 通过 | `92` 个测试文件、`228` 个测试通过 |
| `npm run server-node:test:baseline` | 通过 | 观测基线正常 |
| `npm run server-node:test` | 通过 | `72` 个后端测试通过 |
| `npm run check:content` | 通过 | 内容与覆盖校验正常 |
| `npm run lint:eslint` | 失败 | `330` 个 error、`4` 个 warning |
| `npm run build` | 失败 | 构建完成,但因 warning 被 `build-gate` 拦截 |
当前状态说明:
- 仓库不是“完全不可用”,而是已经进入“测试绿,但门禁信号不一致”的阶段。
- 这类状态比纯红线更危险,因为团队会误以为主链已经稳定。
---
## P0先恢复可信的绿色门禁
### P0-1修复 lint 失真,重新建立可信基线
这是当前第一优先级。
#### 证据
- `npm run lint:eslint` 当前失败,报出 `330` 个 error、`4` 个 warning。
- 问题既有真实源码问题,也有明显的门禁污染:
- `src/``server-node/``scripts/` 中存在 import 排序、未使用导入、少量 hook 规则问题。
- `temp-build-goal-check/` 这类生成产物目录也被 ESLint 扫描进来,放大了噪音。
- `.eslintrc.cjs` 当前忽略了 `dist``media` 等目录,但没有忽略 `temp-build-goal-check`
- `vite.config.ts``server.watch.ignored` 已经忽略了 `**/temp*build*/**`,说明当前 watch 口径和 lint 口径并不一致。
#### 影响
- 团队无法快速判断“现在是源码真问题,还是产物目录噪音”。
- lint 失真会直接削弱 review、回归和集成效率。
- 在这种状态下继续加功能,只会让真实错误被更多噪音淹没。
#### 当前建议
1. 先清理或迁出 `temp-build-goal-check/` 这类生成产物目录,至少不要再让它进入 lint 扫描范围。
2. 统一 `watch / lint / build` 对临时目录和生成目录的忽略口径。
3. 再集中清当前源码层 lint 问题,优先处理:
- import 排序
- 未使用导入
- 少量真实规则错误,例如 hook 误用和 `ban-types`
---
### P0-2修复构建 warning恢复可发布构建
这是和 P0-1 同级的阻塞项。
#### 证据
- `npm run build` 当前会被 `scripts/build-gate.mjs` 拦截。
- 当前构建输出里最关键的 warning 有两类:
- `src/services/ai.ts` 虽然尝试走动态加载,但又被 `src/components/CustomWorldEntityEditorModal.tsx` 静态引入,导致拆包失效。
- `AuthenticatedApp-*.js` 达到 `1078.61 kB`,超过当前 `750 kB` 的 chunk warning 门槛。
- 同轮构建里,`index-*.css` 也已经达到 `157.56 kB`,说明不仅 JS 主块重,样式也在继续膨胀。
#### 影响
- 当前不是“构建有一点 warning 可以先带着走”,而是发布门禁已经被 warning 直接打断。
- editor / custom world / asset 工具能力正在把非主链代码重新带回主包路径。
- 后续如果继续叠加这条链路,首屏、缓存和回归都会继续变差。
#### 当前建议
1. 先切断 `CustomWorldEntityEditorModal.tsx -> ../services/ai` 的静态依赖,让 `ai.ts` 真正留在懒加载路径。
2. 把自定义世界编辑器、资产工作台、非首屏工具能力继续从 `AuthenticatedApp` 主块中拆出。
3. 保持 `build warning = 失败` 的策略,不建议通过放宽阈值掩盖问题。
---
## P1拆掉新的复杂度中心
### P1-1优先拆 editor / custom world / assets 新热点
旧的运行时主链热点已经有所缓解,但复杂度并没有消失,而是转移到了新的模块上。
#### 当前大文件热点
前端:
- `src/components/CustomWorldEntityEditorModal.tsx``2778`
- `src/services/ai.ts``2454`
- `src/services/customWorld.ts``2217`
- `src/data/npcInteractions.ts``2103`
- `src/data/characterPresets.ts``1953`
- `src/services/prompt.ts``1725`
后端:
- `server-node/src/modules/assets/characterAssetRoutes.ts``2295`
- `server-node/src/app.test.ts``1527`
- `server-node/src/auth/authService.ts``1243`
- `server-node/src/modules/quest/runtimeQuestModule.ts``1137`
工具链:
- `scripts/dev-server/*.ts`:已于 `2026-04-19` 删除,旧 Vite 本地 API 链路不再保留实现代码
#### 影响
- 复杂度并没有真正被消灭,而是从运行时 story hook 转移到了自定义世界、资产编辑、提示词和数据装配链。
- 这些文件大多同时承载了:
- 领域规则
- API 调用
- 文本拼装
- UI 状态
- 工具流程
- 后续任何一个小改动,都容易牵动整条大链,回归成本会再次上升。
#### 当前建议
1. 前端优先拆 `CustomWorldEntityEditorModal.tsx`,按“世界锚点 / 角色 / 地点 / 资产 / 高级设置”分段。
2. 后端优先拆 `characterAssetRoutes.ts`,把 route、job orchestration、文件发布、模板读取拆开。
3.`src/services/ai.ts``src/services/customWorld.ts` 继续按运行时 / 编辑器 / 资产工具三条职责分层。
---
### P1-2继续收口 editor / assets 工具链边界(旧链路已删除)
这项的重要性正在上升。
#### 证据
- `docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md` 已说明 editor/assets API 已经迁到 `server-node`,方向是对的。
- `scripts/dev-server/*.ts` 旧 Vite 本地 API 实现代码已于 `2026-04-19` 删除,仓库里不再保留并行实现。
- 目录 `temp-build-goal-check/` 当前包含 `15099` 个文件,已经开始干扰 lint 和本地开发信号。
- 相关日志里还出现了大量指向 `temp-build-goal-check` 的页面 reload 与 `ENOENT` 噪音。
#### 影响
- editor/assets 正式入口已经收口到 `server-node`,这部分双链路问题已解除。
- 当前更大的噪音来源已经转移到临时构建目录、检查目录和历史日志残留。
#### 当前建议
1. 保持 `scripts/dev-server/README.md` 作为迁移结果标记,不要恢复旧 Vite `/api/*` 本地插件链。
2. 将临时构建目录、检查目录、导出目录统一移出主工程扫描面。
3. 继续以 `server-node/src/modules/editor/**``server-node/src/modules/assets/**``src/editor/shared/editorApiClient.ts` 作为唯一推荐入口,减少后续回流。
---
## P2继续做架构收口但不必抢在 P0 前面
### P2-1继续压缩前端遗留 AI / 自定义世界实现
这一项仍然值得做,但当前不再是最前面的阻塞。
#### 原因
- `docs/technical/EXPRESS_BACKEND_WORKSTREAM_AUDIT_2026-04-09.md` 显示正式运行时主链已经大幅回收到后端。
- 当前更明显的遗留,已经集中到编辑器、自定义世界工作台和资产工具,而不是正式运行时 story 主链。
#### 当前建议
1. 继续让正式运行时保持“后端为真相源”。
2. 对仍留在前端的大 AI / prompt / custom world 实现,优先做职责收缩,而不是继续在原文件上堆逻辑。
---
### P2-2继续优化自定义世界工作台但以“减负”和“分层”为主
这一项更适合作为 P0、P1 稳住后的下一轮重点。
#### 依据
- `docs/audits/CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md` 已经明确指出:
- 自定义世界入口、澄清、锁定、局部重生成、结果工作台仍是半收口状态。
- 当前最大的前端热点文件也集中在这条链路上,说明它已经不仅是产品问题,也是工程复杂度问题。
#### 当前建议
1. 优先减少“大一统编辑弹窗”的职责,把高杠杆编辑和高级编辑分层。
2. 让自定义世界生成、锁定、局部重生成规则继续向后端收口。
3. 移动端优先,避免长表单和重弹窗继续吞掉维护成本。
---
## 推荐执行顺序
### 第一阶段:先把门禁拉回可信
1. 修 lint 口径失真
2. 清生成产物扫描污染
3. 修 build warning
### 第二阶段:再拆新的复杂度中心
1.`CustomWorldEntityEditorModal.tsx`
2.`characterAssetRoutes.ts`
3. 收缩 `src/services/ai.ts` / `src/services/customWorld.ts`
### 第三阶段:最后收 editor / custom world 架构尾巴
1. 清理旧 Vite 工具链残留
2. 继续把自定义世界和资产工具收回正式后端边界
---
## 当前不建议优先做的事
- 不建议在当前 lint 与 build 仍然是红线时继续横向扩 editor / custom world 功能。
- 不建议通过放宽 chunk warning 阈值来“修复”构建。
- 不建议继续在 `CustomWorldEntityEditorModal.tsx``src/services/ai.ts``characterAssetRoutes.ts` 这类巨型文件中直接堆新逻辑。
---
## 本文依据
文档依据:
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md`
- `docs/technical/EXPRESS_BACKEND_WORKSTREAM_AUDIT_2026-04-09.md`
- `docs/technical/EDITOR_ASSET_API_MIGRATION_2026-04-08.md`
- `docs/audits/CUSTOM_WORLD_CREATOR_TOOL_AUDIT_2026-04-08.md`
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md`
当前仓库校验依据:
- `npm run check:encoding`
- `npm run typecheck`
- `npm run test`
- `npm run server-node:test:baseline`
- `npm run server-node:test`
- `npm run check:content`
- `npm run lint:eslint`
- `npm run build`

View File

@@ -124,9 +124,8 @@
本次审计结合了四类证据: 本次审计结合了四类证据:
1. 文档基线: 1. 文档基线:
- `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md` - `docs/audits/engineering/README.md`
- `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md` - `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
- `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md`
- `scripts/dev-server/README.md` - `scripts/dev-server/README.md`
2. 当前入口核对: 2. 当前入口核对:
- `src/main.tsx` - `src/main.tsx`
@@ -591,10 +590,9 @@
文档依据: 文档依据:
1. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md` 1. `docs/audits/engineering/README.md`
2. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md` 2. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
3. `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md` 3. `scripts/dev-server/README.md`
4. `scripts/dev-server/README.md`
当前仓库扫描依据: 当前仓库扫描依据:

View File

@@ -366,9 +366,8 @@
文档依据: 文档依据:
1. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md` 1. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md`
2. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_PRIORITIES_2026-04-10.md` 2. `docs/audits/engineering/README.md`
3. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md` 3. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
4. `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md`
当前仓库复核依据: 当前仓库复核依据:
@@ -381,4 +380,3 @@
7. `src/services/runtimeItemAiDirector.ts` 7. `src/services/runtimeItemAiDirector.ts`
8. `src/services/apiClient.ts` 8. `src/services/apiClient.ts`
9. 当前依赖图扫描结果与当前大文件体量扫描结果 9. 当前依赖图扫描结果与当前大文件体量扫描结果

View File

@@ -1,278 +0,0 @@
# 工程优化审查报告2026-03-29
## 说明
- 扫描范围:`src/``scripts/``docs/``package.json``vite.config.ts``tsconfig.json`
- 已执行校验:`npm run lint``npm run build``npm run check:content`
- 本报告只从工程角度讨论结构、边界、质量门禁、可维护性与可扩展性
- 按仓库说明,暂不讨论中文乱码本身
## 当前结论
项目当前**可构建、可运行、内容校验可通过**,说明基础功能链路是通的;但从工程视角看,已经出现明显的“单点过重、边界混杂、质量门禁偏弱、编辑器与运行时耦合”问题。继续叠需求会越来越依赖人工记忆和局部经验,回归风险会持续上升。
当前最值得优先处理的不是单个 UI 细节,而是以下四个工程主题:
1. 运行时主链路的职责拆分还不够,核心 hook / 组件已经过载
2. 缺少真正的工程质量门禁,`lint` 目前本质上只是 `tsc`
3. 编辑器、运行时、类后端能力都混在同一个 Vite 配置里
4. 持久化、AI 调用、编辑器保存等基础设施仍然是“分散手写”
## 运行状态快照
- `npm run lint` 通过
- `npm run build` 通过
- `npm run check:content` 通过
- 应用代码下未发现测试文件:`src/``scripts/``docs/` 内没有 `*.test.*` / `*.spec.*`
- 构建产物已出现较大 chunk
- `dist/assets/App-*.js``407 KB`
- `dist/assets/itemCatalog-*.js``414 KB`
- `dist/assets/PresetEditor-*.js``109 KB`
## 代码体征
下列文件已经明显进入“超大模块”区间:
| 文件 | 行数 | 观察 |
| --- | ---: | --- |
| `src/hooks/useStoryGeneration.ts` | 3304 | 同时管理剧情、NPC 交互、交易、送礼、招募、任务、角色聊天、道具/锻造接入 |
| `src/components/PresetEditor.tsx` | 2244 | 多编辑器入口聚合在一个巨型组件中 |
| `src/hooks/useCombatFlow.ts` | 1791 | 同时承担战斗推演、动画时序、逃跑演出、状态落地 |
| `src/components/GameShell.tsx` | 1592 | 入口 UI、选角、世界选择、自定义世界、场景切换、浮层控制全部集中 |
| `src/types.ts` | 663 | 运行时、AI、编辑器、自定义世界、背包、任务类型集中在一个总文件 |
补充信号:
- `src/components/GameShell.tsx` 内有 16 个 `useState`、10 个 `useEffect`、13 个 `useMemo`
- `src/hooks/useStoryGeneration.ts` 虽然只有少量 React state但内部累计 40+ 个函数,已经是“巨型流程控制器”
- `src/hooks/useCombatFlow.ts` 内有大量时间常量、动画常量、`sleep + setGameState` 过程式循环,测试成本很高
## 优先级问题
## P0运行时主链路职责过度集中
证据:
- `src/hooks/useStoryGeneration.ts:868-930` 进入 hook 后立即开始定义交易、送礼、招募、角色聊天等子流程
- `src/hooks/useStoryGeneration.ts:3191-3303` 返回对象同时暴露剧情、任务、NPC UI、角色聊天 UI、背包/锻造 UI
- `src/components/GameShell.tsx:293-360` 组件 props 很多,内部 state 也很多,承担“壳层 + 流程 + 浮层 + 自定义世界生成 + 场景切换”
- `src/hooks/useCombatFlow.ts:559-1787` 将战斗计算和战斗演出揉在同一层里
影响:
- 任何一个新需求都容易同时碰到剧情、UI、战斗、背包、NPC 关系四五条链路
- 代码 review 很难聚焦,改动一处时往往需要脑内跟完整条大流程
- 单元测试难写,因为逻辑不是纯函数,而是大量闭包 + 过程式状态推进
- 长期会形成“只有熟悉历史上下文的人才能安全修改”的隐性门槛
建议:
-`useStoryGeneration` 拆为“剧情推进”“NPC 交互”“角色聊天”“任务结算”“模态框控制”几个子域
-`useCombatFlow` 拆成“纯战斗结算引擎”和“战斗播放适配层”
-`GameShell` 回到壳层职责,只负责路由态、页面态、模态挂载与 props 编排
- 以“领域职责”拆分,而不是按“文件太长了随便切一刀”拆分
## P0缺少真正的工程质量门禁
证据:
- `package.json:11``lint` 实际只有 `tsc --noEmit`
- `package.json` 中没有 `test``format``lint:fix` 等基础脚本
- 根目录未发现 `.eslintrc*``.prettierrc*``.editorconfig`
- 代码目录下没有测试文件
影响:
- 当前项目的“能过 lint”只代表类型没炸不代表风格一致、依赖正确、Hooks 规则正确、死代码已清理
- 大型 hook / 大型组件的重构几乎没有自动回归保护
- 运行时行为、编辑器行为、AI fallback 行为主要依赖人工回归
建议:
- 补齐 ESLint、Prettier、EditorConfig至少覆盖 React Hooks、import、unused code、复杂度基线
- 引入 Vitest先覆盖纯数据层与纯规则层
-`useCombatFlow``stateFunctions``npcInteractions``questFlow` 增加单元测试
- 为“开局 -> 选世界 -> 选角色 -> 进入剧情 -> 战斗 -> 存档恢复”补最小 E2E smoke
- CI 中至少串联:类型检查 + 单测 + build + 内容校验
## P1编辑器、运行时、类后端能力全部耦合在 Vite 配置里
证据:
- `vite.config.ts:151-203` 在 Vite 插件里实现了 LLM 代理
- `vite.config.ts:206-269` 在 Vite 插件里实现了通用 JSON 文件读写 API
- `vite.config.ts:253` 直接写回 `src/data/*.json`
- `vite.config.ts:265-266``vite.config.ts:400-401``preview` 阶段也挂了这些接口
- `vite.config.ts:425-434` 启动时默认把这些“编辑器后端能力”全部注册进去
影响:
- 本地编辑器能力与运行时能力没有清晰边界
- `preview` 环境仍可写源码文件,发布边界不清晰
- 未来如果要做独立部署、多人协作、远程编辑、权限控制,会非常难迁移
- Vite 配置同时扮演构建配置、代理层、文件服务层、编辑器后端,职责失衡
建议:
- 将编辑器读写 API 从 `vite.config.ts` 抽到独立的本地工具服务或独立脚本
- 至少区分 `dev-only write api``preview/prod read-only api`
- 对编辑器保存接口建立统一客户端 SDK避免组件直接散落 `fetch('/api/...')`
- LLM 代理也建议独立成 `server/``scripts/dev-server/`,不要继续长在构建配置里
## P1持久化策略分散且直接序列化大状态对象
证据:
- `src/hooks/useGamePersistence.ts:152-167` 会在状态变化时自动把完整快照写入 `localStorage`
- `src/hooks/useGamePersistence.ts:157-163` 快照包含 `gameState + bottomTab + currentStory`
- `src/hooks/useGamePersistence.ts:68-116` 恢复逻辑已经开始承担大量 schema 纠偏职责
- `src/data/customWorldLibrary.ts:1-282` 自定义世界库单独维护一套 `localStorage` 读写与 normalize
- `src/hooks/useGameSettings.ts` 也单独维护一套本地设置持久化
影响:
- 状态结构一旦继续膨胀,快照写入频率和反序列化成本都会增加
- schema 迁移会越来越依赖手工 normalize 补丁
- 不同持久化入口各写一套 parser / normalizer风格和鲁棒性难统一
- 当前保存的是“运行中大对象”,而不是“稳定领域快照”,长期会放大兼容成本
建议:
- 建立统一的 persistence 层,集中管理 key、version、migration、节流、序列化策略
-`GameState` 做“可持久化切片”和“运行时临时切片”分层
- 自动保存增加节流/去抖,避免每次状态波动都全量落盘
- 如果继续扩展角色聊天、自定义世界、编辑器草稿,建议评估 IndexedDB 替代 `localStorage`
## P1运行时与编辑器仍在同一个前端入口体系中包体继续膨胀
证据:
- `src/main.tsx:21-34` 通过 `window.location.pathname` 手写分发页面
- `src/main.tsx:60` 只有“游戏”和“PresetEditor”两个大入口
- `PresetEditor``ItemCatalogEditor``StateFunctionEditor` 都属于重型模块
- 构建产物已经出现 `App``407 KB``itemCatalog``414 KB` 的 chunk
影响:
- 游戏端与编辑器端的演进节奏被绑定在一个 SPA 入口上
- 编辑器相关数据和静态资源容易继续抬高构建体积
- 未来增加更多编辑器页、更多世界模板、更多资源目录后,冷启动成本会更明显
建议:
- 将编辑器拆成独立入口,至少做成独立 route module而不是单个 `PresetEditor`
- 继续下钻按 tab 做懒加载,尤其是 `items/functions/npcs`
- 将静态大数据、资源目录索引、编辑器专用预览逻辑做更细的 chunk 拆分
- 如果项目后续会长期保留编辑器,建议直接分成 game app / editor app 两个 entry
## P2编辑器基础设施重复实现较多
证据:
- `src/components/PresetEditor.tsx:111-181` 自己实现 `cloneValue``saveJsonObject`
- `src/components/StateFunctionEditor.tsx:113-130` 再次实现 `cloneValue``SectionCard`
- `src/components/ItemCatalogEditor.tsx:94` 再次实现保存请求
- `src/hooks/useInventoryFlow.ts:8``src/hooks/useEquipmentFlow.ts:10``src/hooks/useForgeFlow.ts:12``src/hooks/useTreasureFlow.ts:10` 重复声明 `CommitGeneratedState`
影响:
- 修改保存行为、错误处理、深拷贝策略时需要多处同步
- 编辑器 UI 风格与交互行为容易逐步漂移
- 公共契约没有收拢到共享层,维护成本会逐步抬高
建议:
-`editor/shared/` 层,集中放保存 SDK、表单字段、卡片容器、克隆工具、错误处理
- 抽通用的 `CommitGeneratedState` 类型定义
- 将编辑器请求和覆盖保存逻辑统一走一个 client
## P2类型系统已经出现“总文件过载”
证据:
- `src/types.ts` 共 663 行
- `src/types.ts:1-260` 同时包含世界、动画、技能、对话、自定义世界、物品等类型
- `src/types.ts:536-663` 又继续承接剧情、聊天、任务、`GameState`、AI 响应
影响:
- 任一领域类型变化都会增加总文件冲突概率
- 新人理解类型边界成本高
- 编辑器类型、运行时类型、AI 传输类型被放在一起,不利于演化
建议:
- 按领域拆分:`types/combat.ts``types/story.ts``types/item.ts``types/customWorld.ts``types/persistence.ts`
- `GameState` 相关类型与 editor override 类型分开
- AI request/response contract 单独收口,避免继续堆进总类型文件
## P2AI 客户端层过厚,且重复了多套请求与解析逻辑
证据:
- `src/services/ai.ts` 共 1153 行
- `src/services/ai.ts:540-605``608-678``745-790` 分别手写了 JSON completion、纯文本 completion、流式 completion
- `src/services/ai.ts:680-697` 手写了多段 JSON 解析兜底
- `src/services/ai.ts:76-78``591-594``662-666` 主要依赖 `console.*` 打日志
影响:
- LLM 行为扩展时容易继续复制请求模板、错误处理、超时逻辑
- 错误分类不够稳定,观测主要停留在 console 层
- prompt、transport、fallback、parse 被放在一起,后续测试和替换模型都不够轻
建议:
-`llmClient`,统一 transport、timeout、stream、error taxonomy
-`llmParsers`,将 JSON parse / plain text parse / suggestion parse 独立
- 为关键 prompt 输出建立 fixture 测试,至少覆盖 fallback 与异常响应
- 如果后续要接多个模型,尽早把 provider 层和 prompt 层解耦
## P2手写路由与死代码开始累积
证据:
- `src/main.tsx:21-34` 采用手写 `pathname.startsWith(...)`
- `src/components/GameShell.tsx:1511` 存在 `false && showTeamModal`
影响:
- 路由能力不具备可扩展性,也不利于后续加 404、重定向、权限判断、嵌套路由
- 死代码继续堆积后,会误导维护者对真实入口和真实 UI 状态的判断
建议:
- 引入正式路由层,哪怕只做轻量路由也比手写分发更清晰
- 清理已经废弃的 UI 分支和不可达逻辑
- 对“临时下线的功能”改为 feature flag 或明确注释,不要用 `false &&`
## 建议落地顺序
### 第一阶段:先补工程底座
- 增加 ESLint / Prettier / EditorConfig
- 增加 `test` 脚本与 Vitest
- 把 CI 最小闭环搭起来类型检查、单测、build、内容校验
### 第二阶段:先拆边界,再拆大文件
- 先把 Vite 中的编辑器写文件接口、LLM 代理抽走
- 再把 `GameShell``useStoryGeneration``useCombatFlow` 按职责拆域
- 拆分时优先保持外部接口稳定,避免一次性全仓大改
### 第三阶段:收敛基础设施
- 统一 persistence 层
- 统一 editor shared 层
- 统一 AI client 层
- 拆分 `types.ts`
### 第四阶段:降低发布成本
- 将 editor 与 game 做更明确的入口拆分
- 优化 chunk 边界
- 评估是否把编辑器做成独立 app
## 一句话结论
这个仓库当前最需要优化的不是“再补几个功能”,而是**把已经验证有效的玩法与工具链,从“靠大文件和经验串起来”升级为“靠清晰边界、统一基础设施和自动化门禁支撑起来”**。只要这一步不做,后续每次加内容、加编辑器能力、加 AI 流程,工程成本都会持续上升。

View File

@@ -1,290 +0,0 @@
# 工程优化审查报告2026-03-30
## 审查范围
- 扫描范围:`src/``scripts/``docs/``.github/``package.json``tsconfig.json``vite.config.ts`
- 实际执行:`npm run lint``npm run test``npm run build``npm run check:content`
- 说明:按仓库要求,本报告不讨论中文乱码问题,只讨论工程结构、边界、质量门禁、可维护性和后续扩展成本
## 先说结论
这轮代码库相较 `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md` 已经有明显进展,项目不再是“所有能力都糊在一个入口文件里”的状态了,但整体仍然处于“重构过渡期”。
已经落地的积极变化:
- 入口路由已经从手写 `pathname` 分发,收敛到 `src/main.tsx` + `src/routing/appRoutes.tsx`
- 持久化能力已经抽到 `src/persistence/`
- 编辑器公共能力已经出现 `src/editor/shared/`
- `CI + ESLint + Prettier + Vitest` 已经接入
- 本地 API 插件已经从 `vite.config.ts` 抽走,落到 `scripts/dev-server/localApiPlugins.ts`
- `preview` 环境里的 JSON 写入接口已经改成只读,这一点比上轮更安全
但当前仍然存在 5 个值得优先处理的工程问题:
1. 运行时主链仍然过于集中,`story/combat` 的真实边界还没有彻底拆开
2. `src/services/ai.ts` 仍处于迁移中间态,存在重复实现和旧逻辑残留
3. 编辑器主入口仍是大型聚合组件,迁移残留没有清干净
4. 质量门禁已经有框架但还不够“硬”warning 和测试覆盖缺口仍然明显
5. 运行时渲染层和构建体积仍偏重,重 UI 模块还没拆到合适粒度
## 当前运行状态
- `npm run test` 通过6 个测试文件共 18 个测试全部通过
- `npm run build` 通过
- `npm run check:content` 通过
- `npm run lint` 通过,但仍有 76 条 warning
当前构建产物里仍然存在较重 chunk
- `dist/assets/GameCanvas-*.js``346.58 kB`
- `dist/assets/App-*.js``326.89 kB`
- `dist/assets/index-*.js``197.80 kB`
- `dist/assets/index-*.css``117.37 kB`
## P0运行时主链仍然过于集中Story/Combat 边界还没有拆透
### 现状
虽然 `App.tsx` 已经明显瘦身,`GameShell` 也比之前更像壳层,但真正决定游戏推进的主逻辑仍然高度集中在两个大 hook 里:
- `src/hooks/useStoryGeneration.ts:824`
- `src/hooks/useCombatFlow.ts:382`
### 证据
`useStoryGeneration` 仍然同时编排了多个本应继续拆开的子领域:
- `src/hooks/useStoryGeneration.ts:852` 接入 `useCharacterChatFlow`
- `src/hooks/useStoryGeneration.ts:1583` 接入 `useTreasureFlow`
- `src/hooks/useStoryGeneration.ts:1588` 接入 `useInventoryFlow`
- `src/hooks/useStoryGeneration.ts:1593` 接入 `useEquipmentFlow`
- `src/hooks/useStoryGeneration.ts:1597` 接入 `useForgeFlow`
- 文件总长仍有约 `3240`
- 结尾返回对象同时暴露剧情推进、地图旅行、NPC 交易/送礼/招募、角色聊天、背包与锻造 UI 能力,典型位置在 `src/hooks/useStoryGeneration.ts:3171-3219`
`useCombatFlow` 也不是纯计算层,它仍然同时承担:
- 战斗前后状态推导
- 动画播放与时间推进
- `setGameState` 驱动的可视化编排
- 逃跑流程与 story 响应同步
关键位置:
- `src/hooks/useCombatFlow.ts:382` `useCombatFlow`
- `src/hooks/useCombatFlow.ts:1195` `playEscapeSequenceWithStorySync`
### 影响
- 任何一个“剧情选项新增”都很容易同时碰到 battle、npc、quest、inventory、chat 五条链路
- review 成本高,回归范围判断依赖人脑上下文
- 单测很难往 hook 级别补,因为副作用、异步节奏和 UI 状态混在一起
- 后续想继续做 camp、custom world、更多 companion 玩法时,改动会继续集中到这两个入口
### 建议
-`useStoryGeneration` 继续下钻成“剧情推进 orchestrator + 领域 action service”
- `useStoryGeneration` 自己只保留编排,不再直接维护 trade/gift/recruit/chat/inventory/forge 的全部细节
- `useCombatFlow` 继续向“纯战斗结算”和“播放适配层”分离
- 先稳定公开接口,再做内部拆分,避免一次性大改
## P1AI 服务迁移只完成了一半,`src/services/ai.ts` 仍然存在双轨实现
### 现状
仓库已经新增了:
- `src/services/llmClient.ts`
- `src/services/llmParsers.ts`
- `src/services/aiFallbacks.ts`
- `src/services/aiTypes.ts`
这说明拆层方向是对的。但 `src/services/ai.ts` 还没有真正变成“纯 orchestration 层”,里面仍然保留着一整套旧 transport / parse / fallback 逻辑。
### 证据
- `src/services/ai.ts:64-66` 已经开始导入 `llmClient`
- `src/services/ai.ts:89-95` 仍然保留本地 `resolveTimeoutMs` 和超时常量
- `src/services/ai.ts:647` 仍然保留 `_requestPlainTextCompletion`
- `src/services/ai.ts:719` 仍然保留 `_parseJsonResponseText`
- `src/services/ai.ts:739` 仍然保留 `_parseLineListContent`
- `src/services/ai.ts:784` 仍然保留 `_streamPlainTextCompletion`
- `src/services/ai.ts:885-904` 仍然保留一批旧的 `_buildOffline...` helper
与之对应,新的实现已经在下面这些文件里存在:
- `src/services/llmClient.ts`
- `src/services/llmParsers.ts`
### 影响
- 同一类能力现在有两套真相源后续改错误分类、超时策略、SSE 行为时容易漏改
- 新同学读代码时很难判断应该继续改 `ai.ts`,还是应该去改 `llmClient.ts`
- 迁移残留会拉高维护成本,也会让测试边界变得模糊
### 建议
-`src/services/ai.ts` 收敛成“业务 prompt 编排 + fallback 选择”层
- 彻底删掉未再需要的 `_requestPlainTextCompletion``_streamPlainTextCompletion``_parse*` 等旧 helper
- 所有 transport / timeout / connectivity error / SSE 解析都只保留在 `llmClient.ts``llmParsers.ts`
- 迁移完成后,给 `ai.ts` 增加一组 orchestration 级测试,防止 fallback 分支回归
## P1编辑器主入口仍然太重而且过渡态残留还在
### 现状
编辑器公共能力已经开始沉淀到 `src/editor/shared/`,这是好事;但主编辑器入口仍然比较重,且部分文件还保留着迁移过程里的死代码和注释块。
### 证据
`PresetEditor` 仍然是一个大型聚合组件:
- `src/components/PresetEditor.tsx:402` `CharacterPresetPanel`
- `src/components/PresetEditor.tsx:1174` `SceneNpcPresetPanel`
- `src/components/PresetEditor.tsx:1547` `ScenePresetPanel`
- `src/components/PresetEditor.tsx:1852` `MonsterPresetPanel`
- `src/components/PresetEditor.tsx:2218` `PresetEditor`
- 文件总长仍有约 `2279`
同时,文件里还留着明显的过渡态残留:
- `src/components/PresetEditor.tsx:227` 仍然保留未使用的 `_SectionCard`
- `src/components/NpcVisualEditor.tsx:684` 保留 `if (false)` 的旧保存路径
- `src/components/NpcVisualEditor.tsx:685` 明确写着 “Deprecated inline save path kept only until the shared client migration is cleaned up.”
- `src/components/NpcVisualEditor.tsx:724` 还有第二处 `if (false)` 残留
### 影响
- 编辑器后续继续扩展时,容易重新长回“大一统文件”
- 过渡代码会误导维护者,以为旧保存链路仍然有效
- 公共层虽然建起来了,但如果不清理旧代码,长期会形成“共享层 + 本地特例”并存
### 建议
- 以“一个 tab 一个容器”的方式,把 `PresetEditor` 再拆一层
- 清理 `NpcVisualEditor` 里的废弃代码块,不要再保留 `if (false)` 分支
- 对编辑器共享层设定明确规则保存请求、克隆、Section 容器、错误提示都必须走 shared
- 对编辑器做一次“小型迁移完成清扫”,优先删掉已经废弃但还挂在文件里的旧路径
## P1质量门禁已经接上但还不够硬
### 现状
基础设施已经比上轮完整很多,但当前门禁仍然偏“有检查,不够严格”。
### 证据
当前 lint 结果:
- 本次 `npm run lint` 实际输出 `76` 条 warning虽然命令返回成功
脚本和规则层面的原因也很明确:
- `package.json:12``lint` 仍然是 `eslint . ... && tsc --noEmit`,没有 `--max-warnings 0`
- `package.json:11``lint:guardrails` 虽然加了 `--max-warnings 0`,但它只覆盖一组显式 allowlist 文件
- `package.json:18``check` 会先跑 `lint:guardrails`,再跑宽松版 `lint`
- `.eslintrc.cjs:45-61` 里大量规则仍然是 `warn`
- `.github/workflows/ci.yml:28-40` 已经把 `lint / guardrails / test / build / check:content` 都接到 CI但 warning 仍能稳定进主干
测试覆盖也还是偏薄:
- `src/` 当前共有 `126` 个文件
- 其中测试文件只有 `6`
- 现有测试主要覆盖 `routing``persistence``jsonClient``llmParsers``battlePlan`
- 关键主链如 `useStoryGeneration``useCombatFlow` 播放层、`GameShell` 集成链路、编辑器保存流程仍然没有直接测试
### 影响
- 代码库会持续积累“已知 warning但先不处理”的债务
- 工程信号会逐渐失真lint 通过不代表代码足够干净
- 大 hook 和大组件的重构依然主要依赖人工回归
### 建议
- 先把 warning 收敛到一个可控范围,再把全仓 `lint` 切成 `--max-warnings 0`
- `lint:guardrails` 不要长期靠 allowlist应该逐步扩大到全仓
- 优先补三类测试:
- `useStoryGeneration` 的状态推进和 modal 决策
- `useCombatFlow` 播放层的关键分支
- 编辑器保存链路和覆盖数据回写
## P2运行时渲染层仍然偏重chunk 也还没有拆到理想粒度
### 现状
入口已经有了 route lazy load模态框也做了一部分懒加载但核心运行时渲染层仍然比较重。
### 证据
- `src/components/AdventurePanel.tsx:470` 导出主组件,文件总长约 `1538`
- `src/components/GameCanvas.tsx:472` 导出主组件,文件总长约 `1131`
- `src/components/GameCanvas.tsx:768` 仍然存在 `false && companions.map(...)` 的死分支
- 本次构建里 `GameCanvas``App` 仍然是最大 chunk 之一
### 影响
- 运行时页面的首屏与热区模块仍然偏重
- 渲染逻辑、场景动画逻辑、实体选中逻辑继续堆在同一层review 和测试都偏吃力
- 清理死分支前,维护者对“哪些渲染路径是真实生效的”判断成本更高
### 建议
- `GameCanvas` 继续拆成 scene layer、entity layer、effect layer、overlay layer
- `AdventurePanel` 继续下沉 quest/stats/settings/reward 子面板
- 清理 `false &&` 与未使用辅助组件,避免假分支继续留在主路径文件中
- 结合真实 chunk 数据做一次 route 内部分包,而不是只靠入口级 lazy
## P2TypeScript 安全基线仍然偏宽松
### 现状
当前类型拆分方向是好的,`src/types.ts` 已经退化成 barrel真实类型开始向 `src/types/` 下沉。但 TypeScript 编译配置还比较保守,类型系统还没有真正变成强约束。
### 证据
- `tsconfig.json:12` `skipLibCheck: true`
- `tsconfig.json:16` `allowJs: true`
- 当前没有启用 `strict`
- 当前没有启用 `noUncheckedIndexedAccess`
### 影响
- 对大对象和字典访问的保护仍然偏弱
- 新模块迁移到更细类型后,收益会被宽松编译选项部分抵消
- “代码能过类型检查”并不等于边界足够安全
### 建议
- 不建议一次性全仓开严格模式
- 可以先从 `src/services/``src/persistence/``src/hooks/combat/` 这些相对纯的目录启更严格约束
- 至少先评估开启 `noUncheckedIndexedAccess` 和减少 `allowJs` 的必要性
## 建议的落地顺序
### 第一阶段:先把过渡态清干净
- 清理 `ai.ts` 的旧 transport / parser / fallback 实现
- 清理 `NpcVisualEditor``GameCanvas``PresetEditor` 等文件里的 `if (false)`、未使用 helper、废弃注释块
- 把 lint warning 数量先打下来
### 第二阶段:拆主链,不再让大 hook 继续膨胀
- 继续拆 `useStoryGeneration`
- 继续拆 `useCombatFlow`
- 优先把“领域动作”和“播放/展示编排”分开
### 第三阶段:补门禁
- 给主链补单测和少量集成 smoke
- 让全仓 lint 朝 `--max-warnings 0` 收敛
- 把 warning 从“长期存在”变成“短周期清零”
### 第四阶段:优化运行时体积
- 细化 `GameCanvas``AdventurePanel` 的模块边界
- 按实际交互热区做 chunk 继续拆分
- 用真实构建产物持续追踪是否降重
## 一句话结论
这轮仓库已经从“完全依赖大文件硬扛”进步到“基础设施开始成形”,但当前最需要做的已经不是继续加功能,而是把这轮重构收尾做完整:继续拆主链、删掉迁移残留、把 lint/test 门禁变硬、再顺手压缩运行时大模块。只要这一步补上,后续加剧情、加编辑器能力、加自定义世界都会轻很多。

View File

@@ -1,200 +0,0 @@
# 工程优化审查报告2026-04-01
## 审查范围
- 扫描范围:`src/``scripts/``docs/``.github/``package.json``tsconfig*.json``vite.config.ts``vitest.config.ts`
- 审查方式:阅读当前工作区代码结构,抽查核心运行时、编辑器、服务层与开发脚本,并执行工程命令验证现状
- 当前快照说明:仓库存在大量未提交改动,本报告基于当前工作区状态,不假定这些改动都已经合入主分支
- 说明:按仓库要求,不把中文乱码本身当成本次审查重点;只讨论工程结构、门禁、可维护性、可测试性和扩展成本
## 已执行检查
- `npm run lint:eslint`
结果:失败。`src/components/ItemCatalogEditor.tsx:167` 存在未使用的 `isSearchPending``startTransition`
- `npm run typecheck`
结果:通过
- `npm run test`
结果:通过,默认套件实际执行 10 个测试文件、28 个测试
- `npm run build`
结果:通过,但 `src/services/customWorldPresentation.ts:163-169` 出现 duplicate key 警告
- `npm run check:content`
结果:通过
## 当前结论
这轮代码库已经明显比前几版更有工程骨架了,至少有这些积极变化:
- `src/main.tsx` + `src/routing/appRoutes.tsx` 已经承担了入口路由分发
- `src/App.tsx` 已经比过去瘦很多,主流程开始交给 hook 和壳组件
- `src/components/PresetEditor.tsx` 已经成为较薄的 lazy shell而不是继续堆成巨型入口
- `src/editor/shared/jsonClient.ts``src/persistence/``src/hooks/combat/``src/hooks/story/` 这些目录说明仓库已经开始做分层
- CI、Vitest、ESLint、内容校验脚本都已经接上不再是完全裸奔状态
但从工程角度看,当前最值得优先优化的,不是继续加功能,而是把“半完成的工程化”补齐。核心问题集中在 6 个方面。
## P0质量门禁和真实风险点仍然脱节
### 现状
仓库已经引入了 lint、typecheck、test、build 和 content checks但关键热区并没有真正纳入统一门禁。
### 证据
- `.eslintrc.cjs:47-63``ignorePatterns` 直接跳过了多个高复杂度核心文件:
`src/components/AdventurePanel.tsx``src/components/NpcVisualEditor.tsx``src/components/preset-editor/PresetEditorPanels.tsx``src/hooks/useStoryGeneration.ts``src/services/customWorldPresentation.ts`
- `tsconfig.typecheck-guardrails.json:6-15` 只对非常有限的一小组文件开启严格类型检查,远没有覆盖主运行时链路
- `vitest.config.ts:8-10``customWorldPresentation` 映射到 stub`vitest.config.ts:20` 还排除了真实存在的 `src/services/ai.test.ts`
- 当前 `src/` 下共有 161 个文件,测试文件共有 11 个,但默认套件只执行其中 10 个
- `npm run build` 已经能暴露 `src/services/customWorldPresentation.ts:163-169` 的 duplicate key 警告,但这块文件同时被 ESLint ignore、被 Vitest stub 掉,说明真实风险没有被完整看见
### 影响
- 工程信号不一致:`test` 绿、`build` 过,不代表关键模块真的健康
- 复杂模块越是难测,越容易被长期豁免,最后演变成“最关键的地方最不受控”
- 后续重构会缺乏可靠的回归保护review 只能更多依赖人工记忆
### 建议
- 先缩小 `.eslintrc.cjs` 的 ignore 范围,优先把 `useStoryGeneration.ts``customWorldPresentation.ts``PresetEditorPanels.tsx` 拉回 lint
-`src/services/ai.test.ts` 重新纳入默认测试套件,除非有明确且短期的阻塞原因
- 不要长期依赖 `tsconfig.typecheck-guardrails.json` 的 allowlist至少把 `src/hooks/``src/services/``src/components/game-shell/` 逐步纳入 strict 范围
- 对 build warning 建立明确策略:要么在 CI 中失败,要么把 warning 收敛到零
## P0当前工作区不在真正的绿色基线
### 现状
当前代码不是“纯优化空间”问题,而是已经存在直接可见的门禁破口。
### 证据
- `package.json:11-15``lint:eslint``typecheck` 定义成正式脚本,说明它们本来就属于项目基线
- 实际执行 `npm run lint:eslint` 时,`src/components/ItemCatalogEditor.tsx:167` 报出未使用变量错误
- `src/components/ItemCatalogEditor.tsx:167` 引入了 `useTransition()` 返回值,但当前组件没有消费它
- `npm run build` 虽然成功,但 `src/services/customWorldPresentation.ts:163-169` 仍然有重复 object key 警告
### 影响
- 团队会越来越难区分“可接受的技术债”和“已经破坏基线的问题”
- 继续叠加功能会把问题扩散到更多文件,后面补起来成本更高
### 建议
- 先恢复工作区绿色基线,再继续推进大功能
- 把“lint 零错误、build 零 warning”作为下一轮工程整理的硬目标
## P1运行时主链路仍然被少数超级模块吸住
### 现状
入口已经变薄,但主复杂度仍集中在少数大文件里,尤其是故事推进、战斗同步和界面编排三层。
### 证据
- `src/hooks/useStoryGeneration.ts` 当前约 2210 行
- `src/hooks/useStoryGeneration.ts:694` 导出主 hook`src/hooks/useStoryGeneration.ts:1416` 接入 `useTreasureFlow`,后面还继续承接 NPC 互动、库存、打字机、AI、历史推进和故事回写
- `src/hooks/useCombatFlow.ts:134` 是主战斗 hook`src/hooks/useCombatFlow.ts:796-832` 仍然负责逃跑流程与 story sync 的耦合
- `src/components/GameShell.tsx` 当前约 791 行,`src/components/GameShell.tsx:260-269` 管理一组本地 UI 状态,`src/components/GameShell.tsx:482` 继续处理场景切换时的选择编排
- 构建产物里 `dist/assets/App-*.js` 约 389 kB`dist/assets/index-*.js` 约 198 kB说明主运行时 chunk 仍然偏重
### 影响
- 任何一个功能变化都容易跨 story、combat、transition、panel 几条链一起改
- hook 单测越来越难写,因为副作用、异步和 UI 编排仍然混在一起
- App 主 chunk 偏重,会继续拖累首屏和回归速度
### 建议
- 继续把 `useStoryGeneration` 收敛成 orchestration 层,把 treasure、NPC、inventory、chat、typewriter、AI 回写拆成更纯的领域 action
-`useCombatFlow` 更明确地区分“战斗结算”和“播放同步”
-`GameShell` 进一步下沉为 scene transition、selection flow、overlay panel 三类 view-model
## P1编辑器共享层只迁移了一半重复基础设施还在
### 现状
编辑器入口已经做了 shell 化,但真正的复杂度仍然堆在大型面板组件里,而且共享层没有吃干净。
### 证据
- `src/components/PresetEditor.tsx:41` 的入口已经很薄,说明方向是对的
-`src/components/preset-editor/PresetEditorPanels.tsx` 仍然约 2163 行
- `src/components/preset-editor/PresetEditorPanels.tsx:55` 仍然自带 `cloneValue`
- `src/components/preset-editor/PresetEditorPanels.tsx:117` 仍然自带 `saveJsonObject`
- `src/components/preset-editor/PresetEditorPanels.tsx:189` 仍然自带 `SectionCard`
- 与之对应,`src/editor/shared/jsonClient.ts:29-40` 已经提供了共享版 `fetchJson` / `saveJsonObject`
- `src/components/preset-editor/PresetEditorPanels.tsx:364``:1128``:1500``:1806` 仍然把四个大型 panel 放在同一个文件里
### 影响
- 编辑器的保存、错误处理、基础 UI 容器会继续多处复制,后续很难统一行为
- 迁移看起来开始了,但没有真正收尾,维护者仍然需要在“大文件 + 共享层”之间来回切换
### 建议
- 继续把 `PresetEditorPanels.tsx` 拆成按 tab 或按领域分文件
- 统一复用 `src/editor/shared/` 下的保存客户端、基础容器、表单片段
- 对编辑器做一次“小型迁移收尾”,目标是消灭重复的基础 helper
## P1本地开发 API 层与构建工具耦合过深
### 现状
本地 API 插件已经把很多临时逻辑吸收进项目内部,这是好事;但它现在承担的职责太多,且全部挂在 Vite 插件层。
### 证据
- `vite.config.ts:7-18` 直接把 `createLocalApiPlugins(__dirname, env)` 注入到 Vite config
- `scripts/dev-server/localApiPlugins.ts` 当前约 394 行
- `scripts/dev-server/localApiPlugins.ts:150` 定义 LLM proxy 插件
- `scripts/dev-server/localApiPlugins.ts:216` 定义通用 JSON 文件编辑插件
- `scripts/dev-server/localApiPlugins.ts:265` 直接把编辑器保存结果写回 `src/data/*.json`
- `scripts/dev-server/localApiPlugins.ts:429` 再统一把所有插件拼到一起
### 影响
- dev server、preview server、编辑器持久化和 LLM 代理被绑在一个文件里,测试与替换成本都偏高
- 随着编辑器继续扩张,这个文件会继续演化成“隐形后端”
- 生产与开发环境的边界容易模糊,问题排查也更依赖熟悉 Vite 插件机制的人
### 建议
- 至少先按职责把 `localApiPlugins.ts` 拆成 `llm-proxy``json-editor-api``asset-catalog` 三块
- 下一阶段可以考虑把编辑器 API 抽成独立本地服务层,而不是继续塞在 Vite 插件里
- 给 JSON 写入接口补 schema 校验,而不只是“是 object 就写入”
## P2构建体积仍有继续优化空间
### 现状
路由 lazy load 和部分 modal lazy load 已经做了,但主游戏运行时包仍然偏大。
### 证据
- `dist/assets/App-*.js` 约 389 kB
- `dist/assets/index-*.js` 约 198 kB
- `dist/assets/index-*.css` 约 117 kB
- `src/components/GameShell.tsx``src/hooks/useStoryGeneration.ts``src/services/prompt.ts` 仍然是较大的主链路文件
### 影响
- 新人本地启动、构建和回归阅读成本仍然偏高
- 主运行时模块越重,越不利于后续继续做场景扩展和编辑器共存
### 建议
- 优先沿着“运行时 orchestration 拆分”去减主 chunk而不是单纯追求更多 lazy import
-`prompt`、自定义世界、编辑器预览等非首屏关键代码继续做边界拆分
- 每轮重构后用真实构建产物复测,而不是只凭代码体感判断
## 建议的落地顺序
1. 先恢复绿色基线:修掉 `ItemCatalogEditor` lint 错误,处理 `customWorldPresentation` 的 duplicate key warning
2. 再补齐门禁:缩小 ESLint ignore、把 `ai.test.ts` 拉回默认测试、扩大 strict typecheck 覆盖
3. 然后拆主链:优先处理 `useStoryGeneration``useCombatFlow``GameShell`
4. 再做编辑器迁移收尾:拆 `PresetEditorPanels.tsx`,统一共享层
5. 最后处理 dev API 分层和 bundle 体积
## 一句话结论
这个仓库已经从“功能堆叠期”进入“工程收尾期”了。当前最值得做的不是再加一层玩法,而是把门禁补齐、把超级模块拆开、把半迁移状态收尾;只要这一步做稳,后续继续扩展剧情、编辑器和自定义世界的成本都会明显下降。

View File

@@ -40,7 +40,7 @@
### 2.1 文档依据 ### 2.1 文档依据
1. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` 1. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
2. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md` 2. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
3. `docs/technical/RUNTIME_STORY_BACKEND_BOUNDARY_MIGRATION_2026-04-19.md` 3. `docs/technical/RUNTIME_STORY_BACKEND_BOUNDARY_MIGRATION_2026-04-19.md`
4. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md` 4. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
5. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md` 5. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`

View File

@@ -1,36 +1,31 @@
# 工程优化审查总览 # 工程优化审查总览
这一组是同主题的连续审查记录,建议不要把它们当作三份彼此独立的文档来看 这一组只保留仍能指导当前 Rust / SpacetimeDB 主线的工程审查入口。早期连续扫描的有效结论已经合并到本 README 的“融合结论”,不再保留逐日旧稿
## 当前推荐入口 ## 当前推荐入口
1. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md) 1. [SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md](./SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md)
这一版是旧 Node 后端冻结、第一批物理删除与后续批次边界记录,明确当前工程只保留 Rust / SpacetimeDB 主线入口。
2. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_F_2026-04-21.md)
这一版是第六批落地记录,聚焦删除无入口 `questDirector`、旧观察文案 helper、一次性硬编码同步脚本并补齐后端运行时 function catalog 契约覆盖。 这一版是第六批落地记录,聚焦删除无入口 `questDirector`、旧观察文案 helper、一次性硬编码同步脚本并补齐后端运行时 function catalog 契约覆盖。
2. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md) 3. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_E_2026-04-21.md)
这一版是第五批落地记录,聚焦旧命名 re-export、空路由骨架、旧发布服务、前端 prompt 镜像与无入口编辑器壳层的物理删除。 这一版是第五批落地记录,聚焦旧命名 re-export、空路由骨架、旧发布服务、前端 prompt 镜像与无入口编辑器壳层的物理删除。
3. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md) 4. [FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md](./FRONTEND_LOGIC_BACKEND_MIGRATION_AUDIT_2026-04-21.md)
这一版是本轮前端越界逻辑专项审计,专门汇总当前仍应继续迁到 Express 后端的运行时、鉴权、生成编排与本地真相残留。 这一版是本轮前端越界逻辑专项审计,专门汇总当前仍应继续迁到 `server-rs` 的运行时、鉴权、生成编排与本地真相残留。
4. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md) 5. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_D_2026-04-21.md)
这一版是第四批落地记录,聚焦未接入业务的数据生成产物、测试专用 stub 与对应配置残留出清。 这一版是第四批落地记录,聚焦未接入业务的数据生成产物、测试专用 stub 与对应配置残留出清。
5. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md) 6. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_C_2026-04-21.md)
这一版是第三批落地记录,聚焦鉴权真相收口,先移除前端保存自动登录用户名/密码的本地真相,并明确运行时快照前置写入为什么当前还不能硬砍。 这一版是第三批落地记录,聚焦鉴权真相收口,先移除前端保存自动登录用户名/密码的本地真相,并明确运行时快照前置写入为什么当前还不能硬砍。
6. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md) 7. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_B_2026-04-21.md)
这一版是第二批落地记录,聚焦旧主流程壳层、旧 bootstrap 和旧 inventory / forge / equipment flow Hook 的正式出清。 这一版是第二批落地记录,聚焦旧主流程壳层、旧 bootstrap 和旧 inventory / forge / equipment flow Hook 的正式出清。
7. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md) 8. [ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md](./ENGINEERING_DEAD_CODE_CLEANUP_BATCH_A_2026-04-21.md)
这一版是第一批落地记录聚焦高置信度小型孤岛、prompt 壳子、stub 和无入口 modal 的首轮清理。 这一版是第一批落地记录聚焦高置信度小型孤岛、prompt 壳子、stub 和无入口 modal 的首轮清理。
8. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md) 9. [CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md](./CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md)
这一版是面向当前仓库状态的优化点盘点,适合直接拿来排优先级和拆执行批次。 这一版是面向当前仓库状态的优化点盘点,适合直接拿来排优先级和拆执行批次。
9. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md) 10. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md)
这一版是对 `2026-04-19` 基线的当前仓库复核,明确哪些问题已经处理、哪些表述需要纠正、热点又迁移到了哪里。 这一版是对 `2026-04-19` 基线的当前仓库复核,明确哪些问题已经处理、哪些表述需要纠正、热点又迁移到了哪里。
10. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md) 11. [ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md](./ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md)
这一版保留原始问题快照和执行回填,适合回看“为什么会有这轮清理与边界收口”。 这一版保留原始问题快照和执行回填,适合回看“为什么会有这轮清理与边界收口”。
11. [ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md)
这一版最适合作为当前工程基线,重点从“是否真正绿色”“门禁有没有覆盖真实风险”来判断仓库状态。
12. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md)
适合回看运行时主链路、Story/Combat 边界、分层过渡期问题。
13. [ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](./ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md)
适合看第一轮系统性工程扫描,了解最早的问题基线。
## 融合结论 ## 融合结论
- 最新专项审计已经把“前端哪些逻辑还该后移到后端”收敛到 6 类:运行时快照、本地 token、本地浏览历史、NPC 委托换单、quest/runtime item 混合编排、浏览器 AI orchestration。 - 最新专项审计已经把“前端哪些逻辑还该后移到后端”收敛到 6 类:运行时快照、本地 token、本地浏览历史、NPC 委托换单、quest/runtime item 混合编排、浏览器 AI orchestration。
@@ -43,11 +38,9 @@
- 当前仓库已经完成“旧 dev 插件链路删除、根目录噪音清理、`server-node -> src/**` 反向依赖切断”这批第一阶段任务。 - 当前仓库已经完成“旧 dev 插件链路删除、根目录噪音清理、`server-node -> src/**` 反向依赖切断”这批第一阶段任务。
- 当前如果想直接判断“今天先优化什么”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md` - 当前如果想直接判断“今天先优化什么”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
- 当前的新重点已经进一步收敛到三类:未接线孤岛模块、前端残留的运行时/鉴权真相、热点向 prompt/runtime profile/平台入口壳层迁移。 - 当前的新重点已经进一步收敛到三类:未接线孤岛模块、前端残留的运行时/鉴权真相、热点向 prompt/runtime profile/平台入口壳层迁移。
- 三轮结论是一致收敛的:问题不在“有没有开始工程化”,而在“工程化是否真正覆盖了最危险的主链路” - 早期三轮工程扫描的结论已经聚合为一条长期规则:工程化不能只看目录和拆分动作,必须覆盖真实主链、质量门禁、绿色基线、关键模块豁免和 build warning
- 最新一轮已经把关注点集中到质量门禁、真实绿色基线、关键模块豁免和 build warning 上。
- `2026-04-19` 这一轮把问题压实到了四类:仓库噪音、旧 dev 入口残留、前端越界运行时逻辑、巨型热点文件。 - `2026-04-19` 这一轮把问题压实到了四类:仓库噪音、旧 dev 入口残留、前端越界运行时逻辑、巨型热点文件。
- `2026-04-20` 这一轮进一步确认:前两类已经阶段性完成,当前真正剩下的是边界尾巴和新热点迁移。 - `2026-04-20` 这一轮进一步确认:前两类已经阶段性完成,当前真正剩下的是边界尾巴和新热点迁移。
- 如果只是为了判断现在先做什么,直接从 `2026-04-01` 开始即可。
- 如果是要看当前清理和边界收口的最新状态,优先看 `ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md` - 如果是要看当前清理和边界收口的最新状态,优先看 `ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
- 如果是要看“当前可执行的优化点清单”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md` - 如果是要看“当前可执行的优化点清单”,优先看 `CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
- 如果是要做长期重构方案,再按 `2026-03-29 -> 2026-03-30 -> 2026-04-01 -> 2026-04-19 -> 2026-04-20` 的顺序回看演进 - 如果是要做长期重构方案, `2026-04-19``2026-04-20` 与当前 dead-code batch 记录开始即可

View File

@@ -12,11 +12,11 @@
2. 禁止新增从前端、Rust 后端、脚本或配置主动调用 `server-node/` 的逻辑。 2. 禁止新增从前端、Rust 后端、脚本或配置主动调用 `server-node/` 的逻辑。
3. 禁止在 `server-node/` 内继续新增业务能力;后续能力必须落到 `server-rs/` 对应 crate。 3. 禁止在 `server-node/` 内继续新增业务能力;后续能力必须落到 `server-rs/` 对应 crate。
4. 历史文档、审计文档、迁移基线中允许保留 `server-node/` 作为旧系统来源说明,但不得把它描述成当前推荐实现。 4. 历史文档、审计文档、迁移基线中允许保留 `server-node/` 作为旧系统来源说明,但不得把它描述成当前推荐实现。
5. 删除 `server-node/` 前,必须先完成提示词资产与提示词相关工作流的最终迁移确认 5. 第一批物理删除后,提示词资产与提示词相关工作流继续按迁移核对项追踪,不再恢复旧工程目录
## 3. 删除前阻断 ## 3. 删除前迁移核对
以下资产仍需要在删除目录前逐项确认迁移或废弃 以下资产曾作为删除前核对项。第一批物理删除后,旧实现不再从工作区直接读取;如需继续核对能力缺口,只允许通过历史提交、迁移文档或 `server-rs/` 已迁移实现追溯
1. `server-node/src/prompts/customWorldEntityPrompts.ts`:自定义世界实体生成 prompt。 1. `server-node/src/prompts/customWorldEntityPrompts.ts`:自定义世界实体生成 prompt。
2. `server-node/src/prompts/customWorldSceneNpcPrompts.ts`:自定义世界场景 NPC prompt。 2. `server-node/src/prompts/customWorldSceneNpcPrompts.ts`:自定义世界场景 NPC prompt。
@@ -27,17 +27,17 @@
## 4. 工程防线 ## 4. 工程防线
1. 根目录 `package.json` 中的 `server-node:*` 脚本统一改为冻结失败入口。 1. 第一批物理删除后,根目录 `package.json` 不再保留 `server-node:*``dev:node``check:server-node-freeze` 等旧入口。
2. 新增 `npm run check:server-node-freeze`,用于阻止新增 `server-node` 引用 2. Vite、Caddy 与本地开发脚本默认只指向 Rust `api-server`,不再保留 Node/Rust 后端切换开关
3. 新增 `scripts/server-node-frozen.mjs`,任何旧 `server-node:*` 入口被误执行时都会直接失败并提示迁移到 `server-rs/` 3. 历史文档允许保留旧 `server-node` 字样,但新增工程入口、脚本、依赖、运行说明不得再指向旧 Node 后端
4. 新增 `scripts/server-node-freeze-baseline.json`,只允许冻结前已经存在的引用继续作为迁移基线存在 4. 若后续需要恢复旧能力,只能迁移到 `server-rs/` 对应 crate 或 Axum facade不恢复 `server-node/` 工程目录
## 5. 后续处理顺序 ## 5. 后续处理顺序
1. 优先迁移或废弃提示词资产与 prompt 工作流。 1. 继续核对提示词资产与 prompt 工作流是否已完整落到 Rust 主线
2. 确认前端不再通过任何路径调用 Node 后端能力 2. 继续把前端残留业务编排迁入 `server-rs/`
3. 删除旧脚本、旧 smoke、旧 manifest 与 `server-node/` 目录 3. 清理技术索引中容易误导当前入口的 Node / Express 文案
4. 删除冻结基线检查中对历史引用的豁免 4. 保留历史审计材料,但不得把旧 Node 后端描述为当前推荐实现
## 6. 已确认迁移项 ## 6. 已确认迁移项
@@ -50,3 +50,46 @@
3. 使用位置:`generate_draft_foundation_act_backgrounds(...)` 收集 `sceneChapterBlueprints[].acts[]` 后,先构造幕背景图专用提示词,再调用 `generate_custom_world_scene_image_for_profile(...)` 3. 使用位置:`generate_draft_foundation_act_backgrounds(...)` 收集 `sceneChapterBlueprints[].acts[]` 后,先构造幕背景图专用提示词,再调用 `generate_custom_world_scene_image_for_profile(...)`
4. 保留语义:世界名、场景名、幕标题、幕摘要、幕目标、过渡钩子、主角色、辅助角色、世界气质、背景描述,以及“只生成环境背景,不出现角色立绘、站位 UI、对白框、按钮或文字”的约束。 4. 保留语义:世界名、场景名、幕标题、幕摘要、幕目标、过渡钩子、主角色、辅助角色、世界气质、背景描述,以及“只生成环境背景,不出现角色立绘、站位 UI、对白框、按钮或文字”的约束。
5. 迁移边界:`server-node/` 仅作为历史来源说明,不再参与运行;后续调整统一修改 Rust 主源。 5. 迁移边界:`server-node/` 仅作为历史来源说明,不再参与运行;后续调整统一修改 Rust 主源。
## 7. 第一批安全删除记录2026-04-25
本批次开始把冻结隔离升级为物理删除。执行依据是项目后端主线已固定为 `server-rs/` 的 Rust + SpacetimeDB 多 crate 方案,旧 `server-node/` 不再作为可运行、可扩展、可引用的工程目录保留。
### 7.1 删除范围
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 双栈切换口。
5. 清理 `.gitignore` 中只服务 `server-node/` 的忽略规则,并同步 `README.md``.env.example``server-rs/README.md``scripts/dev-server/README.md`
### 7.2 暂不处理范围
1. 历史 PRD、审计、迁移基线中的 `server-node` 文案暂时保留为历史记录,不在第一批中大规模改写。
2. `backend-rewrite-tasklist/` 中以旧 Node 后端为对照的迁移材料暂时保留,作为后续核对 Rust 主线能力缺口的历史审计输入。
3. `src/services/ai.ts``src/prompts/customWorldPrompts.ts` 的前端残留编排不属于本批 Node 后端删除范围;后续继续按“前端只负责表现,业务逻辑进入 `server-rs/`”单独收口。
### 7.3 后续批次建议
1. 技术文档索引中的 Node / Express 后端条目只保留为历史资料,不再作为当前入口或推荐方案。
2. 后续如继续整理历史文档,只把仍描述 `Express / PostgreSQL` 为当前目标架构的文字修正为“历史阶段口径”。
3. 继续把前端残留业务逻辑迁入 `server-rs`;涉及 SpacetimeDB 的设计、实现、脚本和绑定继续显式使用相关 skill。
### 7.4 本轮安全核对结果
2026-04-25 本轮开始分批删除时,已确认第一批工程入口层面满足以下条件:
1. 工作区根目录下已不存在 `server-node/` 物理目录。
2. `scripts/` 下已不存在旧 Node 后端专用运行、冻结、smoke、API 对比脚本。
3. 根目录 `package.json` 不再包含 `server-node:*``dev:node``m7:api-compare``check:server-node-freeze` 入口。
4. `package.json``package-lock.json` 不再包含 `express``@types/express``pg``postgres` 依赖包。
5. `README.md``scripts/dev-server/README.md``server-rs/README.md``vite.config.ts``scripts/*.mjs``src/``packages/``server-rs/` 未发现仍主动启动或调用 `server-node` 的当前工程入口。
### 7.5 第二批删除边界
第二批不再删除可运行工程代码,而是清理“容易误导当前实现口径”的历史文档索引:
1. 只修正文档中仍把 `server-node`、Express 或 PostgreSQL 描述为当前推荐后端的句子。
2. 保留审计、PRD、迁移基线中作为历史事实、旧实现来源、能力对照的 `server-node` 引用。
3. 不大规模重写包含中文剧情、需求、审计结论的历史文档,避免把真实历史上下文抹平。
4. 若发现某个历史文档仍指导新开发继续写 Node 后端,先把该文档改为“历史阶段口径”,再继续工程处理。

View File

@@ -1,228 +0,0 @@
# 编辑器 UI / 游戏 UI / 预设内容英文与乱码审计
更新时间:`2026-03-25`
## 范围与方法
- 范围只覆盖当前源码里会直接进入编辑器 UI、游戏运行时 UI、预设内容预览的文案。
- 本轮直接按 `utf-8` 读取 `src/components``src/data` 复核,不把旧审计文档当最终事实来源。
- 不统计 `import`、类型名、变量名、接口字段名、资源路径等纯开发层英文。
## 结论摘要
- 当前目标范围内,确认到 **1 处直接写进源码的中文乱码**
- `src/components/PresetEditor.tsx:72``鐗╁搧`
- 当前更大的问题已经不是“大片中文乱码”,而是 **编辑器与部分游戏界面还残留成组英文 UI 文案**
- 预设内容层面的英文主要集中在 **原始枚举值 / 构筑字段**,例如 `common / rare / legendary``neutral``buildProfile.role``idle / move / attack / die``steady / burst / mobility / finisher / projectile`。这些值本身在数据层可以保留英文,但当前有一部分被界面直接原样显示出来了。
## 编辑器 UI
### 英文残留
- `src/components/PresetEditor.tsx:84-89`
- 编辑器标签仍是 `Characters / NPCs / Scenes / Monsters / Items / Functions`
- `src/components/PresetEditor.tsx:97-104`
- 预设编辑里仍直接使用 `idle / move / attack / die``steady / burst / mobility / finisher / projectile`
- `src/components/StateFunctionEditor.tsx:333-367`
- 预览实体与效果摘要仍是英文:
- `Preview NPC`
- `Fallback NPC preview for ...`
- `Preview Treasure`
- `Treasure preview for ...`
- `Damage x / Incoming x / Heal + / Mana + / Cooldown + / Turn x`
- `src/components/StateFunctionEditor.tsx:496-607`
- 预览阶段与提示说明仍有整段英文:
- `Player Turn Preview`
- `Escape Preview`
- `Travel Result Preview`
- `Explore Preview`
- `Call-Out Preview`
- `Idle Behavior Preview`
- `Predicted skill: ...`
- `Monster counter template uses ...`
- `Battle behaviors are driven by skill weights ...`
- `Escape behaviors always use the chase flow ...`
- `src/components/StateFunctionEditor.tsx:855-939`
- 预览面板头部与信息卡仍是英文:
- `Option`
- `Mode`
- `Replay Preview`
- `Preview Playing`
- `Preview Ready`
- `Live Player`
- `Live Scene`
- `No scene`
- `Resolved Plan`
- `Option kind`
- `Target scene`
- `Cooldowns`
- `Battle Snapshot`
- `Animation`
- `Delivery`
- `Damage`
- `Predicted kill`
- `Target survives`
- `Snapshot based on live playback`
- `src/components/ItemCatalogEditor.tsx:25`
- 稀有度选项仍是 `common / uncommon / rare / epic / legendary`
- `src/components/ItemCatalogEditor.tsx:486-560`
- 物品预览区仍直接显示英文键和值:
- `rarity`
- `value`
- `usable`
- `yes / no`
- `equip`
- `world`
- `neutral`
- `HP / MP / Damage / Guard`
- `HP Restore / MP Restore / CD Reduce`
- `Build / 套装`
- `Role / Set / Piece`
- `none / standalone`
- `src/components/NpcVisualEditor.tsx`
- 目前大部分文案已中文化,但 `NPC` 缩写仍在标题、字段、保存提示中大量保留,属于低优先级统一项,不是乱码问题。
### 确认的中文乱码
| 文件 | 位置 | 当前文本 | 判断 |
| --- | --- | --- | --- |
| `src/components/PresetEditor.tsx` | `72` | `鐗╁搧` | 明确乱码,语义应为“物品” |
### 编辑器 UI 小结
- **最高优先级乱码修复点**`PresetEditor.tsx:72`
- **最高优先级英文清理点**`StateFunctionEditor.tsx``ItemCatalogEditor.tsx`
## 游戏各界面 UI
### 英文残留
- `src/components/GameShell.tsx:414`
- 标题副标仍是 `TAVERNREALMS`
- `src/components/GameShell.tsx:1083-1094`
- 团队弹窗里仍保留 `TavernRealms`
- `src/components/CharacterChatModal.tsx:52,73,76`
- `CHARACTER CHAT`
- `HP`
- `MP`
- `src/components/CharacterDetailModal.tsx:24-40`
- 属性与技能风格映射仍为英文:
- `Strength / Agility / Intelligence / Spirit`
- `Burst / Steady / Mobility / Finisher / Projectile`
- `Female / Male / Unknown`
- `src/components/CharacterDetailModal.tsx:149,186,194-271`
- 详情弹窗区块标题与标签仍有整段英文:
- `INITIAL COMPANION`
- `Close character details`
- `Profile`
- `Candidate`
- `Gender`
- `Stats`
- `Max HP / Max MP`
- `Journey`
- `Reason / Goal`
- `Skills / Loadout / Pack / Backstory / Personality`
- `src/components/InventoryPanel.tsx:39-50`
- 稀有度标签仍是 `Legendary / Epic / Rare / Uncommon / Common`
- `src/components/InventoryPanel.tsx:179-196,215`
- 物品详情仍是英文:
- `Quantity`
- `Owner`
- `Usable`
- `Yes / No`
- `Equipable`
- `Value`
- `Type`
- `Tags`
- `no-tags`
- `src/components/AdventureEntityModal.tsx:206-217`
- 自动生成的物品描述仍是英文整句:
- `helps restore HP`
- `supports MP recovery`
- `fits offensive loadouts`
- `supports defensive gearing`
- `works as a rare trinket-grade pickup`
- `can be saved for crafting or trading`
- `${item.name} can be kept for trading, gifting, or future build planning.`
- `${item.name} is a ${item.category} item that ...`
- `src/components/AdventureEntityModal.tsx:226-235`
- 物品属性摘要仍是英文:
- `HP`
- `MP`
- `Damage`
- `Guard x...`
- `src/components/AdventureEntityModal.tsx:1271-1332`
- NPC 物品详情弹窗仍有一整块英文:
- `ITEM DETAIL`
- `NPC inventory`
- `Quantity`
- `Value`
- `Equip Slot`
- `Not equippable`
- `Usable item`
- `Story, trade, or gift resource`
- `Type`
- `Rarity`
- `Tags`
- `No tags`
- `src/components/CompanionCampModal.tsx:152,176-177,213,216`
- `Active`
- `HP`
- `MP`
- `待命 roster`
- `Reserve`
- `src/components/NpcModals.tsx:507`
- 招募替换提示里仍混入 `roster`
### 当前未确认到的乱码
- 本轮没有在游戏运行时 UI 组件里复核到新的、直接写死在源码中的中文乱码。
- 当前游戏 UI 的主要问题已经转为“英文标签 / 英文句子未汉化”,不是大面积中文乱码。
## 预设内容
### 直接会透到 UI 的英文源
- `src/data/itemDesign.ts:52-72`
- 材质主题里直接保存了英文原始值:
- `worldAffinity: "neutral"`
- `role: "fieldcraft" / "breaker" ...`
- `rarity: "common"`
- `tags: ["scout", "craft"]`
- `src/data/itemCatalog.ts:260-270`
- `designed.rarity / designed.worldAffinity / designed.buildProfile` 会原样流入物品目录数据
- `src/components/ItemCatalogEditor.tsx:486-560`
- 上述原始字段当前会在编辑器预览里被直接显示,因此形成了可见英文泄漏
- `src/data/questFlow.ts:24-30`
- 任务奖励物品稀有度仍是 `rare / uncommon`
- `src/data/npcInteractions.ts:71-82,102-103,166`
- 稀有度与标签推断仍使用 `common / uncommon / rare / epic / legendary`
- 物品标签仍使用 `weapon / armor`
- `src/components/PresetEditor.tsx:97-104`
- 预设编辑直接使用 `idle / move / attack / die`
- 技能风格直接使用 `steady / burst / mobility / finisher / projectile`
- `src/components/StateFunctionEditor.tsx:85-98`
- 行为编辑直接使用 `battle / idle`
- 朝向直接使用 `left / right`
- 怪物动画直接使用 `idle / move / attack`
- 风格直接使用 `steady / burst / mobility / finisher / projectile`
### 当前未确认到的乱码
- 本轮没有在 `src/data/*.ts` 的预设正文里复核到新的、直接写死的中文乱码。
- 当前预设内容层面的问题,主要是“英文字段值没有在显示层做 label 映射”,不是正文汉字被写坏。
## 建议处理顺序
1. 先修 `src/components/PresetEditor.tsx:72``鐗╁搧`
2. 再集中处理 `src/components/StateFunctionEditor.tsx` 的整组英文预览与提示文案。
3. 然后处理 `src/components/ItemCatalogEditor.tsx` 的物品预览英文键名与英文值。
4. 之后清理游戏运行时最明显的英文块:
- `src/components/CharacterDetailModal.tsx`
- `src/components/InventoryPanel.tsx`
- `src/components/AdventureEntityModal.tsx`
- `src/components/CompanionCampModal.tsx`
- `src/components/CharacterChatModal.tsx`
- `src/components/GameShell.tsx`
5. 最后为预设源字段补统一显示映射,把 `common / rare / legendary / neutral / idle / left / right ...` 全部收口到统一词典。

View File

@@ -1,91 +0,0 @@
# 游戏 UI / 预设 / 编辑器 UI 英文与乱码复查
日期:`2026-03-29`
## 结论摘要
- 当前分支里,**确认存在源码级中文乱码的文件只有 1 个**`src/components/CustomWorldEntityEditorModal.tsx`
- 历史上已经出现过的两处高风险乱码,本次**未复现**
- `src/components/GameShell.tsx` 角色选择页返回按钮
- `src/data/npcInteractions.ts` 切磋敌对动作文案
- 目前更主要的问题已经从“大面积乱码”转成了三类:
- 运行时 UI 里的英文缩写或英文句子
- 编辑器 UI 里的英文术语、原始枚举值和英文缩写
- 预设数据中的英文名称、分类、标签、世界倾向、构筑角色等原始值直接透到 UI
## 复查口径
- 只统计会直接进入游戏 UI、编辑器 UI、预设预览或运行时详情的文本。
- 不把纯内部实现名算进问题范围,比如接口路径、变量名、导入路径、素材文件夹名。
- 终端输出已切换到 UTF-8 后复查,避免把“控制台显示乱码”误判成“源码真实乱码”。
## 一、已确认的源码级乱码
### 1. 编辑器 UI
| 文件 | 行号 | 当前文本 | 说明 |
| --- | --- | --- | --- |
| `src/components/CustomWorldEntityEditorModal.tsx` | `360`, `381` | `褰撳墠浼氱洿鎺...``棰勮 #...` | 场景预设选择弹窗的说明文案和预设编号标签已写坏 |
| `src/components/CustomWorldEntityEditorModal.tsx` | `551`, `559`, `569`, `572`, `578`, `581`, `584`, `587` | `褰撳墠澶栬妯℃澘``褰㈣薄妯℃澘``鍚嶇О``绉板彿 / 韬唤``鑳屾櫙``鎬ф牸``鎴樻枟椋庢牸``鏍囩` | 可扮演角色编辑表单的标题与字段标签存在真实乱码 |
| `src/components/CustomWorldEntityEditorModal.tsx` | `663`, `666`, `669`, `672` | `鍚嶇О``韬唤 / 鑱岃兘``鎻忚堪``鍔ㄦ満` | 普通 NPC 编辑表单字段标签存在真实乱码,即“创建自定义世界 -> NPC 编辑页”这一段 |
| `src/components/CustomWorldEntityEditorModal.tsx` | `721`, `732`, `736`, `739` | `鍦烘櫙``棰勮鍥句腑鐨勫垏纾...``鍚嶇О``鎻忚堪` | 场景编辑器默认回退文案、说明文案和字段标签存在真实乱码 |
| `src/components/CustomWorldEntityEditorModal.tsx` | `771`, `791` | ``瑙掕壊-${...}``、`['绾跨储', '浜掑姩']` | 默认新建数据本身带乱码,会继续流入编辑器与结果页 |
## 二、游戏 UI 中的英文残留
| 文件 | 行号 | 当前文本/值 | 说明 |
| --- | --- | --- | --- |
| `src/components/AdventurePanel.tsx` | `179-187` | `restores HP during an adventure run`、`fits offensive loadouts` 等整句英文 | 奖励物品自动描述仍是英文句子,会直接进入运行时奖励详情 |
| `src/components/AdventureEntityModal.tsx` | `815-816`, `988`, `1124-1126`, `1497` | `HP`、`MP` | 玩家、怪物、NPC 状态条与效果预览仍使用英文缩写 |
| `src/components/AdventureEntityModal.tsx` | `1071`, `1109`, `1426` | `NPC 信息`、`敌对NPC` / `NPC`、`NPC 背包` | NPC 相关标题和标签仍是中英混排 |
| `src/components/CompanionCampModal.tsx` | `176-177`, `232-233`, `254` | `HP`、`MP`、`NPC` | 同行编队卡片和空态提示仍保留英文缩写 |
| `src/components/NpcModals.tsx` | `251`, `355`, `407` | `NPC 商品列表`、`NPC 商品`、`HP` / `MP` | 商店弹窗标题和物品效果预览仍是中英混排 |
| `src/components/CharacterDetailModal.tsx` | `117` | `数量 x{item.quantity}` | 数量前缀里仍保留英文乘号写法 |
## 三、编辑器 UI 中的英文残留
| 文件 | 行号 | 当前文本/值 | 说明 |
| --- | --- | --- | --- |
| `src/components/ItemCatalogEditor.tsx` | `601`, `617-623` | `neutral / wuxia / xianxia`、`tag` 原始值 | 世界倾向和标签直接显示原始英文值 |
| `src/components/ItemCatalogEditor.tsx` | `637-651`, `689` | `HP`、`MP`、`Build Buff`、`CD` | 属性预览、使用效果和背包卡片预览仍有英文缩写/术语 |
| `src/components/ItemCatalogEditor.tsx` | `662-665` | `buildProfile.role`、`synergy` 原始值 | 构筑角色和协同标签直接暴露英文角色定位值 |
| `src/components/ItemCatalogEditor.tsx` | `712`, `824`, `841` | `物品 ID`、`使用 Build Buff`、`套装 ID` | 字段标签里仍有 `ID` / `Build Buff` |
| `src/components/StateFunctionEditor.tsx` | `843-846`, `910` | `HP`、`No visible target` | 预览摘要和实时状态里仍有英文缩写与整句英文 |
| `src/components/StateFunctionEditor.tsx` | `1089-1091` | `Option behavior overrides saved.`、`Failed to save option behavior overrides` | 保存反馈文案仍是英文 |
| `src/components/StateFunctionEditor.tsx` | `1133`, `1212`, `1218` | `definition.state`、`AnimationState`、`idle/move/attack` | 原始状态值和动画值仍直接显示 |
| `src/components/PresetEditor.tsx` | `86-88`, `2212` | `NPC`、`敌对 NPC` | 页签和说明文案仍有 `NPC` 英文缩写 |
| `src/components/PresetEditor.tsx` | `893-910`, `1997-2000` | `AnimationState`、`steady/burst/...`、`idle/move/attack/die` | 技能动作、技能风格、怪物预览动作直接显示原始英文枚举值 |
| `src/components/PresetEditor.tsx` | `942`, `1015`, `1142`, `1456`, `1477`, `1483`, `1752`, `1788`, `1796`, `2037`, `2137`, `2174` | `Build Buff`、`ID`、`FPS` | 多个编辑字段和动画配置项仍保留英文术语 |
| `src/components/NpcVisualEditor.tsx` | `568-571`, `581`, `817` | `NPC 视觉编辑器`、`当前 NPC`、`x / y` | 标题、字段名和坐标显示仍有英文缩写 |
## 四、预设 / 数据层中的英文残留
这些内容虽然不一定直接在当前文件里渲染,但会进入运行时详情、掉落展示、物品编辑器、预设编辑器或交易界面。
| 文件 | 行号 | 当前文本/值 | 透出路径 |
| --- | --- | --- | --- |
| `src/data/monsterPresets.ts` | `438-456`, `479-499`, `535-540`, `647-650` | `Material`、`Relic`、`Armor`、`Consumable`、`Stone Shell Shard`、`Blood Lens`、英文描述句子、`rare/uncommon`、`material/relic/mana` | 会进入怪物掉落、物品详情、交易弹窗和编辑器预览 |
| `src/data/itemDesign.ts` | `56-72`, `123-149`, `602-604`, `761-762`, `832-834` | `worldAffinity` 的 `neutral/wuxia/xianxia``role` 的 `fieldcraft/breaker/caster/berserker/assassin``tags` 的 `breaker/burst/mana``pieceName` 的 `dust/crystal/gem`,以及 `build` 混排短语 | 会直接透到 `ItemCatalogEditor` 的世界、标签、构筑角色、协同标签和套装信息 |
| `src/data/characterPresets.ts` | `368-379`, `384-386`, `525-543`, `839-857`, `1024-1045` | `Double Jump`、`jump attack`、`Wall Slide`、`blunt/dry/direct`、`wary/fragmented` 等原始动作名和对话风格值 | 会被 `PresetEditor` 的动作/风格选择器直接显示 |
## 五、未复现的问题
- `src/components/GameShell.tsx` 角色选择页返回按钮旧乱码已修复,当前为“返回”。
- `src/data/npcInteractions.ts` 旧的切磋动作乱码已修复,当前为“敌对/切磋前蓄力,点击后转为原地闪避”。
- 本次扫描 `src/components`、`src/data` 未发现 `<EFBFBD>`replacement character类型的编码损坏。
- 除 `src/components/CustomWorldEntityEditorModal.tsx` 外,本次未再确认到新的源码级中文乱码文件。
- 自定义世界的 NPC 视觉编辑组件 `src/components/CustomWorldNpcVisualEditor.tsx` 本次未发现新的乱码。
## 六、建议处理顺序
1. 先修 `src/components/CustomWorldEntityEditorModal.tsx` 的真实乱码。
2. 再清理 `src/components/AdventurePanel.tsx` 和 `src/data/monsterPresets.ts` 的整句英文,因为它们最容易直接破坏玩家观感。
3. 为高频缩写和枚举值补统一映射层:
- `NPC`
- `HP` / `MP` / `CD`
- `worldAffinity`
- `role`
- `tags`
- `AnimationState`
- 技能风格 `steady/burst/mobility/finisher/projectile`
4. 最后统一编辑器里所有 `ID`、`FPS`、`Build Buff` 之类术语的显示策略。

View File

@@ -1,87 +0,0 @@
# 游戏 UI / 预设 / 编辑器 UI 英文与乱码复核
日期:`2026-03-30`
## 结论摘要
- 当前分支里,确认仍在真实渲染的源码级乱码主要集中在 2 个文件:
- `src/components/GameShell.tsx`
- `src/components/CustomWorldEntityEditorModal.tsx`
- `src/components/NpcVisualEditor.tsx` 中确实还留有旧乱码字符串,但位于 `/* ... */` 注释块里,本次不计入“当前 UI 问题”。
- 英文残留仍然较多,主要分为三类:
- 游戏运行时界面的英文标题、空态文案和缩写
- 编辑器界面的英文术语、英文保存反馈和原始枚举值
- 预设 / 数据层中的英文名称、标签、角色定位、动画目录和 build 相关原值直接透到 UI
## 复核口径
- 显式按 UTF-8 读取文件,避免把终端编码问题误判成源码乱码。
- 只统计会进入游戏 UI、编辑器 UI、预设预览或结果页的文本。
- 注释块、变量名、导入路径、接口路径等内部实现名不计入本次问题清单。
- 英文残留部分以下表中的“当前确实会显示或透传”的高优先级项为主。
## 一、已确认的真实乱码
| 范围 | 文件 | 行号 | 当前文本 | 说明 |
| --- | --- | --- | --- | --- |
| 游戏 UI | `src/components/GameShell.tsx` | `565` | `瑙掕壊` | 主界面底部“角色”页签已写坏 |
| 游戏 UI | `src/components/GameShell.tsx` | `578` | `鍐掗櫓` | 主界面底部“冒险”页签已写坏 |
| 游戏 UI | `src/components/GameShell.tsx` | `591` | `鑳屽寘` | 主界面底部“背包”页签已写坏 |
| 游戏 UI | `src/components/GameShell.tsx` | `710` | `闃熶紞` / `鑳屽寘` | 浮层标题根据面板切换时会显示乱码 |
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `386` | `宸查€?` | 场景预设选择弹窗中的“已选中”状态标签已写坏 |
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `432` | `鍙栨秷` | 统一保存栏的取消按钮已写坏 |
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `436` | `淇濆瓨淇敼` | 统一保存栏的主按钮文案已写坏 |
## 二、游戏 UI 中的英文残留
| 文件 | 行号 | 当前文本 / 值 | 说明 |
| --- | --- | --- | --- |
| `src/components/AdventurePanel.tsx` | `363` | `Currency` | 任务奖励卡的货币标题仍是英文 |
| `src/components/AdventurePanel.tsx` | `371` | `No item bounty attached to this quest.` | 任务奖励空态文案仍是英文 |
| `src/components/AdventurePanel.tsx` | `1424-1428` | `LOOT CACHE``Tap an item icon to inspect its details.``No usable loot dropped this time, but the battle is still settled.` | 战利品弹层标题和说明仍是整句英文 |
| `src/components/AdventurePanel.tsx` | `1442` | `No loot dropped this time.` | 战利品列表空态文案仍是英文 |
| `src/components/AdventurePanel.tsx` | `1352`, `1524` | `x{item.quantity}``HP` / `MP` | 数量展示与效果预览仍保留英文缩写 |
| `src/components/AdventureEntityModal.tsx` | `892-899` | `label="HP"``label="MP"` | 同行状态估计卡仍使用英文缩写 |
| `src/components/AdventureEntityModal.tsx` | `1073`, `1111`, `1428` | `NPC 信息``敌对NPC` / `NPC``NPC 背包` | NPC 详情区仍是中英混排 |
| `src/components/CompanionCampModal.tsx` | `177-178`, `233-234`, `255` | `HP``MP``NPC` | 营地卡片和空态提示仍保留英文缩写 |
| `src/components/NpcModals.tsx` | `79`, `252`, `273`, `356`, `408` | `x{item.quantity}``NPC 商品列表``这个 NPC 当前没有可售商品。``NPC 商品``HP` / `MP` | 交易弹窗、详情弹窗和数量角标存在中英混排 |
| `src/components/CharacterDetailModal.tsx` | `117` | `数量 x{item.quantity}` | 角色详情中的数量前缀仍保留英文 `x` |
## 三、编辑器 UI 中的英文残留
| 文件 | 行号 | 当前文本 / 值 | 说明 |
| --- | --- | --- | --- |
| `src/components/ItemCatalogEditor.tsx` | `576-581`, `621-624` | `fieldcraft``breaker``mana``boots``dust``crystal``gem` 等原值 | 物品标签、构筑角色、部件名和协同信息会直接显示英文原值 |
| `src/components/ItemCatalogEditor.tsx` | `648`, `671`, `729-783`, `800` | `HP` / `MP` / `CD``物品 ID``使用 Build Buff``套装 ID` | 物品编辑器预览与字段标签仍有英文缩写 / 术语 |
| `src/components/StateFunctionEditor.tsx` | `818-821`, `885`, `915` | `HP``No visible target``n/a` | 选项行为预览面板仍有英文缩写和英文空态 |
| `src/components/StateFunctionEditor.tsx` | `1060-1064` | `Failed to save option behavior overrides``Option behavior overrides saved.` | 保存反馈仍是英文 |
| `src/components/StateFunctionEditor.tsx` | `1106`, `1185`, `1191` | `battle/idle``AnimationState` 的原始动作值、`idle/move/attack` | 状态和动作枚举值直接显示为英文 |
| `src/components/PresetEditor.tsx` | `88`, `90`, `1474-1501`, `1806-1814` | `NPC``敌对 NPC``NPC ID``关联角色 ID``敌对资源 ID``连接场景 ID` | 多个标签和页签仍保留英文缩写 / `ID` |
| `src/components/PresetEditor.tsx` | `101-106`, `896-913`, `2008-2018` | `idle/move/attack/die``steady/burst/mobility/finisher/projectile` | 角色技能和敌对资源预览会直接显示英文枚举值 |
| `src/components/PresetEditor.tsx` | `945`, `2155`, `2192` | `Build Buff``FPS` | 技能编辑和动作图集配置仍有英文术语 |
| `src/components/NpcVisualEditor.tsx` | `416-461` | `Failed to load NPC visual overrides``Failed to load NPC layout config``using bundled defaults` | NPC 视觉编辑器的加载失败提示仍是英文 |
| `src/components/NpcVisualEditor.tsx` | `678`, `718` | `Saved NPC visual overrides to ...``Saved shared NPC layout config.` | 保存成功反馈仍是英文 |
| `src/components/NpcVisualEditor.tsx` | `903`, `906`, `919`, `1226` | `NPC 视觉编辑器``当前 NPC``x ... / y ...` | 标题、字段标签与坐标信息仍存在中英混排 |
## 四、预设 / 数据层中会透到 UI 的英文值
| 文件 | 行号 | 当前文本 / 值 | 透出路径 |
| --- | --- | --- | --- |
| `src/data/monsterPresets.ts` | `494-512`, `522-540`, `647-652`, `718-723` | `Armor``Relic``Material``Consumable``Carapace Plate``Guard Core``Spore Pouch``Burst Cap``Ashfire Feather`、英文描述句子、`rare/uncommon` | 会进入怪物掉落、战利品详情、交易弹窗和物品预览 |
| `src/data/itemDesign.ts` | `56-57`, `67-68`, `123-149` | `worldAffinity: neutral/wuxia/xianxia``role: fieldcraft/breaker/caster/berserker/assassin``tags` 中的 `caster/mana/burst/assassin` | 会透到 `ItemCatalogEditor` 的世界、角色定位、标签和协同信息 |
| `src/data/itemDesign.ts` | `213-219`, `538-545`, `589-604`, `731-762`, `820-834`, `906-918` | `pieceName: boots/chest/gloves/...``build``setId``role``dust/crystal/gem` 等 | 会透到物品编辑器、套装信息和部件信息展示 |
| `src/data/characterPresets.ts` | `54-69` | `blunt/wary/dry/direct/fragmented` | 对话风格与性格归类原值会被编辑器直接显示 |
| `src/data/characterPresets.ts` | `368-379`, `525-526`, `839-850`, `1024-1025` | `Double Jump``jump attack``Wall Slide` | 角色动作目录 / 前缀原值会被 `PresetEditor` 直接显示 |
| `src/data/characterPresets.ts` | `384-386`, `541-543`, `855-857`, `1045` | `guardStyle` / `warmStyle` / `truthStyle` 对应的英文原值 | 角色预设风格字段在编辑器中仍会显示英文 |
## 五、未计入项
- `src/components/NpcVisualEditor.tsx:681-683``721-722` 的乱码字符串位于块注释内,不会进入当前界面,因此未计入本次“活跃问题”。
- `docs/*.md` 里的历史审计文档和旧清单不在本次范围内,本次只统计游戏 UI、预设和编辑器 UI。
## 六、建议处理顺序
1. 先修 `src/components/GameShell.tsx``src/components/CustomWorldEntityEditorModal.tsx` 的真实乱码,因为它们已经直接出现在主流程界面。
2. 再清理 `src/components/AdventurePanel.tsx` 的英文空态、战利品标题和 `Currency`,这是玩家最容易直接看到的一批英文。
3. 然后统一编辑器术语映射,优先处理 `HP` / `MP` / `NPC` / `ID` / `FPS` / `Build Buff` / `AnimationState`
4. 最后为 `src/data/itemDesign.ts``src/data/monsterPresets.ts``src/data/characterPresets.ts` 增加显示层映射,避免原始英文值继续直接透到编辑器和运行时界面。

View File

@@ -1,280 +0,0 @@
# 游戏 UI / 预设实体 / 编辑器 UI 英文与乱码复核(续)
日期:`2026-03-30`
## 说明
- 这份文档是对当前分支的重新复核,不直接沿用旧审计文档的正文,因为旧文档本身已经存在较明显乱码。
- 本轮重点覆盖三类范围:
- 游戏运行时 UI`src/components/` 下实际会进入主流程的界面,以及 `src/components/game-shell/`
- 编辑器 UI`src/components/*Editor*.tsx``src/components/preset-editor/``src/editor/shared/`
- 预设实体 / 数据层:`src/data/` 中会被编辑器、预览面板或游戏详情页直接透出的文本
- 复核方式:
- 直接按 UTF-8 读取源码,避免把终端显示问题误判成源码乱码
- 只记录会显示在玩家或编辑器使用者面前的文本
- `import`、类型名、变量名、接口字段名、纯内部注释默认不计入
- 但保存 / 加载提示这类虽然来自 helper 文件、最终会显示到 UI 的字符串,仍计入
## 结论摘要
- 当前分支里,真正“源码里已经写坏”的中文乱码,主要集中在 4 个位置:
- `src/components/GameShell.tsx`
- `src/components/preset-editor/shared.ts`
- `src/components/CustomWorldEntityEditorModal.tsx`
- `src/components/preset-editor/PresetEditorPanels.tsx`
- 其中最严重的是 `src/components/preset-editor/PresetEditorPanels.tsx`
- 角色/NPC/场景/敌对 NPC 资源四个子面板里都有残缺字符串
- 同时混有 `NPC``ID``FPS``Build Buff``Medieval NPC` 等英文术语
- 数据层 `src/data/` 本轮没有再扫到新的中文乱码;问题更多是英文预设值直接透到编辑器 / 预览 UI。
- 游戏运行时 UI 侧已经比旧清单干净很多,但仍有几块明显英文残留:
- `AdventurePanel`
- `AdventureEntityModal`
- `CompanionCampModal`
- `NpcModals`
- `game-shell/CharacterSelectionFlow`
## 一、已确认的真乱码
| 范围 | 文件 | 行号 | 当前文本示例 | 说明 |
| --- | --- | --- | --- | --- |
| 游戏 UI | `src/components/GameShell.tsx` | `565`, `578`, `591`, `710` | `瑙掕壊``鍐掗櫓``鑳屽寘``闃熶紞` | 主界面底部 tab 和浮层标题已写坏 |
| 编辑器 UI | `src/components/preset-editor/shared.ts` | `42-55` | `瑙掕壊``鍦烘櫙``鐗╁搧``鏁屽 NPC``姝︿緺``浠欎緺``鑷畾涔変笘鐣?` | 新版预设编辑器 tab 与世界标签已写坏 |
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `383`, `429`, `433` | `宸查€?``鍙栨秷``淇濆瓨淇敼` | 自定义世界实体编辑弹窗里的已选中/取消/保存文案乱码 |
| 编辑器 UI | `src/components/preset-editor/PresetEditorPanels.tsx` | `251`, `530`, `1383`, `1468`, `1478`, `1830` 等多处 | `鏂版妧鑳?``鏂板鎶€鑳?``绾満鏅?``鑳屾櫙鍥捐矾寰?``涓嶈缃?``... FPS銆?` | 新版预设编辑器存在大面积残缺字符串,部分已经带 `?` 结尾 |
### `PresetEditorPanels.tsx` 乱码分布
- 角色预设区:
- `251`, `310`, `323`, `379`, `467-688`, `719-802`
- 示例:`新技<E696B0>?``预览技<E8A788>?``法力消<E58A9B>?``属性面<E680A7>?``主场<E4B8BB>?`
- NPC 预设区:
- `1000-1208`
- 示例:`这里汇总了场景里的所<E79A84>?NPC 角色预设<E9A284>?``如果<E5A682>?NPC 绑定了角色技能...<2E>?``敌对 NPC 会沿用战斗资源预设展示...<2E>?`
- 场景预设区:
- `1244-1478`
- 示例:`没有可编辑的场景预设<E9A284>?``敌<>?NPC``纯场<E7BAAF>?``背景图路<E59BBE>?``不设<E4B88D>?`
- 敌对 NPC 资源区:
- `1551-1851`
- 示例:`没有可编辑的敌对 NPC 资源<E8B584>?``基础数<E7A180>?``最大生<E5A4A7>?``... 和 FPS<50>?``起始<E8B5B7>?`
## 二、游戏 UI 中仍会显示的英文
### 1. 主冒险面板
- `src/components/AdventurePanel.tsx:363`
- `Currency`
- `src/components/AdventurePanel.tsx:371`
- `No item bounty attached to this quest.`
- `src/components/AdventurePanel.tsx:1424`
- `LOOT CACHE`
- `src/components/AdventurePanel.tsx:1427-1428`
- `Tap an item icon to inspect its details.`
- `No usable loot dropped this time, but the battle is still settled.`
- `src/components/AdventurePanel.tsx:1442`
- `No loot dropped this time.`
- `src/components/AdventurePanel.tsx:1524`
- `HP` / `MP`
### 2. 实体详情与交互弹窗
- `src/components/AdventureEntityModal.tsx:1163-1165`
- `x{item.quantity}`
- `Inspect`
- `src/components/AdventureEntityModal.tsx:1428`
- `NPC 背包`
- `src/components/CompanionCampModal.tsx:177-178`, `233-234`, `255`
- `HP`
- `MP`
- `NPC`
- `src/components/NpcModals.tsx:252`, `273`, `356`, `408`
- `NPC 商品列表`
- `这个 NPC 当前没有可售商品。`
- `NPC 商品`
- `HP` / `MP`
### 3. 开场选角流
- `src/components/game-shell/CharacterSelectionFlow.tsx:28-32`
- `Sword Princess`
- `Royal Blade`
- `Vanguard`
- `Twin Blade Rogue`
- `Assassin`
- `Armored Spear`
- `src/components/game-shell/CharacterSelectionFlow.tsx:35-39`
- `STR`
- `AGI`
- `INT`
- `SPI`
- `src/components/game-shell/CharacterSelectionFlow.tsx:329-333`
- `Character Stats`
- `Gender:`
## 三、编辑器 UI 中仍会显示的英文
### 1. 旧预设编辑入口
- `src/components/PresetEditor.tsx:61-69`
- `Preset Workshop`
- `Unified Preset Preview And Editor`
- `Manage character, NPC, scene, monster, item, and behavior presets from one editor shell. Each tab now loads its own container so the entry component stays small and focused.`
### 2. 新预设编辑器共享配置
- `src/components/preset-editor/shared.ts:60-72`
- `idle`
- `move`
- `attack`
- `die`
- `steady`
- `burst`
- `mobility`
- `finisher`
- `projectile`
### 3. 新预设编辑器主面板
- `src/components/preset-editor/PresetEditorPanels.tsx:620`
- `Build Buff`
- `src/components/preset-editor/PresetEditorPanels.tsx:966`
- `No NPC presets available.`
- `src/components/preset-editor/PresetEditorPanels.tsx:1100-1202`
- `NPC`
- `NPC ID`
- `Medieval NPC`
- `src/components/preset-editor/PresetEditorPanels.tsx:1830`, `1867`
- `FPS`
### 4. 物品编辑器
- `src/components/ItemCatalogEditor.tsx:648`
- `HP`
- `MP`
- `CD`
- `src/components/ItemCatalogEditor.tsx:783`
- `Build Buff`
- `src/components/ItemCatalogEditor.tsx:800`
- `套装 ID`
- `src/components/ItemCatalogEditor.tsx:576-585`, `793-800`
- `selectedItem.tags``buildProfile.role``setId` 等原始英文值会直接显示在预览或输入框里
### 5. 选项行为编辑器
- `src/components/StateFunctionEditor.tsx:818`, `821`
- `HP`
- `No visible target`
- `src/components/StateFunctionEditor.tsx:885`, `915`
- `HP`
- `n/a`
- `src/components/StateFunctionEditor.tsx:1060-1064`
- `Failed to save option behavior overrides`
- `Option behavior overrides saved.`
- `src/components/StateFunctionEditor.tsx:1185`
- `AnimationState` 枚举值直接作为 label 显示
- `src/components/StateFunctionEditor.tsx:1191`
- `idle` / `move` / `attack`
- `src/components/StateFunctionEditor.tsx:1217`
- `steady` / `burst` / `mobility` / `finisher` / `projectile`
### 6. NPC 视觉编辑器与自定义世界编辑器
- `src/components/npcVisualEditorPersistence.ts:27-32`, `46-51`
- `Failed to save NPC visual overrides`
- `Saved NPC visual overrides to src/data/npcVisualOverrides.json.`
- `Failed to save NPC layout config`
- `Saved shared NPC layout config.`
- `src/components/CustomWorldEntityCatalog.tsx:345`
- `MedievalFantasyCharacters`
- `src/components/CustomWorldEntityEditorModal.tsx:457`
- `MedievalFantasyCharacters`
## 四、预设实体 / 数据层中会透到 UI 的英文值
### 1. 物品预设
- `src/data/itemDesign.ts:52-58`, `67-69`, `123-149`
- `worldAffinity: "neutral" / "wuxia" / "xianxia"`
- `role: "fieldcraft" / "breaker" / "caster" / "berserker" / "assassin"`
- `rarity: "common" / "rare" / "epic"`
- `tags: ["caster", "mana"]`
- `src/data/itemDesign.ts:213-219`
- `pieceName: "boots" / "chest" / "gloves" / "helm" / "leggings" / "shield" / "weapon"`
- `src/data/itemDesign.ts:538-545`, `588-606`, `730-766`, `818-836`, `904-919`
- 描述和 profile 中直接拼入 `build``role``dust``crystal``gem` 等英文值
- 这些字段会在 `ItemCatalogEditor` 预览和构筑信息里直出
### 2. 敌对资源 / 掉落预设
- `src/data/monsterPresets.ts:494-540`, `647-723`
- 掉落类别:`Armor``Relic``Material``Consumable`
- 掉落名称:`Carapace Plate``Guard Core``Spore Pouch``Burst Cap``Ashfire Feather``Serpent Eye``Tide Ink``Lake Pearl``Thorn Nectar`
- 掉落描述整句仍是英文
- 这些条目会直接进入掉落预览、NPC 交易与物品详情
### 3. 角色预设
- `src/data/characterPresets.ts:53-70`
- 对话风格值:`blunt``wary``evasive``measured``gentle``teasing``dry``steady``direct``fragmented``deflecting`
- `src/data/characterPresets.ts:368-379`, `525-536`, `839-850`, `1024-1038`
- 动画资源名:`Double Jump``jump attack``Wall Slide``skill1 bullet FX`
- `src/data/characterPresets.ts:384-386`, `541-543`, `855-857`, `1043-1045`
- `guardStyle` / `warmStyle` / `truthStyle` 的英文原值
- 这些值会在角色预设编辑器与动作 / 风格下拉中透出
### 4. Build / 标签词典
- `src/data/buildTags.ts:42`, `56`, `91`, `126-147`, `309-316`
- `assassin`
- `fieldcraft`
- `breaker`
- `caster`
- `armor`
- `relic`
- `material`
- `consumable`
- `rare`
- `wuxia`
- `xianxia`
- `neutral`
- 这些原始 tag 会通过物品标签、build profile 和编辑器预览进入显示层
## 五、本轮复核中未发现新增中文乱码的范围
### 游戏 UI
- `src/components/CharacterChatModal.tsx`
- `src/components/CharacterDetailModal.tsx`
- `src/components/CharacterPanel.tsx`
- `src/components/MapModal.tsx`
说明:
- 上述文件大体已中文化。
- 仍可能存在少量英文缩写、内部 ID 或技术词,但本轮没有再发现新的明显中文乱码。
### 数据层
- `src/data/scenePresets.ts`
- `src/data/npcInteractions.ts`
- `src/data/treasureInteractions.ts`
- `src/data/customWorldLibrary.ts`
- `src/data/customWorldRuntime.ts`
说明:
- 本轮在 `src/data/` 中没有扫到新的中文乱码。
- 当前数据层问题主要是英文 tag、role、rarity、pieceName 等原始值会被上层编辑器直接显示。
## 六、建议优先级
1. 先修 `src/components/preset-editor/PresetEditorPanels.tsx`
- 当前最集中的真乱码源
- 已经影响角色 / NPC / 场景 / 敌对资源四个主编辑子页
2. 再修 `src/components/preset-editor/shared.ts``src/components/GameShell.tsx`
- 一个影响预设编辑入口 tab 与世界标签
- 一个影响玩家主界面底部导航
3. 然后处理 `src/components/CustomWorldEntityEditorModal.tsx`
- 量不大,但按钮文案已经坏到影响操作判断
4. 最后统一清英文术语
- 游戏 UI`AdventurePanel``AdventureEntityModal``CompanionCampModal``NpcModals``CharacterSelectionFlow`
- 编辑器 UI`PresetEditor.tsx``ItemCatalogEditor.tsx``StateFunctionEditor.tsx``npcVisualEditorPersistence.ts`
- 数据层:`itemDesign.ts``monsterPresets.ts``characterPresets.ts``buildTags.ts`

View File

@@ -1,194 +0,0 @@
# 游戏 UI / 预设 / 编辑器 UI 文案排查
日期:`2026-03-31`
## 说明
- 本文档基于当前分支源码重新复核,直接按 UTF-8 读取,不沿用旧审计文档中的乱码文本。
- 只记录会出现在游戏 UI、预设编辑器 UI、结果页预览或保存反馈中的文本。
- `import`、变量名、注释、仅内部使用的路径名,不计入本次问题清单。
- 位图图片里的内嵌文本未做 OCR本次只看源码层可见文案。
## 结论摘要
- 当前问题可以分成 3 类:
- 真实中文乱码或截断。
- 英文或英文缩写直接暴露在中文界面。
- 预设数据中的英文原始值直接透出到编辑器或预览。
- 乱码最集中的文件:
- `src/components/preset-editor/PresetEditorPanels.tsx`
- `src/components/NpcVisualEditor.tsx`
- `src/components/CustomWorldEntityEditorModal.tsx`
- `src/components/GameShell.tsx`
- `src/editor/shared/FormFields.tsx`
- 英文最集中的文件:
- `src/components/adventure-panel/AdventurePanelOverlays.tsx`
- `src/components/game-shell/PreGameSelectionFlow.tsx`
- `src/components/game-shell/CharacterSelectionFlow.tsx`
- `src/components/PresetEditor.tsx`
- `src/components/ItemCatalogEditor.tsx`
- `src/components/StateFunctionEditor.tsx`
- 预设数据层仍有一批英文原始值会直接透出到 UI
- `src/data/itemDesign.ts`
- `src/data/monsterPresets.ts`
- `src/data/characterPresets.ts`
- `src/data/buildTags.ts`
## 一、已确认的中文乱码 / 截断
| 范围 | 文件 | 行号 | 当前文本示例 | 说明 |
| --- | --- | --- | --- | --- |
| 游戏 UI | `src/components/GameShell.tsx` | `598`, `611`, `624` | `瑙掕壊` / `鍐掗櫓` / `鑳屽寘` | 主流程底部三个 tab 标签已写坏 |
| 游戏 UI | `src/components/AdventurePanel.tsx` | `569-571` | `已完<E5B7B2>?` / `已交<E5B7B2>?` / `进行<E8BF9B>?` | 任务状态标签出现截断乱码 |
| 游戏 UI | `src/components/CharacterDetailModal.tsx` | `223` | `属<>?` | 角色详情分区标题截断 |
| 编辑器 UI | `src/components/CustomWorldEntityEditorModal.tsx` | `242`, `384`, `430`, `434` | `鏀寔...URL` / `宸查€?` / `鍙栨秷` / `淇濆瓨淇敼` | 自定义世界实体编辑弹窗的占位、选中态、取消和保存按钮已写坏 |
| 编辑器 UI | `src/components/preset-editor/shared.ts` | `42-55` | `瑙掕壊` / `鍦烘櫙` / `鏁屽 NPC` / `姝︿緺` / `浠欎緺` / `鑷畾涔変笘鐣?` | 预设编辑器主 tab 和世界标签存在乱码 |
| 编辑器 UI | `src/components/preset-editor/PresetEditorPanels.tsx` | `1269`, `1364`, `1371-1372`, `1467`, `1477-1486`, `1521`, `1654-1661`, `1689`, `1707` | 多处整句乱码 | 主编辑面板说明文案、预览模式、帮助文本、提示段落大面积损坏 |
| 编辑器 UI | `src/components/NpcVisualEditor.tsx` | `463`, `521`, `550`, `701-705`, `719`, `786-833` | 多处整句乱码 | NPC 视觉编辑器的空态、失败提示、回滚提示、页头说明和多组选项已写坏 |
| 编辑器 UI | `src/editor/shared/FormFields.tsx` | `156` | `淇濆瓨涓?..` | 通用保存按钮的“保存中...”状态显示乱码 |
## 二、游戏 UI 中的英文残留
### 1. 冒险主界面与奖励弹层
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:114-125`
- 奖励物品描述 fallback 仍是整句英文,如 `restores HP during the run``works as a rare relic reward`
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:136-157`
- 任务目标展示里仍有 `BOUNTY TARGET``CACHE TRACE``SPAR SESSION``Inspect the hidden reward site`
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:262-291`
- 任务奖励卡里仍有 `REWARD CACHE``Tap an item icon to inspect its details.``Affinity``Currency``No item bounty attached to this quest.`
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:351-358`
- 目标详情卡仍有 `Objective``Area`
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:490`, `525`, `668`
- 统计说明、保存禁用提示、空任务提示仍是英文,如 `Inspect play time, kills, quests, and travel history.``Saving is temporarily disabled...``No active quests yet.`
- `src/components/adventure-panel/AdventurePanelOverlays.tsx:749`, `781-785`, `831`, `887-908`, `925-1016`
- 完成奖励与战斗奖励弹层仍有 `Claim reward``QUEST COMPLETE``Reward ready``Quest reward claimed``Battle reward``LOOT CACHE``No loot dropped this time.``Rarity``Quantity``Slot``Not equippable``Usable directly``Effect preview: HP + ... / MP + ...`
### 2. 实体详情与 NPC 交互
- `src/components/AdventureEntityModal.tsx:1073`, `1111`, `1163-1165`, `1252`, `1428`
- 仍有 `NPC 信息``NPC``x{item.quantity}``Inspect``Character``NPC 背包`
- `src/components/AdventureEntityModal.tsx:892`, `898`
- 同伴状态标签仍直接显示 `HP` / `MP`
- `src/components/CompanionCampModal.tsx:177-178`, `233-234`, `255`
- 同伴卡片和空态句子里仍有 `HP` / `MP` / `NPC`
- `src/components/NpcModals.tsx:79`, `252`, `273`, `356`, `408`
- 交易弹窗与详情弹窗里仍有 `x{item.quantity}``NPC 商品列表``这个 NPC 当前没有可售商品。``NPC 商品``效果预览HP + ... / MP + ...`
### 3. 开场流程与角色选择
- `src/components/game-shell/CharacterSelectionFlow.tsx:28-44`
- 角色名、称号、定位、标签全部是英文,如 `Sword Princess``Royal Blade``Vanguard``STR``AGI``Female``Male`
- `src/components/game-shell/CharacterSelectionFlow.tsx:329-391`
- 面板标题和按钮仍有 `Character Stats``Gender:``Backstory``Customize``Details``Enter Camp``Go`
- `src/components/game-shell/PreGameSelectionFlow.tsx:63-75`
- 自定义世界生成进度仍全是英文,如 `Finalizing world archive...``Generating core NPCs...``Parsing world setup...`
- `src/components/game-shell/PreGameSelectionFlow.tsx:252-308`
- 开场按钮和入口仍有 `New Game``Start Game``Developer Team``Go``CONTACTS``WORLD SELECT``Back`
- `src/components/game-shell/PreGameSelectionFlow.tsx:344-421`
- 世界卡片与自定义世界入口仍有 `Online``Featured``Saved``Playable``Landmarks``Custom``Create Custom World``Enter a world setup...`
- `src/components/GameShell.tsx:630`, `651`, `695`
- Suspense fallback 仍显示 `Loading party panel``Loading adventure panel``Loading inventory panel`
### 4. 其他游戏 UI
- `src/components/CharacterDetailModal.tsx:112`
- `数量 x{item.quantity}` 中的 `x` 仍保留英文数量前缀。
## 三、编辑器 UI 中的英文残留
### 1. 编辑器入口与共享配置
- `src/components/PresetEditor.tsx:65-73`
- 页头完整为英文:`Preset Workshop``Unified Preset Preview And Editor` 及其说明段。
- `src/components/preset-editor/shared.ts:43`, `60-72`
- 主 tab 仍有 `NPC`;动画和技能风格选项仍直接使用 `idle``move``attack``die``steady``burst``mobility``finisher``projectile`
### 2. 预设编辑器主面板
- `src/components/preset-editor/PresetEditorPanels.tsx:1267`, `1594`
- 保存反馈仍是 `Saved.`
- `src/components/preset-editor/PresetEditorPanels.tsx:1277-1279`, `1327-1328`, `1414-1415`, `1608-1609`, `1647-1648`
- 多个分区标题和描述仍是占位英文 `Section` / `Editor section.`
- `src/components/preset-editor/PresetEditorPanels.tsx:1283`, `1442`, `1448`
- 表单标签出现错误拼接,如 `Field"NPC"``Field"ID"`
- `src/components/preset-editor/PresetEditorPanels.tsx:1320`, `1640`
- 保存按钮文字仍是 `Save`
- `src/components/preset-editor/PresetEditorPanels.tsx:1421`, `1658-1661`, `1692`, `1698`, `1701`, `1710`, `2149`
- 仍有 `NPC ID``Monster Encounter``NPC Encounter``Empty Scene``None``NPC``FPS` 等英文或英文缩写。
### 3. 物品 / 行为 / NPC 视觉编辑器
- `src/components/ItemCatalogEditor.tsx:648`, `729`, `736`, `760`, `767`, `783`, `800`
- 仍有 `HP``MP``CD``Build Buff``ID`
- `src/components/ItemCatalogEditor.tsx:793-817`
- `buildProfile.role``setId``pieceName` 等原始英文值直接显示在输入框。
- `src/components/StateFunctionEditor.tsx:818-821`, `885`, `915`
- 预览信息里仍有 `HP``No visible target``n/a`
- `src/components/StateFunctionEditor.tsx:1060-1064`
- 保存失败/成功提示仍是英文:`Failed to save option behavior overrides``Option behavior overrides saved.`
- `src/components/StateFunctionEditor.tsx:1106`, `1185`, `1191`, `1217`
- 仍直接展示 `battle` / `idle``AnimationState` 原值、`idle` / `move` / `attack`,以及 `steady` / `burst` / `mobility` / `finisher` / `projectile`
- `src/components/NpcVisualEditor.tsx:538`, `714`, `781`, `798`
- 仍有 `Save failed``Current NPC``Custom Hair Color``Hide Facial Hair`
- `src/components/npcVisualEditorPersistence.ts:26`, `31`, `45`, `50`
- 保存提示仍为 `Failed to save NPC visual overrides``Saved NPC visual overrides to src/data/npcVisualOverrides.json.``Failed to save NPC layout config``Saved shared NPC layout config.`
### 4. 自定义世界结果页 / 编辑弹窗
- `src/components/CustomWorldEntityCatalog.tsx:346`
- 说明文案里直接暴露资产名 `MedievalFantasyCharacters`
- `src/components/CustomWorldEntityEditorModal.tsx:242`, `458`
- 图片路径占位里仍保留 `URL`NPC 形象编辑说明里直接出现 `MedievalFantasyCharacters`
## 四、预设 / 数据层中会透出 UI 的英文原始值
### 1. 物品预设
- `src/data/itemDesign.ts:56-58`, `67-69`, `123-149`
- `worldAffinity``role``rarity``tags` 中仍有 `neutral``wuxia``xianxia``fieldcraft``breaker``caster``berserker``assassin``common``rare``epic` 等原始值。
- `src/data/itemDesign.ts:213-219`
- `pieceName` 仍为 `boots``chest``gloves``helm``leggings``shield``weapon`
- `src/data/itemDesign.ts:538-545`, `581-598`, `731-748`, `906-913`
- 描述拼接和构筑信息里仍直接出现 `build``role``dust``crystal``gem` 等英文原始词。
- 这些值会直接透出到 `ItemCatalogEditor` 的标签、构筑字段和预览信息。
### 2. 怪物掉落预设
- `src/data/monsterPresets.ts:494-536`, `647-721`
- 掉落类别仍有 `Armor``Relic``Material``Consumable`
- 掉落名称仍有 `Carapace Plate``Guard Core``Spore Pouch``Burst Cap``Ashfire Feather``Serpent Eye``Tide Ink``Lake Pearl``Thorn Nectar`
- 掉落描述仍有整句英文,如 `A toxin sac prized by alchemists and assassins alike.`
- 这些值会进入战斗奖励、物品详情和交易 UI。
### 3. 角色预设
- `src/data/characterPresets.ts:54-70`
- 会话风格原始值仍为 `blunt``wary``evasive``measured``gentle``teasing``dry``steady``direct``fragmented``deflecting`
- `src/data/characterPresets.ts:368-386`, `525-543`, `839-857`, `1024-1045`
- 动画文件夹 / 前缀与风格原始值仍有 `Double Jump``jump attack``Wall Slide``guardStyle``warmStyle``truthStyle`
- 这些值会透出到角色预设编辑器、技能预览和部分选择器。
### 4. Build / 标签词典
- `src/data/buildTags.ts:42`, `56`, `91`, `126-147`, `308-316`
- 仍有 `assassin``fieldcraft``breaker``caster``weapon``armor``relic``material``consumable``rare``wuxia``xianxia``neutral` 等原始标签。
- 这些值会在物品编辑器标签、构筑画像和相似度映射结果中直接显示。
## 五、优先级建议
1. 先修 `src/components/preset-editor/PresetEditorPanels.tsx``src/components/NpcVisualEditor.tsx`
- 这两处是当前编辑器侧最严重的问题源,既有大面积乱码,也有大量英文占位词。
2. 再修游戏首屏与奖励相关 UI
- 优先处理 `src/components/adventure-panel/AdventurePanelOverlays.tsx`
- 优先处理 `src/components/game-shell/PreGameSelectionFlow.tsx`
- 优先处理 `src/components/game-shell/CharacterSelectionFlow.tsx`
3. 然后修直接影响主流程判断的乱码
- `src/components/GameShell.tsx`
- `src/components/AdventurePanel.tsx`
- `src/components/CharacterDetailModal.tsx`
- `src/components/CustomWorldEntityEditorModal.tsx`
- `src/editor/shared/FormFields.tsx`
4. 最后补“显示层映射”
-`itemDesign.ts``monsterPresets.ts``characterPresets.ts``buildTags.ts` 这类预设原始值统一增加中文显示映射,避免继续把内部英文值直接透给编辑器和游戏 UI。

View File

@@ -1,325 +0,0 @@
# 游戏 UI / 预设 / 编辑器文本审计
日期:`2026-04-01`
## 范围
- 扫描范围:`src/components/``src/editor/``src/routing/``src/hooks/``src/services/``src/data/`
- 聚焦对象:
- 游戏内实际可见 UI 文本
- 预设编辑器与自定义世界编辑器中的可见文本
- 会直接透出到游戏 UI / 编辑器 UI 的预设原始值
- 未覆盖:
- 图片资源内嵌文字的 OCR
- `docs/` 历史文档本身
- 单纯内部实现用的 import path、className、asset path、纯 id 常量
## 方法
- 先做一轮源码级 AST 扫描,抽取 JSX 可见文本、按钮文案、占位文案、标签文案和常见说明文案。
- 再做一轮“反向解码”复核:
- `瑙掕壊 -> 角色`
- `鍦烘櫙 -> 场景`
- `姝︿緺 -> 武侠`
- `鏈煡 AI 閿欒 -> 未知 AI 错误`
- 结论只保留当前源码里仍然存在的问题,不直接沿用旧审计文档。
## 结论摘要
- 当前仍然有 3 类问题:
1. 真实乱码:主要在 `appRoutes.tsx``AdventurePanel.tsx``CharacterDetailModal.tsx``useStoryGeneration.ts``preset-editor/shared.ts` 和 4 个拆分后的预设面板文件中。
2. 游戏 / 编辑器英文残留:主要在 `AdventurePanelOverlays.tsx``AdventureEntityModal.tsx``PreGameSelectionFlow.tsx``NpcVisualEditor.tsx``ItemCatalogEditor.tsx``StateFunctionEditor.tsx`、自定义世界编辑器几处。
3. 预设原始值直接透出:主要在 `characterPresets.ts``itemDesign.ts``monsterPresets.ts``buildTags.ts``scenePresets.ts``stateFunctions.ts`
- 编辑器侧当前最明显的重灾区不是旧的 `PresetEditorPanels.tsx` 大文件,而是已经拆分出的:
- `src/components/preset-editor/shared.ts`
- `src/components/preset-editor/CharacterPresetPanel.tsx`
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
- `src/components/preset-editor/ScenePresetPanel.tsx`
- `src/components/preset-editor/MonsterPresetPanel.tsx`
- 游戏主流程里影响最直观的点:
- 路由加载页文本乱码
- 冒险面板里的任务状态 / 对话状态 / NPC 交互短描述乱码
- AI 错误兜底文案乱码
## 一、游戏 UI已确认乱码
| 文件 | 行号 | 当前文本 / 范围 | 说明 |
| --- | --- | --- | --- |
| `src/routing/appRoutes.tsx` | `103-115` | `LOADING EDITOR``LOADING GAME``姝e湪杞藉叆缂栬緫鍣?..``姝e湪杞藉叆鍐掗櫓...` | 路由级加载屏文案。后两段是真乱码;结合反向解码可确定原意分别接近“正在载入编辑器...”和“正在载入冒险...”。 |
| `src/components/AdventurePanel.tsx` | `99``101``103``109``111``113` | `查看库存与价<E4B88E>?``聊聊并试探口<E68EA2>?``看看能得到什么帮<E4B988>?``离开并继续探<E7BBAD>?``战斗决胜<E586B3>?``切磋几招看身<E79C8B>?` | NPC 交互短描述里有多处截断 / 乱码。 |
| `src/components/AdventurePanel.tsx` | `200``203` | `可作为制作材<E4BD9C>?``任务奖励物品可用于后续路线、交易或构筑规划<E8A784>?` | 任务奖励物品说明文本被截断。 |
| `src/components/AdventurePanel.tsx` | `569-571` | `已完<E5B7B2>?``已交<E5B7B2>?``进行<E8BF9B>?` | 任务状态标签乱码。 |
| `src/components/AdventurePanel.tsx` | `771` | `<60>?` | 对话气泡里的屏幕阅读器标签损坏。 |
| `src/components/AdventurePanel.tsx` | `833``837``870` | `剧情推演<E68EA8>?..``对话进行<E8BF9B>?``剧情推理完成继续后显示新的冒险选项<E98089>?` | 加载态 / 流式对话态 / 继续冒险提示都有截断。 |
| `src/components/CharacterDetailModal.tsx` | `35-36``223` | `女<>?``男<>?``属<>?` | 性别标签与“属性”标题乱码。 |
| `src/hooks/useStoryGeneration.ts` | `1214``1266``1409``1549``1978``2325` | `鏈煡 AI 閿欒` | 游戏故事流里 AI 失败时的统一兜底提示乱码;可反解为“未知 AI 错误”。 |
## 二、游戏 UI英文残留
### 1. 冒险面板和奖励弹层
- `src/components/adventure-panel/AdventurePanelOverlays.tsx`
- `554-570``Adventure stats``Current area:``ADVENTURE SUMMARY``enemies defeated``items in inventory``scene transitions so far`
- `622-668``Quest log``Total quests:``No active quests yet.`
- `711-798``QUEST BRIEF``Claim reward``QUEST COMPLETE``Reward ready``Reward pickup is now available in the quest log.``Open quest log`
- `887-1016``Battle reward``Defeated enemies:``BATTLE END``LOOT CACHE``Tap an item icon to inspect its details.``No usable loot dropped this time.``No loot dropped this time.``Rarity:``Quantity:``Slot:``Not equippable``Usable directly``Passive / non-immediate item``Effect preview: HP +``MP +``Cooldown -``Tags:``none`
- `src/components/AdventurePanel.tsx`
- `359-388``REWARD CACHE``Tap an item icon to inspect its details.``Affinity``Currency``No item bounty attached to this quest.`
- `636-638``Current area`
- `803``824`:两个按钮都显示 `Refresh`
### 2. 实体详情、同伴、交易
- `src/components/AdventureEntityModal.tsx`
- `892``898``HP``MP`
- `1073``NPC 信息`
- `1111``敌对NPC``NPC`
- `1163`:数量前缀 `x{item.quantity}`
- `1165``Inspect`
- `1428``NPC 背包`
- `src/components/CompanionCampModal.tsx`
- `177-178``233-234``HP``MP`
- `255``NPC`
- `src/components/NpcModals.tsx`
- `252``273``356``NPC 商品列表``这个 NPC 当前没有可售商品。``NPC 商品`
- `408``效果预览HP +... / MP +... / 冷却 -...`
- `src/components/CharacterDetailModal.tsx`
- `112``数量 x{item.quantity}`
### 3. 开场流程与加载态
- `src/components/game-shell/PreGameSelectionFlow.tsx`
- `48-49``QQ Group``WeChat`
- `81``89``核心NPC`
- `471-473``Wuxia Base`
- `519-527``Custom``Create Custom World``Enter a world setup and let the system generate playable characters, NPCs, items, and landmarks.`
- `src/components/game-shell/CharacterSelectionFlow.tsx`
- `401``Character Details`
- `406``Current Character`
- `src/components/GameCanvas.tsx`
- `32``Loading scene`
- `src/components/GameShell.tsx`
- `859``正在加载 NPC 交互...`
### 4. 运行时文案源头
- `src/data/sceneObservation.ts`
- `9-36` 整段观察结果仍是英文:
- `You pause to listen...`
- `Possible NPCs: ...`
- `Possible hostile NPCs: ...`
- `Possible treasure clues: ...`
- `Boss clue: ...`
- `src/hooks/useStoryGeneration.ts`
- `216``219`:最近战斗 / 最近协作提示仍是英文
- `639-646`:营地聊天结果文本混用了英文句子
- `662-667`:预览对话选项里仍有 `Speak with ...``Focus on the person in front of you first...`
## 三、编辑器 UI已确认乱码
### 1. 共享标签与世界名
- `src/components/preset-editor/shared.ts`
- `42``瑙掕壊`,可反解为 `角色`
- `44``鍦烘櫙`,可反解为 `场景`
- `45``鏁屽 NPC`,可反解为 `敌对 NPC`
- `46``鐗╁搧`,可反解为 `物品`
- `47``鍔熻兘`,可反解为 `功能`
- `53``姝︿緺`,可反解为 `武侠`
- `54``浠欎緺`,可反解为 `仙侠`
- `55``鑷畾涔変笘鐣?`,基本可判定原意是“自定义世界”,但当前字符串已经不完整
### 2. 角色预设面板
- `src/components/preset-editor/CharacterPresetPanel.tsx`
- `79`:空状态 / 顶部说明整段乱码
- `372-373`:装备区标题乱码
- `395-397`:背包区标题乱码
- `446-447`:技能预览相关标签乱码
- `475-477`:技能区提示乱码
- `590-592`:底部说明大段乱码
### 3. 场景 NPC 预设面板
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
- `87`:空状态整段乱码
- `267`:技能预览空态说明乱码
- `346`:角色 ID 标签乱码
- `352`:怪物预设 ID 标签乱码
- `389`:视觉编辑器说明整段乱码
### 4. 场景预设面板
- `src/components/preset-editor/ScenePresetPanel.tsx`
- `52`:空状态整段乱码
- `220-221`:敌对 NPC 分区标题乱码
- `254-255`:场景 ID 标签乱码
- `298-299`:怪物 ID 列表标签乱码
- `316-317`:关联 NPC 分区标题乱码
### 5. 怪物预设面板
- `src/components/preset-editor/MonsterPresetPanel.tsx`
- `53`:顶部说明整段乱码
## 四、编辑器 UI英文残留
### 1. 预设编辑器主面板
- `src/components/preset-editor/CharacterPresetPanel.tsx`
- `278-279``Character List``Choose a player character, preview it live, and edit the preset fields.`
- `325``Save Character Overrides`
- 多处通用占位仍是 `Section``Editor section.``Field`
- `347``Inventory World`
- `468-484``Skill Loadout``Add Skill`
- `510``Skill ID`
- `651``Character ID`
- `682``Asset Variant`
- `698``Personality`
- `713``Attributes``Adjust the four core character attributes.`
- `772``Unset`
- `798``scene-id-1 / scene-id-2`
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
- `181-182``NPC Library``Browse and select an NPC preset.`
- `186``NPC ID`
- `223``Save NPC Overrides`
- `230-246``Skill Preview``Preview ranged skills from the linked character.``Skill``World`
- `275-276``Hostile NPCs use monster presets...``Narrative NPCs can preview linked visuals...`
- `318-382``NPC Details``Role``Avatar``Initial Affinity``Description``Visual Editor`
- `src/components/preset-editor/ScenePresetPanel.tsx`
- `145``Scene`
- `172``Save`
- `179-193``Scene Preview``Preview Mode``Monster Preview``NPC Preview``Treasure Preview``Empty`
- `223``230``233``242``None``NPC`
- `248-272``Scene Details``World``Name``Description`
- `288``Unset`
- `src/components/preset-editor/MonsterPresetPanel.tsx`
- `172``Save Monster Overrides`
- `179-180``Monster Override Preview``Editor section.`
- `213-222``Attack Range:``Speed:``HP:``Max HP:`
- `236``Monster ID`
- `242``Name`
- `258``Intro Action`
- `373``FPS`
### 2. 其他编辑器 / 自定义世界
- `src/components/ItemCatalogEditor.tsx`
- `654``HP``MP``CD`
- `677``806``ID`
- `789``Build Buff`
- `458``863``public/Icons``itemOverrides.json`
- `src/components/NpcVisualEditor.tsx`
- `463``702``708``718``NPC`
- `977``Shift`
- `1028-1052``Current loadout:``Unknown headgear``No headgear``Unknown main hand``No main hand``Unknown off hand``No off hand`
- `src/components/CustomWorldEntityCatalog.tsx`
- `139``268``276``349``NPC`
- `224``WORLD DOSSIER`
- `346``MedievalFantasyCharacters`
- `src/components/CustomWorldEntityEditorModal.tsx`
- `242``URL`
- `460``MedievalFantasyCharacters`
- `478-479``730``758-759``AI``AI生成NPC形象``AI生成场景`
- `631-653``NPC`
- `src/components/PresetEditor.tsx`
- `71`:介绍文案里仍然直接显示 `NPC`
- `src/components/StateFunctionEditor.tsx`
- `803``Failed to play preview`
- `818-821``HP``No visible target`
- `915``n/a`
- `1060-1064``Failed to save option behavior overrides``Option behavior overrides saved.`
- `1106`:直接显示原始 `state`
- `1185`:直接把 `AnimationState` 值作为 label
- `1191`:敌对 NPC 反应动画里仍直接显示 `idle` / `move` / `attack`
- `1217`:技能风格仍直接依赖 `steady` / `burst` / `mobility` / `finisher` / `projectile`
## 五、预设 / 数据层:会直接透出 UI 的英文原始值
这一部分不是“源码内部英文就算问题”,而是“当前编辑器或预览没有做显示映射,导致原始英文值直接露给用户”。
### 1. 角色预设
- `src/data/characterPresets.ts`
- 动作文件夹 / 前缀仍是英文:
- `363-379`
- `520-536`
- `739-755`
- `834-850`
- `1019-1038`
- 对话风格原始值仍是英文:
- `384-386``blunt` / `dry` / `direct`
- `541-543``wary` / `dry` / `fragmented`
- `760-762``blunt` / `teasing` / `deflecting`
- `855-857``blunt` / `steady` / `direct`
- `1043-1045``measured` / `steady` / `fragmented`
- 技能风格 / 投射方式仍是英文:
- `407-408``445``473-474`
- `572-573``604-605``636-637``668-669``700-701`
- `791-792``815-818`
- `886``906``926-927``958-959``990-991`
- `1066-1077``1109-1110``1133-1146``1178-1179`
- 这些值当前会在角色预设编辑器、技能预览和部分行为预览里直接露出。
### 2. 物品设计 / Build 标签
- `src/data/itemDesign.ts`
- `56-201``worldAffinity` / `role` / `rarity` 原始值仍是英文,如 `neutral``wuxia``xianxia``fieldcraft``breaker``berserker``legendary`
- `213-219``pieceName` 仍是 `boots``chest``gloves``helm``leggings``shield``weapon`
- `820`:说明文本里仍混入 `build`
- `src/data/buildTags.ts`
- `11-291`:整套 build tag id 都是英文,如 `quickblade``combo``dash``ranged``burst``caster``vanguard``paladin``starter`
- 这些值会进入物品编辑器、构筑标签和相关预览。
### 3. 怪物与掉落
- `src/data/monsterPresets.ts`
- `490-736`:掉落 id、稀有度、tag 原始值大量是英文,如 `rare``uncommon``armor``material``relic``healing`
- `718-736`:有两条掉落本身是完整英文可见值:
- `Consumable` / `Thorn Nectar` / `Sticky sap that can be refined into emergency recovery tonic.`
- `Relic` / `Devour Bloom` / `A predatory blossom that stores concentrated life force.`
### 4. 场景 / 行为 / 锻造 / NPC 交互
- `src/data/scenePresets.ts`
- `349-651`:场景 id 全部是英文连字符格式,如 `wuxia-bamboo-road``xianxia-cloud-gate`
- 当前在编辑器 ID 字段中会直接显示。
- `src/data/stateFunctions.ts`
- `113-372``category` 原始值仍是 `battle` / `recovery` / `escape` / `idle`
- 编辑器预览还会直接显示动画 / delivery 原始值。
- `src/data/forgeSystem.ts`
- `264`:描述里混入 `build`
- `274-281``relic``epic``setId``pieceName` 等原始值会进入物品编辑器链路
- `src/data/npcInteractions.ts`
- `207-209`:兜底对话风格仍是 `measured` / `steady` / `fragmented`
## 六、建议修复顺序
1. 先修最影响主流程观感的真实乱码。
- `src/routing/appRoutes.tsx`
- `src/components/AdventurePanel.tsx`
- `src/components/CharacterDetailModal.tsx`
- `src/hooks/useStoryGeneration.ts`
2. 再修预设编辑器的共享标签和 4 个拆分面板。
- `src/components/preset-editor/shared.ts`
- `src/components/preset-editor/CharacterPresetPanel.tsx`
- `src/components/preset-editor/SceneNpcPresetPanel.tsx`
- `src/components/preset-editor/ScenePresetPanel.tsx`
- `src/components/preset-editor/MonsterPresetPanel.tsx`
3. 再统一清理英文残留。
- 游戏端优先:`AdventurePanelOverlays.tsx``AdventureEntityModal.tsx``PreGameSelectionFlow.tsx`
- 编辑器端优先:`ItemCatalogEditor.tsx``NpcVisualEditor.tsx``StateFunctionEditor.tsx`、自定义世界编辑器
4. 最后做“显示层映射”,避免预设原始英文继续漏到 UI。
- `characterPresets.ts`
- `itemDesign.ts`
- `buildTags.ts`
- `monsterPresets.ts`
- `scenePresets.ts`
- `stateFunctions.ts`
## 七、备注
- 本次结论以当前源码为准,和旧审计文档相比,已有一部分旧问题已经被修掉。
- `src/components/preset-editor/PresetEditorPanels.tsx` 现在只是 re-export 壳文件,真正的问题已经分散到拆分后的 panel 文件里。
- `src/components/preset-editor/shared.ts` 里的几处乱码已经可以明确反解,适合优先直接修正。
- `src/data/` 中很多英文值本身可能是内部枚举,但只要当前编辑器 / 预览没有做中文映射,就仍应视为“会暴露到用户侧”的文本问题。

View File

@@ -1,6 +1,6 @@
# 文本与乱码审计总览 # 文本与乱码审计总览
这一组文档记录的是同一条清理链路的不同阶段:从“发现哪里有英文/乱码”到“扩展到 prompt、npcInteraction、编辑器深层文本” 这一组只保留当前仍需要执行的文本与乱码审计入口。早期逐日扫描已经聚合到当前结论,不再保留旧稿链路
## 当前推荐入口 ## 当前推荐入口
@@ -11,17 +11,8 @@
3. [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md) 3. [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-02.md)
适合看“扩展重查版”的 UI / 预设 / 编辑器问题面。 适合看“扩展重查版”的 UI / 预设 / 编辑器问题面。
## 历史时间线
- [EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md](./EDITOR_GAME_PRESET_TEXT_AUDIT_2026-03-25.md):较早期的整体首轮盘点。
- [GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md](./GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-29.md):复查阶段,开始收紧范围和口径。
- [GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md](./GAME_EDITOR_PRESET_TEXT_AUDIT_2026-03-30.md):继续复核真实乱码与英文残留。
- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-30_CONTINUED.md):对上一轮的续扫补充。
- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-03-31.md):继续收敛 UI、预设与编辑器问题。
- [GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md](./GAME_UI_PRESET_EDITOR_TEXT_AUDIT_2026-04-01.md):进入更明确的审计范围与方法阶段。
## 融合结论 ## 融合结论
- 早期几份文档主要负责“摸清哪里有问题” - 早期几轮扫描已经完成使命,核心结论是:不要把乱码当成普通文案改写,先确认真实编码;文本修复要优先处理会进入玩家体验和 AI 生成链路的内容
- `2026-04-02` 两份文档开始把重点收敛到真正会影响玩家体验和 AI 生成质量的链路。 - `2026-04-02` 两份文档开始把重点收敛到真正会影响玩家体验和 AI 生成质量的链路。
- 现在做文本修复时,不必从最早一份开始逐篇读;优先看 `CHINESE_MOJIBAKE_INVENTORY``2026-04-02` 两份即可。 - 现在做文本修复时,不必从最早一份开始逐篇读;优先看 `CHINESE_MOJIBAKE_INVENTORY``2026-04-02` 两份即可。

View File

@@ -259,7 +259,7 @@
可复用来源: 可复用来源:
- `README.md` - `README.md`
- `docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md` - `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` - `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
### 5.2 技术研发情况 ### 5.2 技术研发情况

View File

@@ -101,8 +101,8 @@
- `docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md` - `docs/prd/AI_NATIVE_CUSTOM_WORLD_CREATION_FLOW_OPTIMIZATION_PRD_2026-04-06.md`
- `docs/prd/AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md` - `docs/prd/AI_NATIVE_STORY_ENGINE_PHASE1_IMPLEMENTATION_PLAN_2026-04-06.md`
- `docs/prd/AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md` - `docs/prd/AI_NATIVE_STORY_ENGINE_PHASE2_IMPLEMENTATION_PLAN_2026-04-06.md`
- `docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md` - `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
- `docs/planning/CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md` - `docs/audits/engineering/README.md`
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md` - `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
## 5. 三个方向的共用材料包 ## 5. 三个方向的共用材料包

View File

@@ -1,293 +0,0 @@
# 当前游戏优先迭代清单2026-04-03
## 结论先说
当前阶段最不该做的,是继续零散加玩法、加场景、加文案,却让主链路、规则底座和工程门禁继续处在半完成状态。
按现有文档和代码状态看,建议优先级顺序如下:
1. `P0`:先恢复工程绿色基线,并把运行时主链路继续拆开
2. `P1`:再落统一角色属性底座,作为战斗 / 对话 / 招募 / Build / 掉落 / 任务的共同语义基础
3. `P1`:在统一属性底座之上,重做 Build、运行时物品奖励、任务系统三条核心玩法链
4. `P2`:最后收尾编辑器共享层、本地 API 分层、移动端体验与运行时包体优化
一句话判断:
**现在的优先级不是“继续扩玩法宽度”,而是“先把底层规则、主流程边界和工程可维护性补齐,再扩玩法深度”。**
补充更新(`2026-04-21`
当前与“主流程边界补齐”直接对应的执行基线,已经从泛化的 `GameShell / useStoryGeneration / customWorld` 热点讨论,收口成两条正式技术方案:
1. [../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md):负责创作入口 -> Agent session -> result preview -> published profile 主链。
2. [../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md):负责平台入口 -> 继续游戏/开始游戏 -> RPG runtime -> runtime story 主链。
因此本文里的 `P0-2`,当前应按这两条主线落地,而不是继续围绕旧 `GameShell` / `PreGameSelectionFlow` / `runtimeRoutes.ts` 命名做泛化式重构。
---
## 优先级清单
## P0-1恢复绿色基线收紧质量门禁
### 为什么必须排第一
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 已明确指出:当前最值得优先优化的不是继续加功能,而是把“半完成的工程化”补齐。
- 文档中提到过 `lint` 失败、`build` warning、核心热区文件被 ESLint ignore、部分测试未进入默认套件这意味着当前代码库还不在真正稳定的绿色基线。
- 在这种状态下继续叠加新玩法,只会把问题扩散到更多运行时链路和编辑器链路。
### 本阶段要做什么
- 修复现有 `lint` / `build warning` / 明确可见的门禁破口
- 缩小高风险核心文件的 ignore 范围
-`lint + typecheck + test + build + check:content` 成为可信的统一门禁
- 对 warning 建立“尽快清零”策略,而不是长期带病开发
### 做到什么算完成
- 主开发分支长期保持 `npm run check` 可稳定通过
- 核心运行时文件不再依赖长期 ignore 才能过门禁
- 构建 warning 收敛到零或有非常明确的短期处理计划
### 为什么它比新功能更优先
- 没有绿色基线,后续所有大改都缺少可靠回归保护
- 这一步是后面统一属性、任务重构、物品系统重构的前置条件
---
## P0-2继续拆运行时主链路防止核心 hook 和壳层继续膨胀
### 为什么必须紧跟在 P0-1 后面
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md``docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md``docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md` 都反复强调:这个项目是叙事、状态、演出、界面四条链路耦合的复合项目,不能靠大文件硬扛。
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md``docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 一致指出,`useStoryGeneration``useCombatFlow``GameShell` 仍然是当前最大的复杂度集中点。
- 如果不先拆主链,后面的统一属性系统、任务系统、物品导演层都会继续堆进现有巨型流程控制器,技术债只会翻倍。
### 本阶段要做什么
- 按 [../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md) 收口创作链,统一 `Agent session -> result preview -> published profile`
- 按 [../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md) 收口 `rpgEntry -> rpgSession -> rpgRuntime -> rpgRuntimeStory -> rpgProfile`
-`GameShell / PreGameSelectionFlow / runtimeRoutes.ts / storyActionService.ts` 只作为历史热区名或兼容 façade不再作为当前新任务默认落点
### 做到什么算完成
- 新功能接入时,不需要再跨 `story + combat + panel + modal` 四五层一起改
- 核心流程可以按领域补测试,而不是只能做人工回归
- 后续玩法扩展能优先加领域模块,而不是继续往大 hook 里塞逻辑
### 这一项的实际意义
- 这是“后续还能继续做大”的结构前提
- 不做这一步,任何系统升级都会越来越难落地
---
## P1-1落地统一角色属性系统作为全玩法共同底座
### 为什么它是最优先的玩法底座
- `docs/prd/AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md` 已经把问题说得很清楚当前玩家、NPC、怪物、Build、对话、掉落还没有共享同一套解释坐标。
- 当前项目已经有 NPC 关系、怪物标签、Build 语义、自定义世界生成能力,但这些系统之间还缺一套统一的世界级属性 schema。
- 如果先做任务、物品、Build 深化,而不先统一属性,后面很容易再次出现“每个系统各自解释角色”的分裂。
### 本阶段要做什么
- 为预设世界固化世界级属性 schema
- 为玩家角色、怪物、关键 NPC 补 `attributeProfile`
- 建立统一的属性解析与校验层
- 先让对话 / 招募 / 送礼 / 详情面板开始读取这套新属性解释
### 做到什么算完成
- 玩家、NPC、怪物都能落到同一套属性语义里
- 聊天、送礼、招募至少有一条链可以直接解释到属性层
- 自定义世界也能生成并持久化自己的属性 schema
### 为什么这项优先于“多做内容”
- 这是后面 Build、物品、任务三条系统统一升级的共同前提
- 没有这层底座,玩法会继续“能跑,但彼此不共语义”
---
## P1-2把 Build 系统从“标签互相影响”改成“标签匹配角色属性”
### 为什么这里要尽快做
- `docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md` 指出:当前 Build 更像标签网络效应,解释成本高、平衡成本高、角色差异感不够强。
- 一旦统一角色属性系统先落地Build 就是最适合第二个接入的玩法层,因为它最直接影响战斗反馈和角色成长感。
### 本阶段要做什么
- 为 Build 标签补属性亲和度向量
- 改写 `buildDamage` 逻辑,让每个标签独立匹配当前角色属性画像
- 调整 Build 面板文案,从“标签协同”转成“属性适配度”
### 做到什么算完成
- 玩家能理解“为什么这个标签适合当前角色”
- 新增标签只影响自身贡献,不再扰动整张标签网络
- Build 面板能解释收益来自哪些属性
### 实际收益
- 提高可解释性
- 降低平衡难度
- 让角色差异感真正进入 Build 体验
---
## P1-3重做运行时物品奖励让奖励真正贴合场景、NPC、最近事件和 Build 缺口
### 为什么它值得排在任务系统前面
- `docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md` 明确指出当前宝藏、NPC、任务、锻造等入口都有物品但缺少统一导演层奖励与场景/NPC/事件的贴合度不够高。
- 相比任务系统,运行时物品奖励能更快提升“世界贴脸感”和“当下反馈质量”,且可以先从宝藏入口低风险落地。
### 本阶段要做什么
- 增加运行时物品上下文采样、导演层、编译器和叙事回写层
- 统一宝藏、NPC 奖励、怪物掉落、任务奖励的物品生成入口
- 让奖励优先围绕 Build 标签、限时 Build Buff、少量数值补足来设计
### 做到什么算完成
- 至少宝藏和 NPC 奖励接入统一导演层
- 物品能解释“为什么在这里出现、和谁有关、补的是什么方向”
- 物品来源可以进入背包、剧情、锻造与存档的同一套结构
### 实际收益
- 奖励不再像泛用掉落池
- 世界、人物、最近剧情与成长反馈终于真正连起来
---
## P1-4把任务系统从“单目标单阶段”升级成“意图 -> 合约 -> 信号推进”
### 为什么它仍然是高优先级
- `docs/prd/AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md` 已经指出:当前任务闭环是成立的,但任务来源偏静态、结构偏扁平、状态过粗、奖励和关系变化也不够贴语境。
- 当前项目已经具备任务 UI、任务奖励、NPC 交互、剧情推进链,这说明任务系统适合做“升级”,而不是推倒重来。
### 本阶段要做什么
- 新增任务生成上下文、AI 任务意图层、本地任务编译层
- 把任务推进改成统一 signal 驱动
- 支持多 step、阶段揭示、完成后回报、后续钩子
### 做到什么算完成
- NPC 接任务不再只是静态模板,而是能根据当前局面生成任务意图
- 运行时能用统一 signal 推进任务步骤
- 奖励除了货币/道具,还能自然进入关系、情报、后续机会
### 为什么它排在物品系统之后
- 任务系统耦合更深,适合作为统一属性和统一奖励导演层之后的升级项
- 先把属性和物品奖励理顺,任务系统落地时会更稳
---
## P2-1收尾编辑器共享层与本地 API 分层,让内容扩张不再继续拖慢主项目
### 为什么它不是最前面,但也不能拖太久
- 最近几份工程审查都指出:编辑器共享层、本地 JSON 写入接口、LLM 代理、Vite 插件职责仍然处于迁移中间态。
- 当前项目已经进入“内容工具很多、正式运行时也很重”的阶段,若不收尾这部分,后续每次扩内容都会重复踩基础设施问题。
### 本阶段要做什么
- 继续拆 editor shared 层
- 清理迁移残留和死分支
- 把本地 API 至少按 `llm proxy / json editor api / asset catalog` 分职责拆开
### 做到什么算完成
- 编辑器保存、共享组件、共享 client 不再重复实现
- 本地 API 分工清晰dev / preview 边界清楚
- 编辑器扩展不再继续依赖大聚合组件
---
## P2-2继续优化移动端冒险体验、首屏信息密度与运行时包体
### 为什么它放在 P2
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md``docs/experience/MOBILE_UI_DEV_EXPERIENCE.md` 都强调:冒险页必须优先保证上方演出、一屏选项和文本区自适应。
- 但从当前文档判断,移动端体验和包体问题更像“持续治理项”,不是当前阶段最核心的系统阻塞点。
### 本阶段要做什么
- 继续优化冒险页一屏布局与文本滚动策略
-`GameCanvas``AdventurePanel` 等高热区大模块
- 按真实交互热区继续做 chunk 拆分
### 做到什么算完成
- 手机首屏稳定容纳画布、文本和关键选项
- 核心页面热区模块更容易维护和测试
- 构建产物中的主 chunk 有持续下降趋势
---
## 不建议当前优先做的事
以下内容不是不能做,而是不建议排在当前这轮前面:
- 大量新增世界、场景、角色 preset
- 继续横向扩 NPC 交互种类,但不补统一规则底座
- 继续堆宝藏、掉落、锻造分支,但不先做统一物品导演层
- 继续增加任务模板数量,但不升级任务 contract
- 继续往 `useStoryGeneration` / `useCombatFlow` / `GameShell` / `PreGameSelectionFlow` / `runtimeRoutes.ts` / `storyActionService.ts` 里直接塞新逻辑
原因很简单:
**这些工作会让表面内容变多,但不会让项目变得更稳,反而会放大当前已经存在的结构问题。**
---
## 推荐迭代顺序
### 第一阶段:先稳住工程与主流程
1. 绿色基线与门禁收紧
2. 创作链按 `CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 收口
3. RPG 进入游戏与运行时链按 `RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md` 收口
### 第二阶段:先补统一语义底座
1. 统一角色属性系统
2. Build 改为属性适配
### 第三阶段:再深化 AI 原生玩法闭环
1. 运行时物品导演层
2. 任务意图与 contract 系统
### 第四阶段:最后做工具与体验收尾
1. 编辑器共享层 / 本地 API 分层
2. 移动端体验与包体优化
---
## 本清单的主要依据
- `docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md`
- `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
- `docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md`
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md`
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md`
- `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md`
- `docs/design/AI_NATIVE_RUNTIME_ITEM_SYSTEM_REDESIGN_2026-04-02.md`
- `docs/prd/AI_NATIVE_UNIFIED_ROLE_ATTRIBUTE_SYSTEM_PRD_2026-04-02.md`
- `docs/prd/BUILD_SYSTEM_ATTRIBUTE_SIMILARITY_PRD_2026-04-02.md`
- `docs/prd/AI_NATIVE_QUEST_SYSTEM_PRD_2026-04-02.md`
## 最后结论
如果只保留一句话,那就是:
**当前最优先的迭代方向不是继续堆新内容而是先把工程基线、主流程边界和统一规则底座补齐只有这样AI 原生任务、物品、Build 和后续内容扩展才会真正开始越做越顺。**

View File

@@ -139,10 +139,10 @@ Git 分支治理可以后置做,但不能和首轮工程清洗混在一起,
本计划基于现有文档已经确认的结论推进,重点参考: 本计划基于现有文档已经确认的结论推进,重点参考:
1. `docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-04-01.md` 1. `docs/audits/engineering/README.md`
2. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md` 2. `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-20.md`
3. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md` 3. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md`
4. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md` 4. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
5. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` 5. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md`
按当前审计结果,首轮就应重点关注下面 3 组对象。 按当前审计结果,首轮就应重点关注下面 3 组对象。

View File

@@ -1,588 +0,0 @@
# Express 后端化并行任务拆分规划2026-04-08
## 1. 目的
这份文档用于把 [EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md](./EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md) 进一步拆成可并行推进、尽量互不冲突的任务流。
目标不是把大重构拆成很多零碎 TODO而是把它拆成
- 可以同时开工
- 写入边界清晰
- 交付物明确
- 依赖关系稳定
- 最后容易集成
---
## 2. 并行拆分原则
## 2.1 基本原则
- 每条任务尽量拥有独占目录或独占模块,不去抢同一批热点文件。
- 热点集成文件只由“集成岗”或最后一轮集成处理,不作为多个任务的日常编辑目标。
- 先搭协议边界,再迁规则执行,再收缩前端。
- 前端与后端可以并行推进,但前提是先冻结 contract。
- 编辑器链路和正式运行时链路分开拆,避免互相阻塞。
## 2.2 当前最容易冲突的文件
以下文件建议默认只由集成岗或最后一轮联调处理:
- `server-node/src/context.ts`
- `server-node/src/routes/runtimeRoutes.ts`
- `server-node/src/app.ts`
- `src/services/apiClient.ts`
- `src/hooks/useStoryGeneration.ts`
- `src/hooks/useGameFlow.ts`
- `src/components/GameShell.tsx`
其他任务如果必须影响这些文件,优先通过:
- 新增独立模块
- 新增 adapter
- 新增中间层入口
而不是直接在热点文件中大改。
---
## 3. 建议并行批次
## 批次 A可立即并行开工
- 任务 0集成岗与接口冻结
- 任务 1共享 contract 与目录抽离
- 任务 2PostgreSQL 持久化基线收口
- 任务 3服务端 HTTP 基础设施与统一响应壳层
- 任务 8编辑器 API 归口与工具链隔离
- 任务 9测试、观测与部署基线
## 批次 B在 contract 初版落地后并行开工
- 任务 4服务端 AI 编排收口
- 任务 5运行时领域模块 AStory / Combat / NPC
- 任务 6运行时领域模块 BInventory / Quest / Build / Runtime Item
- 任务 7前端 SDK、鉴权、持久化瘦身
## 批次 C在服务端 action 和 view model 稳定后开工
- 任务 10前端主流程壳层与大 hook 瘦身
---
## 4. 任务拆分
## 任务 0集成岗与接口冻结
### 目标
负责冻结边界、维护接口文档、控制热点文件的合并节奏,避免多人同时改核心入口。
### 独占范围
- `docs/planning/**`
- `docs/technical/**`
- 最终集成时的热点入口文件
### 主要输出
- 统一任务看板
- contract 版本表
- 热点文件编辑规则
- 每日或每阶段集成清单
### 验收标准
- 团队知道哪些文件不能多人同时改
- 每条任务都有明确的上游 contract 与下游接入点
---
## 任务 1共享 Contract 与目录抽离
### 目标
先把前后端共同识别的类型、schema、响应结构、错误结构抽出来切断 `server-node -> src/**` 的长期反向依赖。
### 独占范围
- `packages/shared/**`
- 新建的共享类型、schema、contract 目录
### 可改边界
- `server-node/src/**` 中的 import 替换入口
- `src/**` 中的 import 替换入口
### 暂不负责
- 具体业务规则迁移
- 前端页面行为调整
- 数据库实现细节
### 主要输出
- 统一 API envelope
- 统一错误对象
- 统一 action / response contract
- 统一领域类型和状态枚举
### 验收标准
- 新增服务端模块不需要继续直接依赖前端目录里的实现细节
- 前后端都以共享 contract 为边界协作
### 并行关系
- 可与任务 2、任务 3、任务 8、任务 9 同时启动
- 是任务 4、任务 5、任务 6、任务 7 的上游基础
---
## 任务 2PostgreSQL 持久化基线收口
### 目标
把“已经切到 PostgreSQL”的状态收成真正稳定的后端基线清掉 SQLite 残留口径与仓储层耦合问题。
### 独占范围
- `server-node/src/config.ts`
- `server-node/src/db.ts`
- `server-node/src/repositories/**`
- `server-node/src/app.test.ts`
- `.env.example`
### 暂不负责
- 剧情规则
- 选项结算
- 前端状态瘦身
### 主要输出
- PostgreSQL 连接配置
- 仓储层接口统一
- 数据表初始化/迁移方案
- 运行时持久化测试基线
- 文档中的数据库现状统一
### 验收标准
- 后端运行时数据完全以后端数据库为准
- 配置、日志、测试、文档里不再把 SQLite 写成当前正式现状
### 并行关系
- 可与任务 1、任务 3、任务 8、任务 9 同时启动
- 为任务 5、任务 6、任务 7 提供稳定持久化基础
---
## 任务 3服务端 HTTP 基础设施与统一响应壳层
### 目标
建立统一的服务端响应结构、错误结构、请求链路日志、版本字段和中间件壳层。
### 独占范围
- `server-node/src/http.ts`
- `server-node/src/errors.ts`
- `server-node/src/middleware/**`
- `server-node/src/app.ts`
### 可改边界
- 为 route 层提供新的响应 helper
- 为后续 action 接口提供统一 envelope
### 暂不负责
- 具体 story / combat / quest 业务逻辑
- 前端页面层接入
### 主要输出
- 统一 JSON 响应格式
- 统一错误格式
- `requestId`
- `latency` 与关键日志字段
- 路由级版本与元信息壳层
### 验收标准
- 后端所有新接口都能套用同一层响应约定
- 前端不需要为不同接口写多套错误解析逻辑
### 并行关系
- 可与任务 1、任务 2、任务 8、任务 9 同时启动
- 是任务 4、任务 5、任务 6、任务 7 的共同基础
---
## 任务 4服务端 AI 编排收口
### 目标
把正式运行时的 prompt 组装、模型调用、容错、SSE 转发都收回后端,浏览器不再保留正式运行时 AI fallback。
### 独占范围
- `server-node/src/services/llmClient.ts`
- `server-node/src/services/chatService.ts`
- `server-node/src/services/storyService.ts`
- `server-node/src/services/customWorldGenerationService.ts`
- `server-node/src/services/questService.ts`
- `server-node/src/services/runtimeItemService.ts`
### 可新增目录
- `server-node/src/modules/ai/**`
### 暂不负责
- 前端主流程组件
- 数据库存储实现
### 主要输出
- 后端统一 AI orchestration 层
- 流式接口统一适配
- prompt 复用策略
- 前端 fallback 清理清单
### 验收标准
- 正式运行时不再依赖浏览器端大体量 AI 实现作为兜底
- AI 失败、超时、流式中断都能在后端统一处理
### 并行关系
- 建议在任务 1、任务 3 有初版后启动
- 可与任务 5、任务 6、任务 7 并行
---
## 任务 5运行时领域模块 AStory / Combat / NPC
### 目标
把剧情推进、战斗结算、NPC 交互这些最核心的运行时状态迁移到后端领域模块。
### 独占范围
- `server-node/src/modules/story/**`
- `server-node/src/modules/combat/**`
- `server-node/src/modules/npc/**`
### 可改边界
- 为 route/action 层提供服务接口
- 为前端提供 view model 所需聚合结果
### 暂不负责
- 背包、Build、任务奖励编排
- 编辑器接口
### 主要输出
- story action resolver
- combat resolution service
- npc interaction service
- 统一返回给 UI 的 presentation/view model 结构
### 验收标准
- 前端不再本地决定 function 合法性、战斗结果、NPC 关键关系变化
- 点击选项时,后端能返回完整下一步展示结果
### 并行关系
- 依赖任务 1、任务 3
- 可与任务 4、任务 6、任务 7 并行
---
## 任务 6运行时领域模块 BInventory / Quest / Build / Runtime Item
### 目标
把任务推进、运行时物品、背包/装备、Build 收益等剩余核心规则迁到后端。
### 独占范围
- `server-node/src/modules/inventory/**`
- `server-node/src/modules/quest/**`
- `server-node/src/modules/build/**`
- `server-node/src/modules/runtime-item/**`
### 可改边界
- 调用任务 2 的仓储层
- 使用任务 1 的共享 contract
### 暂不负责
- 前端页面层改造
- Story / Combat / NPC 主链路
### 主要输出
- inventory mutation service
- quest signal progression service
- build calculation service
- runtime item resolution service
### 验收标准
- 背包、任务、Build、运行时物品不再由前端保留正式结算逻辑
- 这些领域能独立测试,不依赖 UI hook
### 并行关系
- 依赖任务 1、任务 2、任务 3
- 可与任务 4、任务 5、任务 7 并行
---
## 任务 7前端 SDK、鉴权、持久化瘦身
### 目标
让前端从“业务执行层”退回“API 消费层 + 表现层状态协调层”。
### 独占范围
- `src/services/apiClient.ts`
- `src/services/authService.ts`
- `src/services/storageService.ts`
- `src/services/aiService.ts`
- `src/hooks/useGamePersistence.ts`
- `src/hooks/useGameSettings.ts`
### 暂不负责
- 页面组件大范围重构
- `useStoryGeneration.ts` 主流程瘦身
### 主要输出
- 轻量前端 SDK
- 统一鉴权请求层
- 统一错误态与重试策略
- 远端快照/设置消费层
- 正式运行时浏览器 fallback 下线方案
### 验收标准
- 前端服务层不再保留完整正式规则或正式 AI 编排
- 存档与设置以后端返回结果为准
### 并行关系
- 依赖任务 1、任务 3
- 可与任务 4、任务 5、任务 6 并行
- 为任务 10 提供稳定的 API 消费层
---
## 任务 8编辑器 API 归口与工具链隔离
### 目标
把编辑器的写盘、生成、任务查询能力从“散落接口”整理成清晰的编辑器后端模块,避免继续污染正式运行时。
### 独占范围
- `src/editor/shared/**`
- `src/components/preset-editor/**`
- `src/components/npcVisualEditorPersistence.ts`
- `src/components/preset-editor/characterAssetStudioPersistence.ts`
- `scripts/dev-server/**`
- `server-node/src/modules/editor/**`
- `server-node/src/modules/assets/**`
### 暂不负责
- 主游戏运行时 action 逻辑
- 正式剧情流转
### 主要输出
- `/api/editor/*``/api/assets/*` 命名空间
- 统一 editor client SDK
- 写接口权限边界
- 编辑器工具链迁移清单
### 验收标准
- 编辑器组件不再散落直连多个写接口
- 编辑器 API 与运行时 API 的职责边界清晰
### 并行关系
- 可与任务 1、任务 2、任务 3、任务 9 同时启动
- 与任务 5、任务 6、任务 10 基本不冲突
---
## 任务 9测试、观测与部署基线
### 目标
为整个后端化改造提供自动回归、链路日志和部署基线,避免“功能迁过去了但不可验证”。
### 独占范围
- `server-node/src/**/*.test.ts`
- `scripts/**`
- 部署与运维相关文档
- 反向代理与 smoke 测试脚本
### 暂不负责
- 具体业务模块实现
### 主要输出
- 后端接口测试
- 关键主链路 smoke
- request/response 日志校验
- 同域部署基线
- 回滚、备份、迁移检查清单
### 验收标准
- `web + server` 改造过程有最小自动回归保护
- 关键接口失败时能追踪到请求链路
### 并行关系
- 可与任务 1、任务 2、任务 3、任务 8 同时启动
- 后续持续跟进任务 4、任务 5、任务 6、任务 7、任务 10 的交付
---
## 任务 10前端主流程壳层与大 Hook 瘦身
### 目标
在服务端 action 和前端 SDK 稳定后,把 `GameShell``useStoryGeneration` 这一层改成真正的表现层协调器。
### 独占范围
- `src/hooks/useStoryGeneration.ts`
- `src/hooks/story/**`
- `src/hooks/useGameFlow.ts`
- `src/components/GameShell.tsx`
- `src/components/AdventurePanel.tsx`
- `src/components/NpcModals.tsx`
- `src/components/auth/**`
### 暂不负责
- 数据库、服务端仓储
- 编辑器 API
### 主要输出
- 面向 action/view model 的前端流程层
- 页面表现态与业务态分离
- 大 hook 拆分后的协调层
- 更容易测试和替换的主流程壳层
### 验收标准
- 前端主流程不再直接吞下完整运行时规则
- 页面层主要消费后端 view model而不是本地自算结果
### 并行关系
- 依赖任务 5、任务 6、任务 7 至少有一轮稳定输出
- 是最后一批大规模前端接入任务
---
## 5. 推荐协作顺序
## 第一步:先定边界
先启动:
- 任务 0
- 任务 1
- 任务 2
- 任务 3
这一轮完成后,团队会得到:
- 统一 contract
- 稳定数据库基线
- 稳定后端响应壳层
- 稳定任务分工边界
## 第二步:领域层和工具层分头推进
在第一步基础上并行启动:
- 任务 4
- 任务 5
- 任务 6
- 任务 7
- 任务 8
- 任务 9
这一轮是整个改造的主生产阶段。
## 第三步:最后收前端主流程
最后启动:
- 任务 10
原因很简单:
- 如果太早改 `useStoryGeneration``GameShell`,前端还没有稳定的 action contract 和 view model会反复返工。
---
## 6. 建议的多人分工方式
如果是 4 人并行,建议:
- 1 人负责任务 1 + 任务 0 的 contract/集成
- 1 人负责任务 2 + 任务 3 的后端基建
- 1 人负责任务 4 + 任务 5 的运行时主链
- 1 人负责任务 8 + 任务 9之后转入任务 7 或任务 10
如果是 6 人并行,建议:
- 1 人负责任务 0 + 任务 1
- 1 人负责任务 2
- 1 人负责任务 3 + 任务 9
- 1 人负责任务 4
- 1 人负责任务 5
- 1 人负责任务 6 + 任务 8
前端主流程任务 10 建议在第二轮由最熟悉当前 UI 壳层的人接手。
---
## 7. 合并规则建议
- 每条任务优先新增目录和新模块,少直接改热点文件。
- 热点文件统一在集成窗口合并,不在多个任务里同步推进。
- 任何任务如果需要改 `useStoryGeneration.ts`,默认先暂停并和任务 10 对齐。
- 任何任务如果需要改 `server-node/src/routes/runtimeRoutes.ts`,默认先走任务 0 的接口冻结表。
- 编辑器链路和正式运行时链路不要混在同一个 PR 里。
---
## 8. 一句话结论
这次重构最稳的并行方式不是“大家一起改前后端”,而是:
**先用 contract、数据库基线和 HTTP 壳层把边界钉死,再让服务端领域迁移、编辑器归口、前端瘦身分轨并行,最后由主流程壳层统一接入。**

View File

@@ -1,447 +0,0 @@
# Express 后端化工程重构规划2026-04-08
## 1. 背景
当前项目已经引入 `Express` 后端,且 `server-node/` 已经承接了运行时鉴权、存档、设置、自定义世界、剧情生成、角色聊天、NPC 对话、运行时物品意图、任务生成等能力。
但从当前工程状态看,项目仍处于“后端已存在,但运行时领域层尚未完全脱前端”的过渡态,主要表现为:
- 前端 `src/hooks/useStoryGeneration.ts` 仍然承担了大量运行时编排、规则拼接与状态推进职责。
- 前端 `src/services/ai.ts` 仍然保留了完整的 AI 调用、提示词拼装和本地兜底实现。
- 前端 `src/hooks/useGamePersistence.ts` 仍在承担较重的存档恢复、schema 纠偏与归一化职责。
- `server-node/src/**` 当前仍在直接引用 `src/types``src/data``src/services` 中的内容,分层尚未真正闭合。
- 编辑器相关写接口仍然散落在前端组件与 `jsonClient` 中,运行时 API 与编辑器 API 还没有完全归口。
现在既然已经明确“前端只负责做表现,所有逻辑、数据都放到后端进行运算和存储”,就需要把这个原则升级成整个工程的硬边界,而不是只停留在一部分接口迁移完成的状态。
---
## 2. 重构总原则
本轮重构只坚持一个核心原则:
**前端不是业务执行层,而是表现层;后端才是唯一的运行时真相来源。**
进一步展开为:
- 前端只负责页面结构、动画演出、输入采集、局部交互态、加载态和错误态展示。
- 后端负责鉴权、会话、规则计算、剧情推进、AI 编排、任务推进、道具结算、Build 结算、存档读写与持久化。
- 浏览器内不再保留“正式运行时业务规则”的第二套实现。
- 浏览器内允许存在少量纯表现计算,但不允许成为游戏状态真相来源。
- 编辑器能力与正式运行时能力分离,避免 dev 工具链继续污染正式运行时边界。
---
## 3. 重构目标
## 3.1 目标状态
- 浏览器只发送“玩家意图”和必要的展示参数,不直接提交完整运行时真相。
- `Express` 后端成为唯一的运行时状态源、规则执行源和 AI 调度源。
- 运行时快照、任务状态、NPC 状态、背包、属性、Build、剧情历史全部以后端持久化结果为准。
- 前端不再直接 import 正式运行时 AI 逻辑、提示词逻辑和关键规则逻辑。
- `server-node` 不再依赖 `src/**` 中的前端实现细节,而是依赖独立共享层。
- 编辑器 API、运行时 API、资产生成 API 形成清晰命名空间和权限边界。
## 3.2 非目标
- 本轮不追求一次性重写所有玩法系统。
- 本轮不再讨论关系型数据库选型切换,当前后端以 `PostgreSQL` 为准。
- 本轮不改动已有中文剧情、设定和文案方向。
- 本轮不为了“前后端分离”牺牲移动端体验与当前主流程可玩性。
---
## 4. 职责边界
| 领域 | 前端职责 | 后端职责 |
| --- | --- | --- |
| 页面与流程壳层 | 页面切换、面板开关、布局、自适应、动效、加载态 | 不负责页面 UI |
| 用户输入 | 收集点击、拖拽、表单输入、选项选择 | 校验输入是否合法,解释输入对应的运行时动作 |
| 游戏状态 | 仅持有当前展示所需 view model 和局部 UI state | 持有完整游戏状态、快照、事件日志、版本号 |
| 剧情推进 | 展示文本流、选项、动画时间线 | 生成剧情、决定选项集合、推进故事状态 |
| 战斗与数值 | 播放攻击、受击、死亡、位移 | 计算伤害、蓝耗、CD、死亡、掉落、逃跑结果 |
| NPC/同伴交互 | 展示面板、聊天输入框、关系反馈演出 | 计算关系变化、招募条件、交易合法性、对话结果 |
| 背包/装备/Build | 展示背包、装备栏、Build 面板 | 计算背包变化、装备结果、Build 收益与约束 |
| 任务系统 | 展示任务卡片、任务进度、奖励动画 | 生成任务、推进 signal、发放奖励 |
| AI 调用 | 不直接请求正式运行时模型 | 统一做 prompt 组装、模型调用、超时重试、日志 |
| 持久化 | 最多保留极少量表现态缓存 | 负责存档、设置、用户数据、迁移、恢复 |
| 编辑器 | 调用 SDK、展示工具面板 | 负责写盘、生成任务、队列、权限与审计 |
## 4.1 前端允许保留的状态
- 当前面板是否打开
- 当前动画是否播放中
- 当前流式文本已经显示到哪一段
- 表单草稿、搜索词、临时筛选条件
- 与展示相关的 viewport / media / motion 状态
## 4.2 前端禁止继续承载的职责
- function 合法性判定
- 怪物/NPC/任务/物品结算
- 正式运行时 prompt 组装
- 正式运行时 AI fallback
- 存档 schema 迁移主逻辑
-`localStorage` 作为正式运行时主存储
- 编辑器组件直接散落 `fetch('/api/...')` 访问写接口
---
## 5. 当前工程问题归纳
## 5.1 运行时领域逻辑仍然偏前端中心
- `useStoryGeneration` 仍然是大体量编排热区承接了剧情、NPC、战斗后续、任务和部分故事引擎逻辑。
- `src/services/ai.ts` 体量很大,说明正式运行时 AI 编排尚未完全移出浏览器。
- 当前“后端接口 + 前端兜底”的过渡模式,容易让正式逻辑继续双份存在。
## 5.2 服务端分层还没真正闭合
- `server-node` 当前仍直接引用 `src/types``src/data``src/services`
- 这意味着后端虽然有了入口,但核心领域模型仍然绑在前端目录结构上。
- 继续沿着这条路开发,会让后端无法独立测试、独立构建和独立演进。
## 5.3 运行时持久化边界还不够干净
- 虽然正式存档已经走远端接口,但前端仍承担较重的恢复、归一化、迁移纠偏逻辑。
- 这会导致“存档解释权”同时存在于前后端两边,后续迭代容易失配。
## 5.4 编辑器与运行时 API 仍然混杂
- 编辑器读写接口目前仍然有散落访问点。
- 资产生成、JSON 写盘、运行时 API 还没有形成清晰的接口分域。
- 继续混用会让权限控制、生产部署和后续多人协作变得困难。
## 5.5 当前协议更像“接口迁移”,还不是“后端驱动运行时”
- 目前很多接口是把已有前端逻辑搬成了远端调用入口。
- 但真正理想状态应该是玩家点击后后端完成规则结算、状态推进、AI 调用和持久化,再把展示模型返回给前端。
---
## 6. 目标架构
```text
Browser
├─ 页面 / 动画 / 交互 / ViewModel 渲染
├─ 轻量前端 SDK只负责请求与状态绑定
└─ 局部 UI State
packages/shared
├─ contracts
├─ schemas
├─ domain-types
└─ api-client-types
server-node
├─ src/modules/auth
├─ src/modules/runtime-session
├─ src/modules/story
├─ src/modules/combat
├─ src/modules/npc
├─ src/modules/inventory
├─ src/modules/build
├─ src/modules/quest
├─ src/modules/custom-world
├─ src/modules/editor
├─ src/shared/http
├─ src/shared/infra
└─ src/shared/llm
storage
├─ postgres
├─ uploads
└─ generated
```
## 6.1 共享层原则
- `packages/shared` 只放类型、schema、协议、纯函数和序列化约定。
- 共享层不放浏览器专属实现,也不放 Node 专属 IO。
- 所有可执行运行时规则默认放后端,不放共享层。
## 6.2 前端目录目标
前端建议逐步收敛成下面的职责结构:
```text
src/
├─ app
├─ pages
├─ widgets
├─ features
├─ entities
├─ shared/api
├─ shared/ui
└─ shared/lib
```
其中:
- `shared/api` 只保留面向后端 contract 的 SDK。
- `features` 只组织交互流程和 UI 组合,不再承载正式运行时规则。
- 超大 hook 逐步拆成“页面状态协调层 + 远端 action 调用层 + 表现层状态”。
---
## 7. 关键协议重构方向
当前最值得尽快统一的,不是继续加接口数量,而是把协议升级成“意图驱动”。
推荐核心动作协议:
```json
{
"sessionId": "runtime-session-id",
"clientVersion": 12,
"action": {
"type": "story_choice",
"functionId": "fight_attack",
"targetId": "npc_merchant_01",
"payload": {
"optionId": "opt_02"
}
}
}
```
后端统一返回:
```json
{
"sessionId": "runtime-session-id",
"serverVersion": 13,
"viewModel": {},
"presentation": {
"storyText": "",
"options": [],
"battlePlayback": null,
"toast": null
},
"patches": [],
"meta": {
"requestId": "req_xxx"
}
}
```
协议约束:
- 前端不再提交完整 `gameState` 作为后端运算依据。
- 前端提交的是“玩家意图”,不是“玩家已经算好的结果”。
- 后端返回的是“下一帧该怎么演”的展示模型,而不是只回一个零散字段。
---
## 8. 分阶段重构路线
## P0先冻结边界建立共享协议层
### 本阶段目标
把“前端只做表现,后端负责运行时真相”从口头原则变成工程边界。
### 主要任务
- 提取 `shared contracts`,把 `server-node``src/**` 的依赖逐步迁出。
- 固化统一的 API 响应结构、错误结构、`requestId`、版本字段。
- 明确运行时 API 命名空间与编辑器 API 命名空间。
- 新功能一律禁止再把正式运行时规则写回前端。
- 为关键运行时入口补健康检查、日志字段、耗时统计。
### 交付物
- 共享类型与 schema 目录
- 统一 API 约定文档
- 服务端模块边界草图
- 前端 SDK 基础层
### 验收标准
- `server-node` 可以不依赖 `src/**` 中的前端运行时实现继续编译。
- 新增运行时需求不再允许“前端先写一版、后端再补一版”。
## P1把运行时状态与持久化解释权收回后端
### 本阶段目标
让后端成为运行时状态、快照、恢复和迁移的唯一解释者。
### 主要任务
- 建立 `runtime session` / `snapshot aggregate`
- 将存档恢复、版本迁移、默认值补齐、schema 纠偏迁到后端。
- 把前端 `useGamePersistence` 收敛为“拉取快照 + 触发保存 + 接收 view model”。
- 设置、快照、自定义世界库统一归入运行时仓储接口。
- 明确哪些内容允许本地缓存,哪些必须以后端结果为准。
### 交付物
- 统一的运行时 session API
- 快照版本迁移服务
- 服务端持久化 schema 文档
### 验收标准
- 前端不再承担正式存档恢复迁移的主逻辑。
- 同一份存档的解释权只存在于后端。
## P2把核心规则结算从前端迁到后端
### 本阶段目标
把“剧情推进、战斗、NPC、任务、物品、Build”这些真正影响状态的领域结算全部后端化。
### 主要任务
- 把 function 合法性过滤迁入后端。
- 把战斗结算、蓝耗、伤害、死亡、掉落、逃跑结果迁入后端。
- 把 NPC 交互决策、招募条件、关系变化、交易合法性迁入后端。
- 把任务推进 signal、奖励结算、运行时物品结果、Build 结果迁入后端。
- 前端收到的只是一份下一步展示所需的聚合 view model 与演出计划。
### 交付物
- 运行时 action resolver
- 统一领域服务接口
- 面向 UI 的 view model assembler
### 验收标准
- 前端点击一个选项时,发送的是 action不是本地先算完再上传结果。
- 正式运行时的数值、资源、状态迁移不再依赖浏览器逻辑。
## P3把 AI 编排彻底收口到后端
### 本阶段目标
让浏览器彻底退出正式运行时 AI 调用与 prompt 组装。
### 主要任务
- 把剧情生成、角色聊天、NPC 对话、自定义世界生成、任务生成、物品意图生成等统一后端执行。
- 清理前端正式运行时代码中的 AI fallback。
- 将 prompt 构造、模型容错、超时、重试、日志、SSE 转发统一收口到后端。
- 对需要复用的 prompt 纯函数进行共享层抽取,但执行权只留在后端。
### 交付物
- 后端 AI orchestration 模块
- 统一 SSE/streaming 适配层
- 精简后的前端 AI SDK
### 验收标准
- 浏览器正式运行时代码不再直接 import 大体量 AI 编排模块。
- 无后端时,正式运行时不再默默回退到另一套浏览器逻辑。
## P4把编辑器与资产流程独立成正式后端模块
### 本阶段目标
让编辑器能力不再作为运行时副产物存在,而是成为有边界的工具后端模块。
### 主要任务
- 建立 `/api/editor/*``/api/assets/*` 等明确命名空间。
- 给编辑器写接口补权限、环境门禁、审计日志。
- 统一编辑器 JSON 读写、资产生成、任务查询接口。
- 前端编辑器组件全部改走统一 SDK不再散落直连接口。
### 交付物
- editor API contract
- 统一 editor client SDK
- 生成任务与写盘适配器
### 验收标准
- 编辑器写接口不再散落在多个组件内部。
- 运行时 API 与编辑器 API 的职责边界清晰。
## P5补齐质量门禁、部署路径和观测能力
### 本阶段目标
让这次后端化重构可以稳定上线,而不是只在本地联调成立。
### 主要任务
- 为后端补单测、接口测试和关键链路 smoke。
- 为前端补 contract 测试,确保 UI 不依赖本地规则。
- 建立 `Nginx/Caddy -> dist + /api` 的同域部署路径。
- 为流式接口补代理配置、超时、取消和日志。
- 为数据库迁移、备份、回滚预留脚本。
### 验收标准
- `web + server` 可以独立构建、独立测试、联合部署。
- 关键主流程至少具备一条可自动验证的 smoke path。
---
## 9. 具体迁移清单
## 9.1 优先迁移对象
- `src/hooks/useStoryGeneration.ts`
- `src/hooks/useGamePersistence.ts`
- `src/services/ai.ts`
- `src/services/aiService.ts`
- `src/services/storageService.ts`
- `src/services/authService.ts`
- 编辑器持久化模块与 `src/editor/shared/jsonClient.ts`
## 9.2 优先抽离到共享层的内容
- 领域类型定义
- zod schema 或等价校验协议
- API 请求与响应 contract
- 纯序列化函数
- 前后端都要认识的 enum / id / status 常量
## 9.3 不建议抽到共享层的内容
- 依赖数据库、文件系统、LLM、日志的服务
- 正式运行时规则执行器
- 存档迁移执行器
- 资产生成任务调度器
---
## 10. 实施顺序建议
推荐顺序如下:
1. 先抽共享类型与协议,切断 `server-node -> src/**` 的反向依赖。
2. 再把运行时 session、快照解释权、存档迁移收回后端。
3. 再迁核心规则结算,让前端从“业务执行层”退回“表现协调层”。
4. 然后彻底收口 AI 编排,移除正式运行时浏览器 fallback。
5. 最后归整编辑器 API、部署路径、测试门禁和观测能力。
不建议的顺序:
1. 先零散把几个接口改成后端。
2. 继续保留前端完整 fallback。
3. 最后再补共享层和协议。
这个顺序会把“双份逻辑并存”的过渡期拖得很长,后面会越来越难收口。
---
## 11. 风险与控制点
- 最大风险不是“迁不动”,而是长期维持双份规则。
- 后端化期间必须避免再往前端加新的正式运行时规则。
- 协议演进要带版本号,否则快照和 UI 很容易错位。
- 前端瘦身不能牺牲移动端一屏体验,表现层拆分仍要遵守移动端优先。
- 编辑器 API 必须和正式运行时隔离,不要为了方便继续走混用路径。
---
## 12. 一句话结论
这次重构的核心不是“把几个请求改成走 Express”而是
**把项目从“前端主导运行时、后端承接部分接口”的过渡架构升级成“Express 后端统一持有运行时真相,前端只负责表现和交互”的正式工程架构。**

View File

@@ -3,12 +3,10 @@
## 当前入口 ## 当前入口
- [ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md](./ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md):面向无用历史代码、隐形多数据链路和半成品实现的一轮工程大清洗执行计划,强调先建台账、再删重收口、最后恢复主工程可读性。 - [ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md](./ENGINEERING_DEAD_CODE_AND_HIDDEN_BRANCH_CLEANUP_PLAN_2026-04-21.md):面向无用历史代码、隐形多数据链路和半成品实现的一轮工程大清洗执行计划,强调先建台账、再删重收口、最后恢复主工程可读性。
- [CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md](./CURRENT_GAME_ITERATION_PRIORITIES_2026-04-03.md):当前阶段最值得优先做什么、为什么,以及它和审计/PRD 的对应关系。
- [../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)当前创作入口、Agent session、结果页自动保存、作品库与进入世界主链的正式文件级重构基线涉及目录落位、命名规范、阶段验收与工作包拆分时优先看这一份。 - [../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)当前创作入口、Agent session、结果页自动保存、作品库与进入世界主链的正式文件级重构基线涉及目录落位、命名规范、阶段验收与工作包拆分时优先看这一份。
- [../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)当前平台入口、继续游戏、角色选择、RPG runtime 与 runtime story 主链的正式文件级重构基线涉及入口壳层、session、runtime、story、route/service/repository 拆分时优先看这一份。 - [../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md](../technical/RPG_ENTRY_RUNTIME_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md)当前平台入口、继续游戏、角色选择、RPG runtime 与 runtime story 主链的正式文件级重构基线涉及入口壳层、session、runtime、story、route/service/repository 拆分时优先看这一份。
- [CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md](./CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md):创作链高层目标、冻结边界与执行顺序说明;文件级拆分与阶段验收以创作链重构执行方案为准。 - [CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md](./CURRENT_AGENT_CREATION_FLOW_OPTIMIZATION_PLAN_2026-04-20.md):创作链高层目标、冻结边界与执行顺序说明;文件级拆分与阶段验收以创作链重构执行方案为准。
- [EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md](./EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.md)基于“前端只做表现、逻辑与数据全部后端化”的工程重构规划 - [../technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](../technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)当前后端唯一落地口径,后续排期涉及服务端、数据真相或 SpacetimeDB 时优先按这一份判断方向
- [EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md](./EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md):将后端化重构拆成可并行推进、尽量不冲突的任务流与协作顺序。
- [BEIJING_POLICY_APPLICATION_OVERVIEW_13_21_24_2026-04-14.md](./BEIJING_POLICY_APPLICATION_OVERVIEW_13_21_24_2026-04-14.md):北京市方向 13 / 21 / 24 的统一判断、共用材料框架和准备顺序。 - [BEIJING_POLICY_APPLICATION_OVERVIEW_13_21_24_2026-04-14.md](./BEIJING_POLICY_APPLICATION_OVERVIEW_13_21_24_2026-04-14.md):北京市方向 13 / 21 / 24 的统一判断、共用材料框架和准备顺序。
- [BEIJING_DIRECTION13_APPLICATION_MATERIALS_2026-04-14.md](./BEIJING_DIRECTION13_APPLICATION_MATERIALS_2026-04-14.md):方向 13 软件智能化提升奖励的硬门槛、必交材料、底稿建议和证据清单。 - [BEIJING_DIRECTION13_APPLICATION_MATERIALS_2026-04-14.md](./BEIJING_DIRECTION13_APPLICATION_MATERIALS_2026-04-14.md):方向 13 软件智能化提升奖励的硬门槛、必交材料、底稿建议和证据清单。
- [BEIJING_DIRECTION21_APPLICATION_MATERIALS_2026-04-14.md](./BEIJING_DIRECTION21_APPLICATION_MATERIALS_2026-04-14.md):方向 21 “创赢未来”成长计划的报名表、BP、Demo 和融资规划整理。 - [BEIJING_DIRECTION21_APPLICATION_MATERIALS_2026-04-14.md](./BEIJING_DIRECTION21_APPLICATION_MATERIALS_2026-04-14.md):方向 21 “创赢未来”成长计划的报名表、BP、Demo 和融资规划整理。
@@ -18,4 +16,5 @@
- 需要排期、拆阶段、判断先修基线还是先加功能时,先看这份。 - 需要排期、拆阶段、判断先修基线还是先加功能时,先看这份。
- 当前如果要推进创作链或 RPG 运行时主链重构,先看上面的两份 `2026-04-21` 执行方案,再回来看高层优先级和冻结边界。 - 当前如果要推进创作链或 RPG 运行时主链重构,先看上面的两份 `2026-04-21` 执行方案,再回来看高层优先级和冻结边界。
- 涉及后端方案时,不再参考已删除的 Express / Node 规划文档,统一回到 Rust / SpacetimeDB 当前基线。
- 这份文档大量引用了经验文档、工程审查和 PRD适合作为跨文档导航页使用。 - 这份文档大量引用了经验文档、工程审查和 PRD适合作为跨文档导航页使用。

View File

@@ -0,0 +1,56 @@
# api-server 本地 Rust 栈冷编译等待修复记录
日期:`2026-04-25`
## 1. 背景
本地执行 `npm run dev:rust` 时,日志出现:
```text
[dev:rust] 等待 api-server 就绪
Compiling api-server v0.1.0
[dev:rust] 等待 api-server 就绪超时: http://127.0.0.1:8082/healthz
[dev:rust] 停止 api-server
error: linking with `link.exe` failed: exit code: 143
```
这类失败发生在 `api-server` 仍处于 `cargo run` 的冷编译或链接阶段时,`/healthz` 还没有机会监听端口。
## 2. 根因
根目录 `scripts/dev-rust-stack.sh` 同时使用 `SPACETIME_TIMEOUT_SECONDS=60` 控制:
1. SpacetimeDB standalone 的启动等待。
2. Rust `api-server``/healthz` 就绪等待。
SpacetimeDB 的本地启动通常较快,但 `api-server` 在 Windows MSVC 链接、依赖增量失效、首次构建或新增大依赖后可能超过 60 秒。脚本在超时后执行清理逻辑,主动杀掉仍在运行的 `cargo run` 子进程,因此 `link.exe exit code: 143` 是被本地栈脚本中断后的表现,不应优先判断为 Visual Studio Build Tools 损坏。
## 3. 修复口径
`scripts/dev-rust-stack.sh` 将 SpacetimeDB 与 `api-server` 的等待窗口拆开:
1. `SPACETIME_TIMEOUT_SECONDS` 继续只控制 SpacetimeDB 就绪等待,默认 `60` 秒。
2. 新增 `API_SERVER_TIMEOUT_SECONDS` 控制 `api-server` `/healthz` 就绪等待,默认 `300` 秒。
3. 新增命令行参数 `--api-timeout-seconds <seconds>` 便于本地低性能机器或全量重编译时临时放宽。
4. `api-server` 进程如果在等待窗口内自行退出,仍立即报错,不吞掉真实编译错误。
## 4. 使用方式
常规本地启动继续使用:
```bash
npm run dev:rust
```
如本地需要更长冷编译窗口,可执行:
```bash
npm run dev:rust -- --api-timeout-seconds 600
```
## 5. 验收标准
1. 冷编译期间脚本不会在 60 秒时误杀 `cargo run -p api-server`
2. `/healthz` 真正可访问后,脚本继续启动 Vite。
3. 如果 `api-server` 编译失败或运行时提前退出,脚本仍能快速停止并输出原始错误。
4. SpacetimeDB 启动异常仍使用独立的 `--spacetime-timeout-seconds` 判断。

View File

@@ -112,7 +112,7 @@
1. 工程修改必须同步对应阶段任务清单。 1. 工程修改必须同步对应阶段任务清单。
2. 新增或改变接口时,同步更新 [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md)。 2. 新增或改变接口时,同步更新 [RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md](./RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md)。
3. 仍存在 Node 旧能力差异时,同步更新 [NODE_BACKEND_MODULE_AND_API_INDEX.md](./NODE_BACKEND_MODULE_AND_API_INDEX.md) 的过期说明或新增 Rust 侧补充索引。 3. 仍存在旧能力差异时,同步更新 [CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](./CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md) 或新增 Rust 侧补充索引。
4. M4 结构变更同步维护 RPG runtime 链路文档。 4. M4 结构变更同步维护 RPG runtime 链路文档。
5. M5 结构变更同步维护 creation flow 链路文档。 5. M5 结构变更同步维护 creation flow 链路文档。
6. M6 资产链路变更同步维护 OSS / asset_object / generated path 文档。 6. M6 资产链路变更同步维护 OSS / asset_object / generated path 文档。
@@ -135,4 +135,3 @@
3. 关键 SSE 接口联调。 3. 关键 SSE 接口联调。
4. SpacetimeDB publish / rollback 演练。 4. SpacetimeDB publish / rollback 演练。
5. 灰度环境双跑对比。 5. 灰度环境双跑对比。

View File

@@ -0,0 +1,33 @@
# 当前后端实现基线2026-04-25
## 1. 当前唯一落地口径
后续正式后端实现统一以 `server-rs` 为准:
- HTTP 门面Rust `api-server` / Axum。
- 实时状态与业务真相:`crates/spacetime-module` / SpacetimeDB。
- 共享领域与契约:`server-rs` 多 crate 分层维护。
- 前端职责:只做表现、输入采集、临时 UI 状态与服务端结果渲染。
涉及 SpacetimeDB 的表、reducer、绑定生成、发布、本地联调必须按仓库内 SpacetimeDB skills 执行。
## 2. 已替代的旧方向
以下旧方向不再作为新功能设计和编码依据:
- `server-node` / Express / PostgreSQL 正式后端路线。
- Go 服务端试验路线。
- 浏览器侧承担正式运行时逻辑、正式生成编排或正式数据真相的路线。
旧实现只允许作为迁移参考:可以阅读其 contract、提示词、测试用例和边界经验但不得为了兼容旧服务端继续扩展新代码。
## 3. 新文档落点
后续补充后端方案时优先落到这些文档族:
- Rust / SpacetimeDB 架构与切流:`SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md``BACKEND_REWRITE_CROSS_CUTTING_GOVERNANCE_2026-04-22.md``M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md`
- SpacetimeDB 模块拆分:`SPACETIME_MODULE_LIB_RS_SPLIT_EXECUTION_2026-04-23.md`
- Rust API 路由索引:`RUST_API_SERVER_ROUTE_INDEX_2026-04-22.md`
- 本地与远端部署:`RUST_LOCAL_AND_REMOTE_DEPLOYMENT_SCRIPTS_2026-04-22.md``JENKINS_RUST_BUILD_DEPLOY_PIPELINES_2026-04-23.md`
如果旧文档与本基线冲突,以本基线和更新日期更近的 Rust / SpacetimeDB 文档为准。

View File

@@ -1,101 +0,0 @@
# 编辑器与资产 API 迁移清单2026-04-08
## 1. 任务定位
对应 [EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md](../planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md) 中的任务 8编辑器 API 归口与工具链隔离。
本轮目标是把编辑器写盘、资产生成、生成任务查询从旧的 Vite 本地 API 插件里收口到 `server-node`,并把前端编辑器组件改成通过统一 SDK 访问。
---
## 2. 新命名空间
编辑器写盘与读取:
- `GET /api/editor/catalog/items`
- `GET /api/editor/json/:resourceId`
- `POST /api/editor/json/:resourceId`
资产生成与任务查询:
- `POST /api/assets/character-visual/generate`
- `POST /api/assets/character-visual/publish`
- `GET /api/assets/character-visual/jobs/:taskId`
- `POST /api/assets/character-animation/generate`
- `POST /api/assets/character-animation/publish`
- `GET /api/assets/character-animation/jobs/:taskId`
- `POST /api/assets/character-animation/import-video`
- `GET /api/assets/character-animation/templates`
---
## 3. 前端接入
统一入口:
- `src/editor/shared/editorApiClient.ts`
已切换的编辑器链路:
- 角色预设覆盖保存
- 敌人预设覆盖保存
- 场景预设覆盖保存
- 场景角色覆盖保存
- NPC 形象覆盖与布局配置保存
- 物品目录读取与物品覆盖保存
- 状态行为覆盖保存
- 角色主形象生成、发布与任务查询
- 角色动作生成、导入、发布、模板读取与任务查询
---
## 4. 权限与环境边界
`server-node` 通过环境变量控制工具接口:
- `EDITOR_API_ENABLED`:控制 `/api/editor/*`
- `ASSETS_API_ENABLED`:控制 `/api/assets/*`
默认策略:
-`production` 环境默认开启。
- `production` 环境默认关闭。
- `ASSETS_API_ENABLED` 未设置时跟随 `EDITOR_API_ENABLED`
这批接口会读写 `src/data/*.json``public/generated-*`,不应作为正式运行时 API 使用。
---
## 5. 旧工具链隔离状态
`2026-04-19` 起,`scripts/dev-server/**` 中的旧 Vite 本地插件实现代码已经从仓库删除,也不再作为当前开发入口使用。
当前保留状态:
- `scripts/dev-server/` 目录只保留迁移说明 README。
- 旧链路的历史背景由 `docs/audits/engineering/ENGINEERING_CLEANUP_AND_BACKEND_BOUNDARY_AUDIT_2026-04-19.md` 等审计文档承接。
新增编辑器或资产能力时,应优先写入:
- `server-node/src/modules/editor/**`
- `server-node/src/modules/assets/**`
- `src/editor/shared/editorApiClient.ts`
不要再新增旧式散落接口:
- `/api/item-overrides`
- `/api/npc-visual-overrides`
- `/api/character-overrides`
- `/api/character-visual/*`
- `/api/animation/*`
- `/api/qwen-sprite/*`
---
## 6. 当前验收状态
- `/api/editor/*``/api/assets/*` 命名空间已落地。
- 前端编辑器组件已通过统一 SDK 或资源 ID 访问编辑器 API。
- Vite 已代理 `/api/editor``/api/assets` 到 Node 后端。
- 写接口已经有环境门禁。
- 旧 Vite 本地插件代码已删除,不再保留并行实现。

View File

@@ -1,108 +0,0 @@
# Express 后端接口冻结与集成清单2026-04-09
## 1. 目的
这份文档补齐 `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md` 中任务 0 缺失的仓库内产物,用来明确:
- 当前 contract 的冻结版本
- 热点文件的编辑规则
- 各类改动进入集成窗口前的最小检查清单
它不是新的重构计划,而是给当前并行改造提供一个统一落库的“不要互相踩”的边界表。
---
## 2. Contract 版本表
| 范围 | 当前版本 | 源头文件 | 说明 |
| --- | --- | --- | --- |
| 统一 API envelope | `2026-04-08 / v1` | `packages/shared/src/http.ts` | `ApiResponse`、错误结构、`meta` 字段、envelope 头约定的统一来源。 |
| auth contract | `2026-04-08` | `packages/shared/src/contracts/auth.ts` | 前后端都以 shared auth contract 识别登录、用户信息与 token 响应。 |
| runtime snapshot/settings contract | `2026-04-08` | `packages/shared/src/contracts/runtime.ts` | 存档、设置、自定义世界会话与库表相关请求/响应来源。 |
| runtime story action contract | `2026-04-08` | `packages/shared/src/contracts/story.ts` | `RuntimeStoryActionRequest/Response`、Task5/Task6 function id 与 view model 来源。 |
| Node HTTP route meta | `2026-04-08` | `server-node/src/app.ts` | `/api/auth``/api/runtime/story``/api/editor``/api/assets` 都以这一轮 route version 为当前冻结口径。 |
| editor/assets route 命名空间 | `2026-04-08` | `server-node/src/modules/editor/editorRoutes.ts``server-node/src/modules/assets/**` | 编辑器与资产接口统一走 `/api/editor/*``/api/assets/*`。 |
---
## 3. 热点文件编辑规则
以下文件继续视为高冲突入口,默认不要在多个任务里并行大改:
- `server-node/src/context.ts`
- `server-node/src/routes/runtimeRoutes.ts`
- `server-node/src/app.ts`
- `src/services/apiClient.ts`
- `src/hooks/useStoryGeneration.ts`
- `src/hooks/useGameFlow.ts`
- `src/components/GameShell.tsx`
统一规则:
- 新需求优先新增独立模块,再通过桥接或小入口接入,不要直接把逻辑堆进热点文件。
- 需要改 `server-node/src/routes/runtimeRoutes.ts` 时,先确认 shared contract 是否已落库,再补 route 接入。
- 需要改 `src/hooks/useStoryGeneration.ts` 时,优先确认是否其实应该落到 `server-node/src/modules/**``src/services/runtimeStoryService.ts``src/hooks/story/**`
- 编辑器链路与正式运行时链路不要混在同一轮提交里。
- 如果同一轮同时碰到后端 action 与前端 UI 壳层,先冻结 action/view model再接 UI。
---
## 4. 集成窗口清单
### 4.1 shared contract 变更
- 只在 `packages/shared/**` 改类型、schema、纯序列化约定。
- 同步检查 `server-node/src/**``src/**` 是否都已切到 shared contract。
- 至少跑一次 `npm run server-node:test`
- 如果前端消费层也改了,再补 `npm run typecheck`
### 4.2 runtime action / domain module 变更
- 业务规则优先写在 `server-node/src/modules/**`,不要直接写回前端 hook。
- 如果影响 `RuntimeStoryActionResponse`,同步检查 `packages/shared/src/contracts/story.ts`
- 至少覆盖对应模块测试,或补到 `server-node/src/modules/story/storyActionRoutes.test.ts`
- 合并前至少跑一次 `npm run server-node:test`
### 4.3 persistence / repository / config 变更
- 只把 PostgreSQL 视为正式基线。
- 如果改到 `server-node/src/db.ts``repositories/**`、迁移脚本,优先确认 `pg-mem` 测试仍通过。
- 合并前至少跑一次 `npm run server-node:test`
### 4.4 editor / assets 变更
- 后端入口只放在 `/api/editor/*``/api/assets/*`
- 前端统一从 `src/editor/shared/editorApiClient.ts` 或对应 persistence 层进入。
- 不要新增旧 Vite 本地插件式散落接口。
### 4.5 前端壳层接入变更
- 优先消费 `runtime story state / action response / shared contract`,不要把正式规则写回前端。
- 如果恢复流程有改动,优先以后端 runtime state 为准。
- 若影响主流程,至少补对应 hook / view model 测试并跑 `npm run typecheck`
---
## 5. 当前剩余非冻结区
以下几块仍处于“可继续收口但尚未完全冻结”的状态,改动时要额外小心:
- `server-node/src/bridges/legacyBuildRuntimeBridge.ts`
- `server-node/src/bridges/legacyInventoryRuntimeBridge.ts`
- `server-node/src/modules/ai/storyOrchestrator.ts`
- `server-node/src/modules/ai/chatOrchestrator.ts`
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
它们目前仍残留一部分对 `src/**` 历史实现的复用,不建议在没有额外测试兜底时顺手混改。
---
## 6. 本轮落库结论
从 2026-04-09 起,仓库内已经具备任务 0 要求的这几类最小产物:
- contract 版本表
- 热点文件编辑规则
- 集成窗口检查清单
后续如果 shared contract、runtime action 或热点入口发生明显演进,应优先更新这份文档,而不是让口径只停留在聊天记录里。

View File

@@ -1,109 +0,0 @@
# Express 后端任务 4 AI 编排收口状态2026-04-08
## 1. 结论
`EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md` 的任务 4 定义,本轮已经把正式运行时的 `story / character chat / npc chat / custom world generation / quest intent / runtime item intent` 的主要 AI 编排入口收回到 Express 后端。
当前可以视为:
- 正式运行时主链已不再依赖浏览器端大体量 AI 实现作为兜底。
- prompt 组装、上游模型请求、SSE 转发已以后端为主。
- 前端保留的本地 AI 大模块只通过懒加载方式服务于非正式运行时遗留入口,不再作为正式运行时默认路径。
---
## 2. 已完成项
### 2.1 后端统一 orchestration 入口
- `server-node/src/modules/ai/storyOrchestrator.ts`
- `server-node/src/modules/ai/chatOrchestrator.ts`
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
这些模块承接:
- story prompt 组装
- character chat prompt 组装
- npc chat / recruit prompt 组装
- custom world generation 后端入口封装
### 2.2 服务层收口
已收口到后端服务或模块的文件:
- `server-node/src/services/llmClient.ts`
- `server-node/src/services/storyService.ts`
- `server-node/src/services/chatService.ts`
- `server-node/src/services/customWorldGenerationService.ts`
- `server-node/src/services/questService.ts`
- `server-node/src/services/runtimeItemService.ts`
### 2.3 前端正式运行时 fallback 清理
`src/services/aiService.ts` 已完成以下收缩:
- story 正式路径不再 fallback 到浏览器本地 AI 编排
- character suggestions / summary / reply stream 不再 fallback 到浏览器本地 AI 编排
- npc dialogue / recruit stream 不再 fallback 到浏览器本地 AI 编排
- 移除了正式运行时对 `VITE_ENABLE_BROWSER_RUNTIME_AI_FALLBACK` 的依赖
- 移除了正式运行时对本地轻量离线文案 fallback 的默认依赖
-`./ai` 的引用改为懒加载,避免正式运行时默认把大体量 AI 模块打进主路径
- 正式运行时主流程 hook 已统一改走 `aiService`,不再直接从 `src/services/ai.ts` 获取 story/chat 主链能力
### 2.4 旧 bridge 清理
已移除:
- `server-node/src/bridges/legacyAiRuntimeBridge.ts`
---
## 3. 当前边界说明
以下项仍然存在于前端目录,但不再属于正式运行时默认 AI 执行路径:
- `src/services/ai.ts`
- `src/services/aiFallbacks.ts`
- `src/components/CustomWorldEntityEditorModal.tsx` 中的工具链直连调用
它们当前主要作为:
- 兼容性遗留实现
- 懒加载的非主路径工具能力
- 非本轮正式运行时链路的复用来源
这不再构成任务 4 的主阻塞,但后续仍应继续配合任务 1 / 任务 7 做彻底分层。
---
## 4. 非任务 4 主阻塞但需要记录的事项
### 4.1 仍属编辑器/工具链范畴的遗留调用
- `generateCustomWorldSceneImage` 仍通过懒加载复用旧实现。
原因:
- 该能力属于自定义世界工具链,不是正式运行时剧情 / 对话主链。
- 当前不会再影响“浏览器正式运行时是否依赖本地大 AI 编排”这一任务 4 验收项。
### 4.2 分层彻底闭合仍需后续任务配合
尽管任务 4 已完成主链收口,但以下更深层收敛仍建议交由后续任务继续推进:
- 继续减少 `server-node``src/**` 纯提示词/纯规则模块的历史复用
- 继续把共享 contract / schema 下沉到 `packages/shared`
- 继续把工具链与正式运行时拆分
这些属于任务 1、任务 7、任务 8 的后续工作,不再阻塞任务 4 验收。
---
## 5. 本轮建议验收口径
任务 4 可按以下口径验收:
- 浏览器正式运行时不再默认兜底到本地大体量 AI 编排
- story/chat/custom world generation 主链 prompt 组装与请求执行权在后端
- SSE 主链以后端转发为准
- upstream timeout / abort / error 统一走后端处理链

View File

@@ -1,425 +0,0 @@
# Express 后端并行任务完成度审计2026-04-09
## 1. 审计范围
本次审计以 `docs/planning/EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md` 为基准,只检查仓库内可直接验证的代码、测试、脚本和文档产物。
不能仅靠仓库静态内容确认的团队协作物,例如看板、口头冻结流程、每日集成节奏,只按“仓库内可见状态”记录,不把它们误判成完全验收。
---
## 2. 任务完成度总览
| 任务 | 状态 | 结论 |
| --- | --- | --- |
| 任务 0集成岗与接口冻结 | 基本完成 | 已补齐接口冻结与集成清单文档contract 版本表、热点文件编辑规则、集成窗口检查项都已落库;但团队执行节奏本身仍无法仅凭仓库静态确认。 |
| 任务 1共享 Contract 与目录抽离 | 部分完成 | `packages/shared` 已建立并承接 auth/runtime/story contract且 NPC Task6 bridge 与 chat prompt builder 已进一步下沉到后端本地模块;但 AI 编排主链仍残留少量 `src/**` 反向依赖,分层尚未完全闭合。 |
| 任务 2PostgreSQL 持久化基线收口 | 已完成 | `server-node/src/config.ts``db.ts``repositories/**`、迁移脚本、`pg-mem` 测试与部署文档均已到位。 |
| 任务 3HTTP 基础设施与统一响应壳层 | 已完成 | 统一错误格式、`requestId`、route meta、响应壳层、观测测试已落地。 |
| 任务 4服务端 AI 编排收口 | 基本完成 | orchestration 模块、SSE 转发和主链调用已回到后端,但仍有少量 prompt/规则模块通过 `src/**` 复用,属于后续继续收敛项。 |
| 任务 5Story / Combat / NPC | 已完成 | 后端 story action route、session 组装、combat/npc 领域服务和对应回归测试已落地。 |
| 任务 6Inventory / Quest / Build / Runtime Item | 已完成 | 对应模块、服务与回归测试已经覆盖主要正式运行时结算。 |
| 任务 7前端 SDK、鉴权、持久化瘦身 | 部分完成 | `apiClient``authService``storageService` 已统一,但前端仍保留一部分存档归一化和主流程协调职责。 |
| 任务 8编辑器 API 归口与工具链隔离 | 基本完成 | editor/assets 模块、前端 editor client、迁移文档均已出现职责边界基本清晰。 |
| 任务 9测试、观测与部署基线 | 已完成 | baseline test、smoke、proxy smoke、部署与回滚清单、日志链路均已具备。 |
| 任务 10前端主流程壳层与大 Hook 瘦身 | 部分完成 | `GameShellRuntime` / `useGameShellRuntimeViewModel` 以及新的 `storyRequestCoordinator` 已拆出,但 `useStoryGeneration.ts` 仍然过重,主流程尚未彻底退回“表现层协调器”。 |
---
## 3. 本轮补齐项
本轮先补齐了任务 0 缺失的仓库内协作产物:
- 新增 `docs/technical/EXPRESS_BACKEND_INTEGRATION_FREEZE_2026-04-09.md`
- 落库当前 contract 版本表
- 明确热点文件编辑规则
- 补齐 shared/runtime/editor/frontend 壳层各自的集成窗口清单
这让任务 0 不再只停留在规划文档里,而是有了一份可随版本一起更新的冻结口径。
随后补上了一段此前仍偏向前端快照解释的恢复链路:
- `src/hooks/story/runtimeStoryCoordinator.ts`
- 新增 `resumeServerRuntimeStory`
- 继续游戏时,若当前是正式运行时故事快照,会先请求 `/api/runtime/story/state/:sessionId`
- 让当前 `storyText` 与可用 `options` 优先以后端 runtime state 为准
- `src/hooks/useGamePersistence.ts`
- `continueSavedGame()` 现在会优先走后端 runtime story 恢复
- 如果服务端恢复失败,再回退到本地快照归一化结果
- 额外补了 `bottomTab` 归一化,避免恢复时吃到宽泛字符串
- `src/hooks/story/runtimeStoryCoordinator.test.ts`
- 新增“继续游戏时优先从后端恢复 runtime story”的测试
- 新增“非正式运行时快照不额外请求后端”的测试
这次补丁对应的是任务 7 与任务 10 之间的一段未完全闭合边界:前端在恢复流程里不应该只把远端存档当作“原始 JSON 缓存”,而应优先相信后端当前 runtime state。
此外,本轮还继续补了任务 1 的一段后端分层收口:
- 新增 `server-node/src/modules/runtime/runtimeStatePrimitives.ts`
- 后端本地承接 `addInventoryItems`
- 后端本地承接 `removeInventoryItem`
- 后端本地承接 `incrementGameRuntimeStats`
- 后端本地承接 `buildRelationState`
- 新增 `server-node/src/modules/runtime/runtimeStatePrimitives.test.ts`
- 校验背包合并、移除、运行时统计累加、关系阶段映射
- 调整桥接层:
- `server-node/src/bridges/legacyInventoryRuntimeBridge.ts`
- `server-node/src/bridges/legacyNpcTask6Bridge.ts`
- `server-node/src/modules/quest/questTask6Bridge.ts`
这意味着任务 5/6 主链里最基础的一批状态原语,已经不再依赖前端 `src/data/runtimeStats.ts``src/data/attributeResolver.ts``src/data/npcInteractions.ts` 的对应实现。
本轮又继续把 NPC Task6 bridge 里最后一批直接挂到前端 `npcInteractions.ts` 的函数下沉到了后端本地:
- 新增 `server-node/src/modules/npc/npcTask6Primitives.ts`
- 后端本地承接 `applyStoryChoiceToStanceProfile`
- 后端本地承接 `buildInitialNpcState`
- 后端本地承接 `syncNpcTradeInventory`
- 后端本地承接 `getGiftCandidates`
- 后端本地承接 `buildNpcGiftCommitActionText`
- 后端本地承接 `buildNpcGiftResultText`
- 后端本地承接 `buildNpcTradeTransactionActionText`
- 后端本地承接 `buildNpcTradeTransactionResultText`
- 新增 `server-node/src/modules/npc/npcTask6Primitives.test.ts`
- 覆盖 NPC 初始库存生成
- 覆盖交易库存刷新时保留非交易物品
- 覆盖赠礼偏好排序
- 调整桥接层:
- `server-node/src/bridges/legacyNpcTask6Bridge.ts`
这意味着 Task6 的 NPC 交易/赠礼/初始库存这条支链,已经不再直接依赖前端 `src/data/npcInteractions.ts`
同时还把叙事语言检测工具下沉到了共享层:
- 新增 `packages/shared/src/llm/narrativeLanguage.ts`
- `src/services/narrativeLanguage.ts` 改为复用共享实现
- `server-node/src/modules/ai/storyOrchestrator.ts` 改为直接依赖共享层
这部分虽然不是大块业务迁移,但它属于任务 1 最稳定的一类收口:把纯函数共识从前端目录中抽离出来。
再补充一批已经完成的后端纯原语迁移:
- 新增 `server-node/src/modules/runtime/runtimeEconomyPrimitives.ts`
- 后端本地承接 `formatCurrency`
- 后端本地承接 `getInventoryItemValue`
- 后端本地承接 `getNpcPurchasePrice`
- 后端本地承接 `getNpcBuybackPrice`
- 新增 `server-node/src/modules/runtime/runtimeTreasureTexts.ts`
- 后端本地承接 `buildTreasureResultText`
- 新增 `server-node/src/modules/runtime/runtimeEconomyPrimitives.test.ts`
- 覆盖交易定价、货币文本、宝藏奖励文本
对应桥接层:
- `server-node/src/bridges/legacyNpcTask6Bridge.ts`
- 不再依赖前端 `src/data/economy.ts`
- `server-node/src/bridges/legacyTreasureRuntimeBridge.ts`
- 不再从前端导出 `buildTreasureResultText`
这说明任务 5/6 主链中一部分交易、礼物、宝藏结算反馈文本,也已经从前端数据层抽离。
本轮又进一步补了 NPC 状态与叙事记忆的后端本地原语:
- 新增 `server-node/src/modules/runtime/runtimeNpcStatePrimitives.ts`
- 后端本地承接 `normalizeNpcPersistentState`
- 后端本地承接 `markNpcFirstMeaningfulContactResolved`
- 新增 `server-node/src/modules/runtime/runtimeNarrativeMemory.ts`
- 后端本地承接 `appendStoryEngineCarrierMemory`
- 新增 `server-node/src/modules/runtime/runtimeNpcStatePrimitives.test.ts`
- 覆盖 NPC 状态归一化、首次有效接触标记、叙事载体记忆写入
对应桥接层:
- `server-node/src/bridges/legacyNpcTask6Bridge.ts`
- 不再依赖前端 `src/services/storyEngine/echoMemory.ts`
- 不再依赖前端 `src/data/npcInteractions.ts` 中的 NPC 状态归一化与首次接触标记逻辑
这进一步缩小了后端在任务 5/6 主链上对前端 story-engine 服务目录的借用范围。
本轮还整块收口了 Quest 与 Runtime Item 两条桥接链:
- 新增 `server-node/src/modules/quest/runtimeQuestModule.ts`
- 后端本地承接 `buildQuestForEncounter`
- 后端本地承接 `evaluateQuestOpportunity`
- 后端本地承接 `buildFallbackQuestIntent`
- 后端本地承接 `compileQuestIntentToQuest`
- 后端本地承接 `buildQuestGenerationContextFromState`
- 后端本地承接 `buildQuestIntentPrompt`
- 后端本地承接 Quest 进度归一化与 signal 推进
- 更新桥接层:
- `server-node/src/bridges/legacyQuestProgressBridge.ts`
- `server-node/src/bridges/legacyQuestRuntimeBridge.ts`
这意味着 Quest 的“确定性委托构建 + AI 意图上下文 + 任务推进”已经不再依赖前端 `src/data/questFlow.ts``src/services/questDirector.ts``src/services/questPrompt.ts`
同时Runtime Item 也已经收回到后端本地:
- 新增 `server-node/src/modules/runtime-item/runtimeItemModule.ts`
- 后端本地承接 `buildRuntimeItemAiIntent`
- 后端本地承接 `buildRuntimeItemIntentPrompt`
- 后端本地承接 `buildLooseRuntimeItemGenerationContext`
- 后端本地承接 `buildQuestRuntimeItemGenerationContext`
- 后端本地承接 `buildDirectedRuntimeReward`
- 后端本地承接 `buildRuntimeInventoryStock`
- 后端本地承接 `flattenDirectedRuntimeRewardItems`
- 新增 `server-node/src/modules/runtime-item/runtimeTreasureModule.ts`
- 后端本地承接 `resolveTreasureReward`
- 更新桥接层:
- `server-node/src/bridges/legacyRuntimeItemBridge.ts`
- `server-node/src/bridges/legacyRuntimeItemResolutionBridge.ts`
- `server-node/src/bridges/legacyTreasureRuntimeBridge.ts`
这说明 Runtime Item / Treasure 相关的 AI 意图、奖励生成和库存生成,也已经从前端目录中抽离。
本轮还继续整块收口了 Build / Inventory / Forge / Equipment 规则桥接:
- 新增 `server-node/src/modules/runtime/runtimeEquipmentModule.ts`
- 后端本地承接 `getEquipmentSlotFromItem`
- 后端本地承接 `getEquipmentSlotLabel`
- 后端本地承接 `getEquipmentBonuses`
- 后端本地承接 `applyEquipmentLoadoutToState`
- 新增 `server-node/src/modules/runtime/runtimeInventoryEffectsModule.ts`
- 后端本地承接 `isInventoryItemUsable`
- 后端本地承接 `resolveInventoryItemUseEffect`
- 后端本地承接 `buildInventoryUseResultText`
- 新增 `server-node/src/modules/runtime/runtimeForgeModule.ts`
- 后端本地承接 `getForgeRecipeViews`
- 后端本地承接 `executeForgeRecipe`
- 后端本地承接 `executeDismantleItem`
- 后端本地承接 `executeReforgeItem`
- 后端本地承接 `getReforgeCostView`
- 后端本地承接 `buildForgeSuccessText`
- 新增 `server-node/src/modules/runtime/runtimeBuildModule.ts`
- 后端本地承接 `appendBuildBuffs`
- 后端本地承接 `getPlayerBuildDamageBreakdown`
- 后端本地承接 `resolvePlayerOutgoingDamageResult`
对应桥接层:
- `server-node/src/bridges/legacyBuildRuntimeBridge.ts`
- `server-node/src/bridges/legacyInventoryRuntimeBridge.ts`
这意味着 Build / Inventory / Forge / Equipment 相关的后端主链结算,已经不再依赖前端 `src/data/buildDamage.ts``src/data/equipmentEffects.ts``src/data/forgeSystem.ts``src/data/inventoryEffects.ts`
本轮又补了一段任务 1 的 AI 编排收口:
- 新增 `server-node/src/modules/ai/chatPromptBuilders.ts`
- 后端本地承接 character chat reply / suggestions / summary prompt 组装
- 后端本地承接 npc chat / recruit prompt 组装
- 调整:
- `server-node/src/modules/ai/chatOrchestrator.ts`
- `server-node/src/modules/ai/orchestrator.test.ts`
这意味着 chat orchestration 已不再依赖前端 `src/services/characterChatPrompt.ts``src/services/prompt.ts`
本轮继续把 story orchestration 主链也收回到了后端本地:
- 新增 `server-node/src/modules/ai/storyPromptBuilders.ts`
- 后端本地承接 `SYSTEM_PROMPT`
- 后端本地承接 `buildUserPrompt`
- 重写 `server-node/src/modules/ai/storyOrchestrator.ts`
- 正式生产链不再依赖前端 `src/services/prompt.ts`
- 正式生产链不再依赖前端 `src/data/stateFunctions.ts`
- 正式生产链不再依赖前端 `src/data/scenePresets.ts`
- 正式生产链不再依赖前端 `src/data/hostileNpcs.ts`
- 调整:
- `server-node/src/modules/ai/orchestrator.test.ts`
到这里,`server-node` 正式生产代码路径里Story / Chat / Quest / Runtime Item / Treasure / Build / Inventory / Forge / NPC Task6 主链都已经从前端 `src/**` 目录脱钩。
本轮也继续推进了任务 10 的主流程瘦身:
- 新增 `src/hooks/story/storyRequestCoordinator.ts`
- 抽离运行时 option source 解析
- 抽离服务端 option catalog 回退策略
- 抽离 initial / next story 请求参数协调
- 新增 `src/hooks/story/storyRequestCoordinator.test.ts`
- 覆盖服务端 option catalog 切换
- 覆盖显式 option catalog 短路
- 覆盖服务端目录加载失败时回退本地可用项
- 调整:
- `src/hooks/useStoryGeneration.ts`
这说明 `useStoryGeneration.ts` 虽然仍重,但“故事请求协调”已经不再和主流程 UI 状态、NPC/战斗/宝藏后续处理混在同一个大段里。
本轮又补了一段任务 10 的纯展示逻辑拆分:
- 新增 `src/hooks/story/storyPresentation.ts`
- 抽离 story options 去重与补齐
- 抽离对白 turn 解析
- 抽离 dialogue story moment 组装
- 抽离 typewriter delay
- 新增 `src/hooks/story/storyPresentation.test.ts`
- 覆盖对白解析与 dialogue story moment
- 覆盖选项池去重与补齐
- 调整:
- `src/hooks/useStoryGeneration.ts`
这意味着 `useStoryGeneration.ts` 又减少了一批与 React 状态本身无关的纯函数逻辑,任务 10 的主流程壳层拆分继续向前推进。
本轮还补上了任务 1 的最后一条后端反向依赖:
- 删除 `server-node/src/bridges/legacyCustomWorldAiBridge.ts`
- 重写 `server-node/src/modules/ai/customWorldOrchestrator.ts`
- 后端本地承接 custom world generation 的 deterministic 生成流程
- 后端本地承接 generation progress 汇报
这意味着 `server-node/src/**` 的正式生产代码路径已经不再反向依赖前端 `src/**` 目录。
---
## 4. 仍需继续收口的重点
### 4.1 任务 1 的剩余问题
当前从仓库内直接扫描看,`server-node/src/**` 的正式生产代码路径已经不再存在对前端 `src/**` 的反向依赖。
其中 Story / Chat / Quest / Runtime Item / Treasure / Build / Inventory / Forge / Equipment / NPC Task6 / Custom World generation 相关正式生产链都已经从这个列表中退出。
同时,`server-node/src/modules/ai/orchestrator.test.ts` 已不再直接依赖前端 `src/services/prompt.ts``src/data/stateFunctions.ts`
这说明任务 1 在“后端正式生产运行时不反向依赖前端目录”这一层面已经完成。
#### 4.1.1 当前残留依赖的真实形态
从当前状态看,任务 1 后续不再是“补洞”,而是“优化”:
- 继续提高 custom world generation 的质量与保真度
- 继续把真正通用的 prompt / JSON repair / batch helper 整理进 `packages/shared``server-node/src/modules/ai/**`
- 维持后端不再回流引用前端目录的约束
#### 4.1.2 更适合的继续收口顺序
结合当前状态,任务 1 后续更适合做的是能力优化:
1. 继续增强 custom world generation 的语义保真度与校验强度。
2. 继续把 prompt builder、JSON repair、批处理工具整理到 `packages/shared``server-node/src/modules/ai/**`
3. 持续维持“后端正式生产代码不反向依赖前端目录”的约束,避免后续重新开洞。
### 4.2 任务 7 的剩余问题
前端虽然已经大量改成 SDK 消费层,但 `src/persistence/runtimeSnapshot.ts` 里仍保留较重的存档归一化与迁移修复逻辑。
这部分后续仍建议继续向后端迁移,避免前后端双边解释快照。
#### 4.2.1 `runtimeSnapshot.ts` 当前仍承担的职责
从文件本身看,`src/persistence/runtimeSnapshot.ts` 仍不是一个单纯的“读取后端结果并转成 UI 状态”的轻薄消费层。
它当前仍直接负责:
- 存档迁移 manifest 应用
- roster 归一化
- player currency 默认值补齐
- equipment loadout 回填与属性重算
- NPC persistent state 归一化
- quest log 归一化
- runtime stats 归一化
- scene encounter preview 修复
- story engine memory 缺省补齐
这说明任务 7 的剩余问题,不只是“前端还有一点 normalize 代码”,而是:
- 前端仍在解释正式存档的结构语义
- 前端仍在决定若干正式运行时字段的缺省和修复策略
- 后端与前端之间仍存在“双边都能定义快照有效形态”的空间
#### 4.2.2 下一步更合适的迁移方向
结合本轮已经补上的“继续游戏优先以后端 runtime story state 为准”这段恢复链路,任务 7 后续更适合继续向这个方向推进:
1.`runtimeSnapshot.ts` 中的迁移、归一化、缺省补齐继续下沉到后端持久化层或专门的 runtime snapshot service。
2. 让前端拿到的远端快照尽量已经是“可直接消费的正式形态”,而不是“仍需前端补算的半成品”。
3. 把前端保留的本地 hydrate 逻辑收缩到纯 UI 恢复字段,例如当前页签、面板开合、局部显示态。
只有这样,任务 7 才算真正回到“前端只做消费层,后端才是正式状态解释者”的目标。
### 4.3 任务 10 的剩余问题
当前最大的未完成项仍然是:
- `src/hooks/useStoryGeneration.ts`
它仍承担了大量:
- 正式运行时故事编排
- 本地 fallback 组织
- NPC / 宝藏 / 战斗后续协调
- 主流程状态推进
现阶段虽然已经有:
- `src/hooks/useGameShellRuntime.ts`
- `src/components/game-shell/useGameShellRuntimeViewModel.ts`
- `src/hooks/story/runtimeStoryCoordinator.ts`
- `src/hooks/story/storyRequestCoordinator.ts`
但主流程还没有彻底完成 action/view model 化,任务 10 仍应视为“进行中”。
#### 4.3.1 `useStoryGeneration.ts` 当前仍然为什么过重
从仓库现状看,`src/hooks/useStoryGeneration.ts` 目前仍有约 1700 行,且其过重问题已经不是单纯“文件太长”,而是它还保留着大量正式业务协调职责。
当前这个 hook 里仍集中着:
- `buildStoryContextFromState` 这一整块故事上下文拼装
- AI 请求前的 option catalog / fallback 组织
- NPC / 宝藏 / 战斗 / inventory / goal / session 多条子链的集中协调
- 本地 fallback story 生成
- 一部分运行时规则与 narrative context 的最终拼装入口
这意味着即使已经拆出了:
- `runtimeStoryCoordinator`
- `storyRequestCoordinator`
- `choiceActions`
- `npcEncounterActions`
- `sessionActions`
`useStoryGeneration.ts` 仍然不是“单纯的 UI 壳层 hook”而更像前端侧的运行时总协调器。
#### 4.3.2 任务 10 后续更值得优先拆的部分
按当前文件结构看,后续最值得优先继续抽离的不是零散按钮逻辑,而是下面三类真正还握在主 hook 里的核心块:
1. `buildStoryContextFromState` 这一整块 story-engine 叙事上下文装配。
2. `buildFallbackStoryForState` / `getAvailableOptionsForState` 这类“正式规则兜底与选项来源判断”。
3. NPC / Treasure / Character Chat / Session 这些子流之间的最终总装协调。
如果只是继续把零散函数拆到 `src/hooks/story/**`,但上述三块还留在主 hook 里,那么任务 10 仍然不会真正完成。
### 4.4 建议的下一轮补齐顺序
结合任务 1、任务 7、任务 10 当前的剩余形态,后续更合适的补齐顺序建议是:
1. 先继续收任务 7 的 runtime snapshot 解释权,把正式快照的迁移、修复、归一化口径继续回收到后端。
2. 再继续收任务 10`useStoryGeneration.ts` 压回主流程协调壳层,而不是继续让它承担正式运行时总装职责。
这样排的原因是:
- 任务 1 已经完成后,新的主阻塞就变成了任务 7 和任务 10。
- 如果任务 7 不继续收,前端仍会在恢复链路里保留正式状态解释权,任务 10 也很难真正变薄。
- 等到 runtime state 与 snapshot 口径都更稳定后,再继续瘦 `useStoryGeneration.ts`,返工会更少。
---
## 5. 本轮验证
已通过:
- `npm run server-node:test`
- `npx vitest run src/hooks/story/storyRequestCoordinator.test.ts src/hooks/story/runtimeStoryCoordinator.test.ts`
- `npm run typecheck`
- `npm run check:encoding`
---
## 6. 结论
从仓库可验证结果看,任务 2、3、5、6、9 已经达到“可认为完成”的状态;任务 0、4、8 基本完成;任务 1、7、10 仍有明显后续收口空间。
当前最主要的未完成中心已经不再是后端基建,而是:
**把前端主流程和存档恢复彻底收成“以后端 runtime state 和 view model 为唯一真相源”。**

View File

@@ -1,190 +0,0 @@
# 前端逻辑后移实施方案2026-04-21
更新时间:`2026-04-21`
## 1. 目标
本方案只回答一件事:
**怎样把当前仍残留在前端的正式运行时逻辑、正式会话真相与正式生成编排,继续收回到 Express 后端。**
这份文档不是泛泛而谈的方向说明,而是直接面向本轮与后续几轮编码落地的实施基线。
---
## 2. 本轮确定的硬边界
根据仓库约束与当前审计结果,本轮继续冻结以下边界:
1. 前端只负责表现、输入采集、临时 UI 状态与服务端结果渲染。
2. 后端负责正式鉴权、正式会话、正式运行时快照、正式任务生成、正式运行时物品意图生成、正式自定义世界生成。
3. `codex/backend-rewrite-spacetimedb` 目标分支的鉴权仍以服务端签发 JWT、前端 Bearer token 携带为准;本轮合入不采用 `codex/dev` 的 access cookie 会话方案。
4. 浏览器内不再把浏览历史作为本地正式真相,不再保留正式 quest / runtime item / custom world 生成编排。
5. 运行时主链必须继续向“前端提交意图,后端解释快照并返回展示模型”收敛。
---
## 3. 现状拆分
当前残留问题已经收敛为三批:
### 3.1 第一批:正式真相仍在前端
1. `src/services/apiClient.ts`
- 浏览器当前仍保存 access token并在请求层拼接 `Authorization: Bearer ...`
- 该链路在 `codex/backend-rewrite-spacetimedb` 仍是既定正式实现,不再按 cookie access session 改写
2. `src/services/authService.ts`
- 登录、微信绑定、回调消费流程都要与 JWT/Bearer 方案保持一致,避免混入 access cookie 分支语义
3. `src/components/game-shell/PreGameSelectionFlow.tsx`
- 浏览历史仍是本地写入 + 后端回填的双真相
4. `src/services/platformBrowseHistory.ts`
- 维护浏览历史本地存储、迁移标记与同步状态
### 3.2 第二批:运行时主链仍依赖前端预写快照
1. `src/hooks/story/runtimeStoryCoordinator.ts`
- 在请求 runtime state / runtime action 前,仍先 `PUT /runtime/save/snapshot`
2. `src/hooks/story/npcEncounterActions.ts`
- 待接委托的“更换任务”“放弃任务”仍由前端正式结算
### 3.3 第三批:正式生成编排仍残留在浏览器
1. `src/services/questDirector.ts`
2. `src/services/runtimeItemAiDirector.ts`
3. `src/services/aiService.ts` 的 custom world profile 生成入口
4. `src/services/ai.ts` 中仍保留的浏览器侧 legacy AI orchestration
---
## 4. 分批实施策略
## 4.1 第一批:先收正式真相
### 鉴权
目标状态:
1. 后端继续通过 JWT 承载 access token并只从 `Authorization: Bearer ...` 读取当前访问身份。
2. 前端请求层继续负责保存、刷新和携带 access token公开请求与静默探测不得误清正式 token。
3. access cookie 会话方案不进入 `codex/backend-rewrite-spacetimedb`,避免和目标分支已有 JWT 方案并存。
4. `AuthGate` 通过 refresh cookie / `/api/auth/me` 恢复出用户后,必须先确保本地 access token 可用,再把 `readyUser` 暴露给运行时、设置、作品列表等受保护业务 hook业务 hook 不能只凭 `user.id` 在 token 尚未补齐时启动远端请求。
本批涉及:
1. `server-node/src/routes/authRoutes.ts`
2. `server-node/src/middleware/auth.ts`
3. `src/services/apiClient.ts`
4. `src/services/authService.ts`
5. `src/components/auth/AuthGate.tsx`
### 浏览历史
目标状态:
1. 浏览历史唯一真相在 `runtimeRepository`
2. 前端不再保留本地浏览历史、迁移标记、同步标记。
3. 浏览历史只通过 `storageService` 读取和写入。
本批涉及:
1. `src/components/game-shell/PreGameSelectionFlow.tsx`
2. `src/components/game-shell/PlatformHomeView.tsx`
3. `src/services/storageService.ts`
4. `src/services/platformBrowseHistory.ts`
## 4.2 第二批:把 runtime story 快照解释权收回后端
目标状态:
1. 前端不再通过单独的 `PUT /runtime/save/snapshot` 预写快照再触发动作。
2. runtime state / runtime action 允许前端提交当前快照上下文,由后端内部决定是否写入、如何解释、何时持久化。
3. NPC 待接委托的 replace / abandon / accept 全部走后端 runtime action。
建议实施方式:
1. 扩展 `packages/shared/src/contracts/story.ts`
- `RuntimeStoryActionRequest` 增加可选 `snapshot`
- 新增 `RuntimeStoryStateRequest`
2. 新增 `POST /api/runtime/story/state/resolve`
3. `storyActionService` 内部统一处理“请求携带快照上下文时的服务端同步”
4.`npc_chat_quest_offer_replace` / `npc_chat_quest_offer_abandon` 接到后端 runtime action
## 4.3 第三批:把正式生成编排收成后端唯一出口
目标状态:
1. `questDirector` 只保留轻量 SDK。
2. `runtimeItemAiDirector` 只保留轻量 SDK。
3. custom world profile 正式生成走后端 route。
4. 浏览器侧 `src/services/ai.ts` 不再承担正式浏览器主链。
建议实施方式:
1. `server-node/src/routes/runtimeRoutes.ts`
-`custom-world/profile` 正式 route
2. `src/services/aiService.ts`
- custom world 入口改走后端
3. `src/services/questDirector.ts`
- 只请求 `/api/runtime/quests/generate`
4. `src/services/runtimeItemAiDirector.ts`
- 只请求 `/api/runtime/items/runtime-intent`
---
## 5. 本轮落地范围
本轮优先完成以下内容:
1. 鉴权维持 `codex/backend-rewrite-spacetimedb` 既有 JWT/Bearer 方案,不合入 `codex/dev` 的 access cookie 访问认证。
2. 浏览历史从前端本地真相后移到后端唯一真相。
3. custom world profile 正式生成入口补齐后端 route并把前端收成 SDK。
4. `questDirector` / `runtimeItemAiDirector` 收缩为前端 SDK。
5. runtime story contract 开始补“随请求提交快照上下文”的后端承接能力,并把 NPC 待接委托 replace / abandon 接到后端。
### 5.1 已完成
1. `codex/backend-rewrite-spacetimedb` 本轮保留 JWT access token + refresh cookie 组合方案,不合入 access cookie 写入与读取链路。
2. 浏览历史已收敛为后端唯一真相,前端不再维护正式本地 browse history 链。
3. runtime story 已支持随请求提交 snapshot由后端内部解释与持久化。
4. NPC 待接委托 `replace / abandon / accept` 已以后端 runtime action 为准。
5. custom world profile 浏览器正式入口已改走后端 route。
6. `questDirector` / `runtimeItemAiDirector` 已收缩为前端 SDK不再承担正式浏览器编排。
7. NPC 招募正式结算已迁到后端:
- 前端只负责招募对白展示与 release 目标选择
- 后端负责 `npcStates / companions / roster / currentEncounter / storyHistory` 正式结算
- 满员换队招募已由后端承接
### 5.2 剩余未完成
1. `src/services/ai.ts` 仍保留 legacy fallback / test 能力,尚未彻底压缩出正式浏览器主链。
2. 仍需继续审视是否存在其他 NPC / 运行时分支,把正式状态裁决留在前端。
---
## 6. 验收标准
### 第一批验收
1. 浏览器继续保存 access token并由 `fetchWithApiAuth` 稳定拼接 `Authorization: Bearer ...`
2. 401 刷新链只在已发送 Bearer token 时触发,并且刷新响应必须返回新的 JWT。
3. 浏览历史仅通过远端接口读写。
4. `src/services/platformBrowseHistory.ts` 不再是正式链路依赖。
5. 手机验证码登录、微信回调或 refresh cookie 会话恢复完成后,首屏并发读取设置、存档、个人看板、浏览历史、作品列表前,必须已经完成 access token 写入,避免出现“用户已 ready 但请求缺少 Authorization Bearer Token”的竞态。
### 第二批验收
1. `runtimeStoryCoordinator.ts` 不再在动作前独立 `PUT /runtime/save/snapshot`
2. `NPC` 待接委托 replace / abandon / accept 都以后端返回结果为准。
### 第三批验收
1. `questDirector.ts``runtimeItemAiDirector.ts` 不再保留正式 fallback orchestration。
2. custom world profile 的浏览器正式入口不再直接 import legacy `./ai`
---
## 7. 一句话结论
这轮迁移的重点不是“把几个 helper 挪到 server-node 目录”,而是:
**把前端里仍然承担正式真相、正式运行时解释和正式生成编排的那一层职责,继续收回到 Express 后端。**

View File

@@ -11,7 +11,7 @@
- [M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md) - [M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_RUNTIME_SETTINGS_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md)
- [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md) - [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md)
- [NODE_BACKEND_MODULE_AND_API_INDEX.md](./NODE_BACKEND_MODULE_AND_API_INDEX.md) - [CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](./CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)
- `server-node/src/routes/rpg-profile/rpgProfileRoutes.ts` - `server-node/src/routes/rpg-profile/rpgProfileRoutes.ts`
- `server-node/src/repositories/runtimeRepository.ts` - `server-node/src/repositories/runtimeRepository.ts`

View File

@@ -10,7 +10,7 @@
关联现状: 关联现状:
- [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) - [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
- [NODE_BACKEND_MODULE_AND_API_INDEX.md](./NODE_BACKEND_MODULE_AND_API_INDEX.md) - [CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](./CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)
- `server-node/src/routes/rpg-profile/rpgProfileRoutes.ts` - `server-node/src/routes/rpg-profile/rpgProfileRoutes.ts`
- `server-node/src/repositories/runtimeRepository.ts` - `server-node/src/repositories/runtimeRepository.ts`

View File

@@ -24,11 +24,8 @@
本文以以下现有文档和代码为准: 本文以以下现有文档和代码为准:
1. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) 1. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
2. [NODE_BACKEND_MODULE_AND_API_INDEX.md](./NODE_BACKEND_MODULE_AND_API_INDEX.md) 2. [CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](./CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)
3. [EXPRESS_BACKEND_TASK4_AI_ORCHESTRATION_STATUS_2026-04-08.md](./EXPRESS_BACKEND_TASK4_AI_ORCHESTRATION_STATUS_2026-04-08.md) 3. 历史 Node AI 编排代码仅作为迁移背景,不再作为当前实现依据。
4. `server-node/src/modules/ai/storyOrchestrator.ts`
5. `server-node/src/modules/ai/chatOrchestrator.ts`
6. `server-node/src/modules/ai/customWorldOrchestrator.ts`
## 3. 现状问题 ## 3. 现状问题

View File

@@ -11,7 +11,7 @@ M7 的目标不是新增玩法功能,而是在 `M0 ~ M6` 已迁移的 Rust 后
1. 固定本地、灰度、切流前的检查命令。 1. 固定本地、灰度、切流前的检查命令。
2. 固定 `Axum + SpacetimeDB + OSS` 的部署与回滚口径。 2. 固定 `Axum + SpacetimeDB + OSS` 的部署与回滚口径。
3. 固定观测字段、慢请求、上游失败日志与资产任务日志。 3. 固定观测字段、慢请求、上游失败日志与资产任务日志。
4. 固定旧 `server-node` 与新 `server-rs` 的双跑和 API 对比方式。 4. 固定旧 `server-node` 删除后的 Rust 主线回归与部署验证方式。
5. 等价拆分 `server-rs/crates/spacetime-module/src/lib.rs`,避免 SpacetimeDB 主工程继续退化为单大文件。 5. 等价拆分 `server-rs/crates/spacetime-module/src/lib.rs`,避免 SpacetimeDB 主工程继续退化为单大文件。
## 2. 执行约束 ## 2. 执行约束
@@ -19,8 +19,8 @@ M7 的目标不是新增玩法功能,而是在 `M0 ~ M6` 已迁移的 Rust 后
1. 不改变现有 HTTP contract、SSE contract、SpacetimeDB 表名、reducer 名、procedure 名和对象键前缀。 1. 不改变现有 HTTP contract、SSE contract、SpacetimeDB 表名、reducer 名、procedure 名和对象键前缀。
2. 不把 LLM、OSS、短信、微信等外部副作用移入 SpacetimeDB reducer。 2. 不把 LLM、OSS、短信、微信等外部副作用移入 SpacetimeDB reducer。
3. `spacetime-module` 拆分只做物理结构收口,不做 schema 重命名、字段删除、字段重排或 reducer/procedure 改名。 3. `spacetime-module` 拆分只做物理结构收口,不做 schema 重命名、字段删除、字段重排或 reducer/procedure 改名。
4. 迁移期保留 `server-node` 作为回退锚点M7 不删除旧后端 4. `server-node/` 已进入物理删除流程M7 不再把旧 Node 后端作为运行时回退锚点
5. 前端切换默认指向 Node只有显式设置 `GENARRATIVE_BACKEND_STACK=rust``GENARRATIVE_RUNTIME_SERVER_TARGET` 时才切到 Rust。 5. 前端默认指向 Rust `api-server`;如需临时覆盖目标,只允许使用 `RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 指向 Rust 兼容服务
## 3. 测试体系 ## 3. 测试体系
@@ -36,7 +36,6 @@ M7 固定四层测试入口:
```powershell ```powershell
.\server-rs\scripts\m7-preflight.ps1 .\server-rs\scripts\m7-preflight.ps1
.\server-rs\scripts\smoke.ps1 .\server-rs\scripts\smoke.ps1
node scripts\run-tsx.cjs scripts\m7-api-compare.ts
``` ```
## 4. 部署准备 ## 4. 部署准备
@@ -72,32 +71,32 @@ OSS / CDN / 域名方案:
6. `DASHSCOPE_BASE_URL``DASHSCOPE_API_KEY` 6. `DASHSCOPE_BASE_URL``DASHSCOPE_API_KEY`
7. `SMS_AUTH_ENABLED` 与短信供应商变量 7. `SMS_AUTH_ENABLED` 与短信供应商变量
8. `WECHAT_AUTH_ENABLED` 与微信 OAuth 变量 8. `WECHAT_AUTH_ENABLED` 与微信 OAuth 变量
9. `GENARRATIVE_BACKEND_STACK``NODE_SERVER_TARGET``RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 9. `RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET`
## 5. 灰度与切流 ## 5. 灰度与切流
灰度环境固定为三段: 灰度环境固定为三段:
1. `shadow`Node 继续承接用户流量Rust 只由脚本和内部账号请求 1. `preflight`:只跑 Rust 预检、smoke 与人工主链验证,不接正式用户流量
2. `dual-run`同一组 smoke/API compare 同时打 Node 与 Rust差异必须登记 2. `limited-rust`小范围账号或灰度域名访问 Rust `api-server`,差异必须登记到 M7 验收记录
3. `rust-primary`:反向代理或 Vite dev proxy 指向 RustNode 进程保留但不作为主入口 3. `rust-primary`:反向代理或 Vite dev proxy 指向 Rust `api-server`,旧 Node 后端不作为运行时入口保留
前端切换方式: 前端切换方式:
1. 默认 `GENARRATIVE_BACKEND_STACK=node` 1. 默认使用 `RUST_SERVER_TARGET``GENARRATIVE_API_TARGET` 指向 Rust `api-server`
2. 本地或灰度切 Rust 设置 `GENARRATIVE_BACKEND_STACK=rust`,并配置 `RUST_SERVER_TARGET` 2. 本地或灰度可覆盖 `GENARRATIVE_RUNTIME_SERVER_TARGET`,但目标仍必须是 Rust 兼容服务
3. 紧急回退设置 `GENARRATIVE_BACKEND_STACK=node` 或直接覆盖 `GENARRATIVE_RUNTIME_SERVER_TARGET` 指回 Node 3. 紧急回退优先回滚到上一个 Rust 发布包或反向代理配置,不恢复 `server-node/` 工程目录
## 6. API 对比 ## 6. API 回归
`scripts/m7-api-compare.ts` 负责对比 Node 与 Rust 的基础 contract 第一批删除后不再保留 Node/Rust 对比脚本M7 回归改为 Rust 主线 contract 验证
1. 默认对比 `/healthz``/api/auth/login-options` 1. `server-rs/scripts/m7-preflight.ps1` 覆盖 Rust 工作区构建、测试与关键脚本门禁
2. 可通过 `M7_COMPARE_PATHS` 扩展只读路径清单 2. `server-rs/scripts/smoke.ps1` 覆盖 `/healthz`、envelope 与 request id 基础 contract
3. 对比时会固定传入 `x-request-id`,并归一化 `requestId / timestamp / latencyMs` 等波动字段 3. `server-rs/scripts/oss-smoke.ps1` 覆盖真实 OSS 链路
4. 默认严格模式下发现差异直接返回非零退出码 4. 新增只读 contract 时优先补进 Rust 侧 smoke 或 handler 测试,不恢复 Node 对比脚本
该脚本只承担“无状态 GET contract”对比带登录、写入、OSS 或 SSE 的主流程仍由专门 smoke 脚本负责。 带登录、写入、OSS 或 SSE 的主流程仍由专门 smoke、handler 测试和人工验证清单负责。
## 7. 观测能力 ## 7. 观测能力
@@ -113,10 +112,10 @@ M7 观测字段固定为:
## 8. 数据迁移与回滚 ## 8. 数据迁移与回滚
当前 M7 不做一次性“Node PostgreSQL 全量导入 SpacetimeDB的危险迁移,采用双跑验证与按主链确认的渐进策略: 当前 M7 不做一次性历史数据导入 SpacetimeDB 的危险迁移,采用按主链确认的渐进策略:
1. 已迁移主链以 SpacetimeDB 为真相源。 1. 已迁移主链以 SpacetimeDB 为真相源。
2. 未迁移或灰度失败主链继续回退到 Node。 2. 未迁移或灰度失败主链必须继续迁入 Rust 主线后再开放,不回退到 Node 工程
3. 资产二进制以 OSS 为真相,不回滚到本地 `public/generated-*` 写盘。 3. 资产二进制以 OSS 为真相,不回滚到本地 `public/generated-*` 写盘。
4. 若 SpacetimeDB schema 需要清库重发,只允许在开发库或明确灰度库执行 `--clear-database` 4. 若 SpacetimeDB schema 需要清库重发,只允许在开发库或明确灰度库执行 `--clear-database`
5. 生产回滚优先切反向代理目标,不优先改代码。 5. 生产回滚优先切反向代理目标,不优先改代码。
@@ -128,6 +127,6 @@ M7 完成时必须满足:
1. M7 文档、脚本、任务清单均同步。 1. M7 文档、脚本、任务清单均同步。
2. `api-server``spacetime-module` 至少通过 `cargo check` 2. `api-server``spacetime-module` 至少通过 `cargo check`
3. 基础 smoke 脚本可执行,并覆盖 `healthz + envelope + request id` 3. 基础 smoke 脚本可执行,并覆盖 `healthz + envelope + request id`
4. Node/Rust API 对比脚本可执行。 4. Rust 主线预检和 smoke 脚本可执行。
5. Vite dev proxy 已具备 Node/Rust 切换与回退开关。 5. Vite dev proxy 默认指向 Rust `api-server`,仅保留 Rust 目标覆盖开关。
6. `spacetime-module` 已从单 `lib.rs` 拆为按 `runtime / gameplay / custom_world / asset_metadata / ai` 组织的文件结构。 6. `spacetime-module` 已从单 `lib.rs` 拆为按 `runtime / gameplay / custom_world / asset_metadata / ai` 组织的文件结构。

View File

@@ -1,399 +0,0 @@
# Node 后端模块与接口索引
> 该文档由 `server-node/src/manifest/backendCapabilityManifest.ts` 自动生成。
> 生成命令:`npm run server-node:manifest:backend`
> 生成时间:`2026-04-20T14:26:38.663Z`
>
> 过期说明:该索引生成于 `2026-04-20`,其中 `createQwenSpriteRoutes` 与 `/api/assets/qwen-sprite/*` 相关描述已在 `2026-04-21` 后失效。当前 Node 现役资产挂载面仅保留 `createCharacterAssetRoutes``Qwen` 仅剩 prompt 模板复用与 `/generated-qwen-sprites/*` 历史路径兼容,不再存在独立路由主链。
## 总览
- 对外挂载面6 个
- 已登记路由96 条
- 内部模块目录12 个
- 公开接口10 条
- JWT 接口69 条
- 受环境开关控制的接口17 条
- 流式接口6 条
## 产物
- JSON 清单:`server-node/manifests/backend-capability-index.json`
- Markdown 索引:`docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md`
- Manifest 源:`server-node/src/manifest/backendCapabilityManifest.ts`
## 对外挂载面
### 资产生成工具面
- 标识:`assets`
- 路由数14
- 入口:`server-node/src/app.ts -> /api/assets -> createCharacterAssetRoutes``server-node/src/app.ts -> /api/assets/qwen-sprite -> createQwenSpriteRoutes`
- 关联模块:`assets`
- 责任:
- 生成角色主形象、动作、动作模板与工作流缓存。
- 承接 Qwen 精灵表主图、整表、修帧与保存链路。
- 把产物发布到 `public/generated-*` 目录并落地局部 manifest。
- 主要服务边界:
- 负责对接 DashScope、Ark 等外部媒体供应商,但不维护 runtime 快照与业务状态。
- 统一受 `ASSETS_API_ENABLED` 开关控制,产物以文件与 JSON manifest 形式落在仓库工作区。
### 鉴权与会话面
- 标识:`auth`
- 路由数17
- 入口:`server-node/src/app.ts -> /api/auth -> createAuthRoutes`
- 关联模块:无
- 责任:
- 承接本地账号、短信验证码与微信登录流程。
- 管理 refresh session、用户信息、会话吊销、审计日志与风险拦截。
- 主要服务边界:
- HTTP 层只做 schema 校验、请求上下文拼装与 Cookie 管理,核心鉴权逻辑统一收口到 `server-node/src/auth/*`
- 用户、身份、会话、风控与短信事件等持久化职责全部下沉到 repository 层,避免路由直接碰数据库细节。
### 编辑器工具面
- 标识:`editor`
- 路由数3
- 入口:`server-node/src/app.ts -> /api/editor -> createEditorRoutes`
- 关联模块:`editor`
- 责任:
- 读取编辑器资源 JSON。
- 回写编辑器覆盖文件。
- 枚举 `public/Icons` 下的物品图标资源。
- 主要服务边界:
- 只对工作区文件系统与 `public` 目录负责,不参与运行时数据库存储。
- 统一受 `EDITOR_API_ENABLED` 开关控制,生产环境可按需关闭。
### 基础健康检查
- 标识:`health`
- 路由数1
- 入口:`server-node/src/app.ts -> /healthz -> createApp`
- 关联模块:无
- 责任:
- 提供 Node 后端进程级健康探针。
- 给反向代理、部署平台和本地联调提供最小可用状态确认。
- 主要服务边界:
- 只返回服务静态信息,不触达数据库、鉴权或外部模型供应商。
### 运行时主能力面
- 标识:`runtime-main`
- 路由数59
- 入口:`server-node/src/app.ts -> /api -> createRuntimeRoutes``server-node/src/routes/runtimeRoutes.ts -> /runtime/custom-world/agent -> createCustomWorldAgentRoutes`
- 关联模块:`ai``custom-world``quest``runtime``runtime-item``story`
- 责任:
- 承接运行时资料库、公开画廊、存档、设置与个人档案接口。
- 承接剧情生成、聊天流、任务生成、运行时物品意图与自定义世界链路。
- 承接 Custom World Agent 会话、消息流和操作回放。
- 主要服务边界:
- HTTP contract 收口在 `runtimeRoutes.ts`,真正的世界生成、剧情、聊天、任务和资源逻辑继续下沉到 `services/*``src/modules/*`
- 除公开画廊外,运行时接口统一走 JWT 鉴权,并依赖 `runtimeRepository`、session store 与 LLM client 执行。
### 运行时 Story Action 面
- 标识:`runtime-story-action`
- 路由数2
- 入口:`server-node/src/app.ts -> /api/runtime/story -> createStoryActionRoutes`
- 关联模块:`story``quest``inventory``runtime-item``npc``progression``combat``runtime`
- 责任:
- 把前端 story choice 动作解析为新的运行时状态。
- 查询指定 story session 的可恢复状态。
- 主要服务边界:
- 路由层只做鉴权与 schema 校验,真正的动作分发与跨模块协作集中在 `storyActionService.ts`
- Story Action 会联动 quest、inventory、runtime-item、npc 等内部模块,但对前端只暴露 story 这一条稳定入口。
## 接口索引
| 方法 | 路径 | 访问 | 响应 | 挂载面 | 内部模块 | 说明 |
| --- | --- | --- | --- | --- | --- | --- |
| POST | `/api/assets/character-animation/generate` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 生成角色动作草稿。 |
| POST | `/api/assets/character-animation/import-video` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 导入动作参考视频并转为可消费素材。 |
| GET | `/api/assets/character-animation/jobs/:taskId` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 查询角色动作生成任务状态。 |
| POST | `/api/assets/character-animation/publish` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 发布角色动作帧集到 public 目录。 |
| GET | `/api/assets/character-animation/templates` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 列出内置角色动作模板。 |
| POST | `/api/assets/character-visual/generate` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 生成角色主形象候选图。 |
| GET | `/api/assets/character-visual/jobs/:taskId` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 查询角色主形象生成任务状态。 |
| POST | `/api/assets/character-visual/publish` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 发布选中的角色主形象到 public 目录。 |
| POST | `/api/assets/character-workflow-cache` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 保存角色资产工作流缓存。 |
| GET | `/api/assets/character-workflow-cache/:characterId` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 按角色读取角色资产工作流缓存。 |
| POST | `/api/assets/qwen-sprite/frame-repair` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 对单帧做 Qwen 修复。 |
| POST | `/api/assets/qwen-sprite/master` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 生成 Qwen 精灵主图。 |
| POST | `/api/assets/qwen-sprite/save` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 保存 Qwen 精灵资产到 public 目录。 |
| POST | `/api/assets/qwen-sprite/sheet` | 开关: ASSETS_API_ENABLED | json | `assets` | `assets` | 生成 Qwen 精灵表。 |
| GET | `/api/auth/audit-logs` | JWT | json | `auth` | 无 | 查询当前账号的鉴权审计日志。 |
| POST | `/api/auth/entry` | 公开 | json | `auth` | 无 | 用户名密码登录;不存在则创建本地账号。 |
| GET | `/api/auth/login-options` | 公开 | json | `auth` | 无 | 返回当前启用的登录方式与入口配置。 |
| POST | `/api/auth/logout` | JWT | json | `auth` | 无 | 退出当前会话并清理 refresh cookie。 |
| POST | `/api/auth/logout-all` | JWT | json | `auth` | 无 | 退出当前账号的全部会话。 |
| GET | `/api/auth/me` | JWT | json | `auth` | 无 | 读取当前登录用户的鉴权资料。 |
| POST | `/api/auth/phone/change` | JWT | json | `auth` | 无 | 已登录用户更换绑定手机号。 |
| POST | `/api/auth/phone/login` | 公开 | json | `auth` | 无 | 手机号验证码登录。 |
| POST | `/api/auth/phone/send-code` | 公开 | json | `auth` | 无 | 发送手机号登录或绑定验证码。 |
| POST | `/api/auth/refresh` | 公开 | json | `auth` | 无 | 使用 refresh session 刷新 JWT。 |
| GET | `/api/auth/risk-blocks` | JWT | json | `auth` | 无 | 查询当前用户命中的风控封禁。 |
| POST | `/api/auth/risk-blocks/:scopeType/lift` | JWT | json | `auth` | 无 | 请求解除指定维度的风控拦截。 |
| GET | `/api/auth/sessions` | JWT | json | `auth` | 无 | 列出当前账号的活跃会话。 |
| POST | `/api/auth/sessions/:sessionId/revoke` | JWT | json | `auth` | 无 | 吊销指定会话。 |
| POST | `/api/auth/wechat/bind-phone` | JWT | json | `auth` | 无 | 为已登录微信账号绑定手机号。 |
| GET | `/api/auth/wechat/callback` | 公开 | redirect | `auth` | 无 | 处理微信回调并重定向回前端。 |
| GET | `/api/auth/wechat/start` | 公开 | json | `auth` | 无 | 发起微信登录并返回授权 URL。 |
| POST | `/api/custom-world/cover-image` | JWT | json | `runtime-main` | `custom-world``assets` | 生成自定义世界封面图。 |
| POST | `/api/custom-world/cover-upload` | JWT | json | `runtime-main` | `custom-world``assets` | 上传并落地自定义世界封面图。 |
| POST | `/api/custom-world/entity` | JWT | json | `runtime-main` | `custom-world``ai` | 按世界 profile 生成单个角色或地标实体。 |
| POST | `/api/custom-world/scene-image` | JWT | json | `runtime-main` | `custom-world``assets` | 生成自定义世界场景图。 |
| POST | `/api/custom-world/scene-npc` | JWT | json | `runtime-main` | `custom-world``ai``npc` | 按地标生成场景 NPC。 |
| GET | `/api/editor/catalog/items` | 开关: EDITOR_API_ENABLED | json | `editor` | `editor` | 列出 `public/Icons` 下的物品图标资源。 |
| GET | `/api/editor/json/:resourceId` | 开关: EDITOR_API_ENABLED | json | `editor` | `editor` | 读取指定编辑器资源 JSON。 |
| POST | `/api/editor/json/:resourceId` | 开关: EDITOR_API_ENABLED | json | `editor` | `editor` | 回写指定编辑器资源 JSON。 |
| POST | `/api/llm/chat/completions` | JWT | proxy | `runtime-main` | `ai` | 把聊天补全请求透传到上游模型。 |
| DELETE | `/api/profile/browse-history` | JWT | json | `runtime-main` | `runtime` | 清空平台浏览历史。 |
| GET | `/api/profile/browse-history` | JWT | json | `runtime-main` | `runtime` | 读取平台浏览历史。 |
| POST | `/api/profile/browse-history` | JWT | json | `runtime-main` | `runtime` | 写入或批量同步平台浏览历史。 |
| GET | `/api/profile/dashboard` | JWT | json | `runtime-main` | `runtime` | 读取运行时个人主页汇总。 |
| GET | `/api/profile/play-stats` | JWT | json | `runtime-main` | `runtime` | 读取个人游玩统计。 |
| GET | `/api/profile/save-archives` | JWT | json | `runtime-main` | `runtime` | 列出个人存档摘要。 |
| POST | `/api/profile/save-archives/:worldKey` | JWT | json | `runtime-main` | `runtime` | 恢复指定世界的最近存档。 |
| GET | `/api/profile/wallet-ledger` | JWT | json | `runtime-main` | `runtime` | 列出个人资产流水。 |
| POST | `/api/runtime/chat/character/reply/stream` | JWT | stream | `runtime-main` | `ai``story` | 流式生成角色回复。 |
| POST | `/api/runtime/chat/character/suggestions` | JWT | json | `runtime-main` | `ai``story` | 生成角色聊天建议语。 |
| POST | `/api/runtime/chat/character/summary` | JWT | json | `runtime-main` | `ai``story` | 生成角色聊天摘要。 |
| POST | `/api/runtime/chat/npc/dialogue/stream` | JWT | stream | `runtime-main` | `ai``npc``story` | 流式生成 NPC 对话。 |
| POST | `/api/runtime/chat/npc/recruit/stream` | JWT | stream | `runtime-main` | `ai``npc``story` | 流式生成招募 NPC 对话。 |
| POST | `/api/runtime/chat/npc/turn/stream` | JWT | stream | `runtime-main` | `ai``npc``story` | 流式生成 NPC 单回合发言。 |
| GET | `/api/runtime/custom-world-gallery` | 公开 | json | `runtime-main` | `custom-world``runtime` | 列出公开的自定义世界画廊。 |
| GET | `/api/runtime/custom-world-gallery/:ownerUserId/:profileId` | 公开 | json | `runtime-main` | `custom-world``runtime` | 读取指定公开世界作品详情。 |
| GET | `/api/runtime/custom-world-library` | JWT | json | `runtime-main` | `custom-world``runtime` | 列出当前账号的自定义世界资料库。 |
| DELETE | `/api/runtime/custom-world-library/:profileId` | JWT | json | `runtime-main` | `custom-world``runtime` | 删除指定自定义世界 profile。 |
| PUT | `/api/runtime/custom-world-library/:profileId` | JWT | json | `runtime-main` | `custom-world``runtime` | 写入或更新指定自定义世界 profile。 |
| POST | `/api/runtime/custom-world-library/:profileId/publish` | JWT | json | `runtime-main` | `custom-world``runtime` | 发布指定世界到公开画廊。 |
| POST | `/api/runtime/custom-world-library/:profileId/unpublish` | JWT | json | `runtime-main` | `custom-world``runtime` | 撤回指定世界的公开发布状态。 |
| POST | `/api/runtime/custom-world/agent/sessions` | JWT | json | `runtime-main` | `custom-world``ai` | 创建 Custom World Agent 会话。 |
| GET | `/api/runtime/custom-world/agent/sessions/:sessionId` | JWT | json | `runtime-main` | `custom-world``ai` | 读取 Agent 会话快照。 |
| POST | `/api/runtime/custom-world/agent/sessions/:sessionId/actions` | JWT | json | `runtime-main` | `custom-world``ai``assets` | 执行 Agent 卡片生成、资产同步或发布动作。 |
| GET | `/api/runtime/custom-world/agent/sessions/:sessionId/cards/:cardId` | JWT | json | `runtime-main` | `custom-world``ai` | 读取 Agent 卡片详情。 |
| POST | `/api/runtime/custom-world/agent/sessions/:sessionId/messages` | JWT | json | `runtime-main` | `custom-world``ai` | 向 Agent 会话提交一条创作消息。 |
| POST | `/api/runtime/custom-world/agent/sessions/:sessionId/messages/stream` | JWT | stream | `runtime-main` | `custom-world``ai` | 流式提交 Agent 消息并实时接收回执。 |
| GET | `/api/runtime/custom-world/agent/sessions/:sessionId/operations/:operationId` | JWT | json | `runtime-main` | `custom-world``ai` | 查询 Agent 后台操作状态。 |
| POST | `/api/runtime/custom-world/entity` | JWT | json | `runtime-main` | `custom-world``ai` | 按世界 profile 生成单个角色或地标实体(兼容路径)。 |
| POST | `/api/runtime/custom-world/scene-npc` | JWT | json | `runtime-main` | `custom-world``ai``npc` | 按地标生成场景 NPC兼容路径。 |
| POST | `/api/runtime/custom-world/sessions` | JWT | json | `runtime-main` | `custom-world` | 创建传统自定义世界问答会话。 |
| GET | `/api/runtime/custom-world/sessions/:sessionId` | JWT | json | `runtime-main` | `custom-world` | 读取传统自定义世界问答会话。 |
| POST | `/api/runtime/custom-world/sessions/:sessionId/answers` | JWT | json | `runtime-main` | `custom-world` | 回答传统自定义世界问答题目。 |
| GET | `/api/runtime/custom-world/sessions/:sessionId/generate/stream` | JWT | stream | `runtime-main` | `custom-world``ai` | 流式编译传统自定义世界 profile。 |
| GET | `/api/runtime/custom-world/works` | JWT | json | `runtime-main` | `custom-world``runtime` | 列出当前账号的自定义世界作品汇总。 |
| POST | `/api/runtime/items/runtime-intent` | JWT | json | `runtime-main` | `runtime-item``ai` | 生成运行时物品意图。 |
| DELETE | `/api/runtime/profile/browse-history` | JWT | json | `runtime-main` | `runtime` | 清空平台浏览历史。(兼容路径) |
| GET | `/api/runtime/profile/browse-history` | JWT | json | `runtime-main` | `runtime` | 读取平台浏览历史。(兼容路径) |
| POST | `/api/runtime/profile/browse-history` | JWT | json | `runtime-main` | `runtime` | 写入或批量同步平台浏览历史。(兼容路径) |
| GET | `/api/runtime/profile/dashboard` | JWT | json | `runtime-main` | `runtime` | 读取运行时个人主页汇总。(兼容路径) |
| GET | `/api/runtime/profile/play-stats` | JWT | json | `runtime-main` | `runtime` | 读取个人游玩统计。(兼容路径) |
| GET | `/api/runtime/profile/save-archives` | JWT | json | `runtime-main` | `runtime` | 列出个人存档摘要。(兼容路径) |
| POST | `/api/runtime/profile/save-archives/:worldKey` | JWT | json | `runtime-main` | `runtime` | 恢复指定世界的最近存档(兼容路径)。 |
| GET | `/api/runtime/profile/wallet-ledger` | JWT | json | `runtime-main` | `runtime` | 列出个人资产流水。(兼容路径) |
| POST | `/api/runtime/quests/generate` | JWT | json | `runtime-main` | `quest``ai` | 按当前遭遇生成任务候选。 |
| DELETE | `/api/runtime/save/snapshot` | JWT | json | `runtime-main` | `runtime` | 删除当前用户的运行时存档。 |
| GET | `/api/runtime/save/snapshot` | JWT | json | `runtime-main` | `runtime``progression``quest` | 读取当前用户的运行时存档。 |
| PUT | `/api/runtime/save/snapshot` | JWT | json | `runtime-main` | `runtime``progression``quest` | 保存并归一化当前运行时存档。 |
| GET | `/api/runtime/settings` | JWT | json | `runtime-main` | `runtime` | 读取运行时设置。 |
| PUT | `/api/runtime/settings` | JWT | json | `runtime-main` | `runtime` | 更新运行时设置。 |
| POST | `/api/runtime/story/actions/resolve` | JWT | json | `runtime-story-action` | `story``quest``inventory``runtime-item``npc``progression``combat``runtime` | 解析前端 story choice 动作为新的运行时结果。 |
| POST | `/api/runtime/story/continue` | JWT | json | `runtime-main` | `story``ai` | 生成下一段故事内容。 |
| POST | `/api/runtime/story/initial` | JWT | json | `runtime-main` | `story``ai` | 生成首段故事内容。 |
| GET | `/api/runtime/story/state/:sessionId` | JWT | json | `runtime-story-action` | `story``runtime` | 读取指定 story session 的运行时状态。 |
| GET | `/api/ws/health` | JWT | json | `runtime-main` | `runtime` | 保留给未来实时链路的占位健康检查。 |
| GET | `/healthz` | 公开 | json | `health` | 无 | 返回 Node 后端进程健康状态。 |
## 内部模块边界
### AI 编排模块
- 标识:`ai`
- 目录:`server-node/src/modules/ai`
- 对外可见面:`runtime-main`
- 关联路由数23
- 职责:
- 统一剧情、多轮聊天与自定义世界编排器的 prompt 构造与输出归一化。
- 屏蔽前端对不同 AI 链路的直接拼装细节。
- 主要服务边界:
- 专注提示词与编排,不负责持久化与 HTTP 传输。
- 通过 `services/llmClient.ts` 与外部模型交互,由路由与 service 层决定何时调用。
- 关键文件:
- `server-node/src/modules/ai/chatOrchestrator.ts`
- `server-node/src/modules/ai/customWorldOrchestrator.ts`
- `server-node/src/modules/ai/storyOrchestrator.ts`
### 资产工具模块
- 标识:`assets`
- 目录:`server-node/src/modules/assets`
- 对外可见面:`assets`
- 关联路由数18
- 职责:
- 承接角色资产与 Qwen 精灵表的生成、查询、发布和保存。
- 维护资产流程需要的缓存、草稿与产物 manifest。
- 主要服务边界:
- 以文件系统和外部媒体模型为主要边界,不碰 runtimeRepository。
- 对外暴露稳定 HTTP 路径,对内通过私有 helper 处理媒体编码、任务轮询与写盘。
- 关键文件:
- `server-node/src/modules/assets/characterAssetRoutes.ts`
- `server-node/src/modules/assets/qwenSpriteRoutes.ts`
### 战斗结算模块
- 标识:`combat`
- 目录:`server-node/src/modules/combat`
- 对外可见面:`runtime-story-action`
- 关联路由数1
- 职责:
- 提供运行时战斗结算与数值变更能力。
- 为 story action 里的战斗型交互提供纯计算服务。
- 主要服务边界:
- 聚焦状态推导与结果计算,不负责 transport 与持久化。
- 关键文件:
- `server-node/src/modules/combat/combatResolutionService.ts`
### 自定义世界运行时模块
- 标识:`custom-world`
- 目录:`server-node/src/modules/custom-world`
- 对外可见面:`runtime-main`
- 关联路由数26
- 职责:
- 规范 creator intent、世界运行时类型与 profile compile。
- 把世界创作输入整理成运行时可消费的数据结构。
- 主要服务边界:
- 偏纯领域建模与 compile不直接做 HTTP、数据库查询或模型调用。
- 关键文件:
- `server-node/src/modules/custom-world/creatorIntentRuntime.ts`
- `server-node/src/modules/custom-world/runtimeProfile.ts`
- `server-node/src/modules/custom-world/runtimeTypes.ts`
### 编辑器资源模块
- 标识:`editor`
- 目录:`server-node/src/modules/editor`
- 对外可见面:`editor`
- 关联路由数3
- 职责:
- 提供编辑器资源目录枚举与 JSON 读写入口。
- 主要服务边界:
- 只负责工作区文件输入输出,不参与运行时业务计算。
- 关键文件:
- `server-node/src/modules/editor/editorRoutes.ts`
### 背包与物品变更模块
- 标识:`inventory`
- 目录:`server-node/src/modules/inventory`
- 对外可见面:`runtime-story-action`
- 关联路由数1
- 职责:
- 维护背包变更、NPC 背包交互与 story action 里的物品副作用。
- 主要服务边界:
- 对运行时状态做局部变更,不直接暴露 HTTP 路由。
- 关键文件:
- `server-node/src/modules/inventory/inventoryMutationService.ts`
- `server-node/src/modules/inventory/inventoryStoryActionService.ts`
- `server-node/src/modules/inventory/npcInventoryStoryActionService.ts`
### NPC 交互模块
- 标识:`npc`
- 目录:`server-node/src/modules/npc`
- 对外可见面:`runtime-story-action``runtime-main`
- 关联路由数6
- 职责:
- 维护 NPC 互动规则、任务 primitive 与关系变更逻辑。
- 主要服务边界:
- 专注 NPC 侧状态推导,供 story action 与聊天/任务链路复用。
- 关键文件:
- `server-node/src/modules/npc/npcInteractionService.ts`
- `server-node/src/modules/npc/npcTask6Primitives.ts`
### 成长与关卡进程模块
- 标识:`progression`
- 目录:`server-node/src/modules/progression`
- 对外可见面:`runtime-story-action``runtime-main`
- 关联路由数3
- 职责:
- 提供角色成长、敌对等级、章节推进与 benchmark 逻辑。
- 主要服务边界:
- 只做成长数值与章节进度计算,由 runtime hydrate 与 story action 复用。
- 关键文件:
- `server-node/src/modules/progression/playerProgressionService.ts`
- `server-node/src/modules/progression/hostileProgressionService.ts`
- `server-node/src/modules/progression/chapterProgressionPlanner.ts`
### 任务运行时模块
- 标识:`quest`
- 目录:`server-node/src/modules/quest`
- 对外可见面:`runtime-main``runtime-story-action`
- 关联路由数4
- 职责:
- 生成任务意图、维护任务日志与处理任务进度信号。
- 为运行时 quest 接口与 story action 提供统一任务语义。
- 主要服务边界:
- 领域逻辑以 quest module 为中心AI 生成只是一种输入来源。
- 不直接处理 HTTP 响应,统一由 routes/service 层调用。
- 关键文件:
- `server-node/src/modules/quest/runtimeQuestModule.ts`
- `server-node/src/modules/quest/questProgressionService.ts`
- `server-node/src/modules/quest/questStoryActionService.ts`
### 运行时状态基座模块
- 标识:`runtime`
- 目录:`server-node/src/modules/runtime`
- 对外可见面:`runtime-main``runtime-story-action`
- 关联路由数32
- 职责:
- 定义运行时状态 primitive、经济与装备规则。
- 负责存档 hydration、兼容迁移与状态归一化。
- 主要服务边界:
- 是 runtimeRepository 与 story action 的共同状态基座,不承担 HTTP 入口职责。
- 关键文件:
- `server-node/src/modules/runtime/runtimeSnapshotHydration.ts`
- `server-node/src/modules/runtime/runtimeStatePrimitives.ts`
- `server-node/src/modules/runtime/runtimeEquipmentModule.ts`
### 运行时物品模块
- 标识:`runtime-item`
- 目录:`server-node/src/modules/runtime-item`
- 对外可见面:`runtime-main``runtime-story-action`
- 关联路由数2
- 职责:
- 生成运行时物品意图、物品奖励与剧情指纹。
- 维护宝藏与物品解析逻辑。
- 主要服务边界:
- 聚焦物品领域编译与奖励拼装,由 route/service 选择具体触发时机。
- 关键文件:
- `server-node/src/modules/runtime-item/runtimeItemModule.ts`
- `server-node/src/modules/runtime-item/runtimeItemResolutionService.ts`
- `server-node/src/modules/runtime-item/treasureStoryActionService.ts`
### 故事会话模块
- 标识:`story`
- 目录:`server-node/src/modules/story`
- 对外可见面:`runtime-main``runtime-story-action`
- 关联路由数10
- 职责:
- 维护运行时故事会话状态与 action 分发。
- 为 story resolve、story state 查询提供统一入口。
- 主要服务边界:
- story 模块是 runtime 主循环的编排层,必要时再向 quest、inventory、combat 等领域模块分发。
- 关键文件:
- `server-node/src/modules/story/runtimeSession.ts`
- `server-node/src/modules/story/storyActionRoutes.ts`
- `server-node/src/modules/story/storyActionService.ts`
## 维护规则
- 新增 `server-node/src/modules/*` 目录时,必须先补充 manifest 里的模块说明,再重新生成产物。
- 新增或下线路由时,先更新 manifest 里的路由清单,再运行生成命令同步 JSON 与文档。
- 如果路由来自兼容路径或中间件派生路径,`sourceHint` 需要指向源代码里的真实表达式,确保生成脚本能做最小校验。

View File

@@ -1,245 +0,0 @@
# Node 后端知识图谱
日期:`2026-04-08`
## 1. 当前定位
当前运行时后端以 `server-node/` 为唯一有效服务端实现。
当前职责:
- 承接运行时鉴权
- 承接运行时持久化
- 承接运行时 AI 接口
- 承接编辑器写盘与资产生成工具接口
- 为 Vite 前端提供开发期代理目标
当前不再使用:
- 已删除的旧 Vite 本地 API 插件链路 `scripts/dev-server/*.ts`
## 2. 技术栈
- HTTP 框架:`Express`
- 语言与构建:`TypeScript` + `tsx` + `esbuild`
- 数据库:`PostgreSQL`
- JWT`jose`
- 密码哈希:`@node-rs/argon2`
- 日志:`pino` + `pino-http` + `pino-roll`
## 3. 运行入口
推荐命令:
```bash
npm run dev
```
相关脚本:
- 根目录联调:`npm run dev` / `npm run dev:node`
- 仅前端开发:`npm run dev:web`
- 单独启动后端开发模式:`npm run server-node:dev`
- 构建后端:`npm run server-node:build`
- 运行后端测试:`npm run server-node:test`
默认监听:
- 前端:`3000`
- Node 后端:`8081`
## 4. 目录与主入口
服务端主入口:
- `server-node/src/server.ts`
- `server-node/src/app.ts`
路由入口:
- `server-node/src/routes/authRoutes.ts`
- `server-node/src/routes/runtimeRoutes.ts`
- `server-node/src/modules/editor/editorRoutes.ts`
- `server-node/src/modules/assets/characterAssetRoutes.ts`
- `server-node/src/modules/assets/qwenSpriteRoutes.ts`
基础设施:
- `server-node/src/config.ts`
- `server-node/src/logging.ts`
- `server-node/src/db.ts`
- `server-node/src/context.ts`
数据访问:
- `server-node/src/repositories/userRepository.ts`
- `server-node/src/repositories/runtimeRepository.ts`
鉴权相关:
- `server-node/src/auth/authService.ts`
- `server-node/src/auth/token.ts`
- `server-node/src/auth/password.ts`
- `server-node/src/middleware/auth.ts`
## 5. 鉴权模型
当前采用:
- 前端本地保存 `JWT + 自动生成的用户名密码`
- 请求头使用 `Authorization: Bearer <token>`
- 后端 middleware 统一解析出 `UserID`
- handler 不直接解析 token
当前账号策略:
- 默认自动匿名账号启动
- 本地无 JWT 时,前端会自动生成随机用户名密码并调用 `POST /api/auth/entry`
- 本地 JWT 失效但仍保留随机凭据时,前端自动重新调用 `auth/entry` 恢复同一账号
JWT 现状:
- 当前为永久签发
- claim 仍保留:`sub``iat``iss``ver`
- `logout` 通过递增 `token_version` 立即失效旧 token
## 6. 数据存储
当前数据库:
- 当前运行时持久化已切换到 `PostgreSQL`
- 连接信息由后端 `DATABASE_URL` 环境变量注入
- 不再以本地 `SQLite` 文件作为正式运行时数据库
- 后端测试默认使用 `pg-mem` 作为内存级 PostgreSQL 兼容实现
- 基础表初始化通过 `schema_migrations` 基线管理
- 可通过 `npm run server-node:db:migrate` 主动校验并补齐数据库基线
当前核心表:
- `users`
- `save_snapshots`
- `runtime_settings`
- `custom_world_profiles`
当前隔离原则:
- 所有运行时数据按用户隔离
## 7. 已承接接口
鉴权:
- `POST /api/auth/entry`
- `GET /api/auth/me`
- `POST /api/auth/logout`
运行时持久化:
- `GET /api/runtime/save/snapshot`
- `PUT /api/runtime/save/snapshot`
- `DELETE /api/runtime/save/snapshot`
- `GET /api/runtime/settings`
- `PUT /api/runtime/settings`
- `GET /api/runtime/custom-world-library`
- `PUT /api/runtime/custom-world-library/:profileId`
- `DELETE /api/runtime/custom-world-library/:profileId`
运行时 AI
- `POST /api/llm/chat/completions`
- `POST /api/custom-world/scene-image`
- `POST /api/runtime/story/initial`
- `POST /api/runtime/story/continue`
- `POST /api/runtime/custom-world/agent/sessions`
- `GET /api/runtime/custom-world/agent/sessions/:sessionId`
- `POST /api/runtime/custom-world/agent/sessions/:sessionId/messages`
- `POST /api/runtime/custom-world/agent/sessions/:sessionId/actions`
- `GET /api/runtime/custom-world/works`
- `POST /api/runtime/chat/character/suggestions`
- `POST /api/runtime/chat/character/summary`
- `POST /api/runtime/chat/character/reply/stream`
- `POST /api/runtime/chat/npc/dialogue/stream`
- `POST /api/runtime/chat/npc/recruit/stream`
- `POST /api/runtime/items/runtime-intent`
- `POST /api/runtime/quests/generate`
补充说明(`2026-04-19`
- `POST /api/custom-world/scene-image` 现在支持前端仅提交 `profile + landmark + userPrompt` 上下文,由后端统一补齐场景图 prompt 与默认 negative prompt。
- runtime story option 的 `interaction` 元数据现在由后端随 option 一并返回,前端不再本地按 `functionId` 重建 NPC / treasure 交互语义。
编辑器工具:
- `GET /api/editor/catalog/items`
- `GET /api/editor/json/:resourceId`
- `POST /api/editor/json/:resourceId`
资产工具:
- `POST /api/assets/character-visual/generate`
- `POST /api/assets/character-visual/publish`
- `GET /api/assets/character-visual/jobs/:taskId`
- `POST /api/assets/character-animation/generate`
- `POST /api/assets/character-animation/publish`
- `GET /api/assets/character-animation/jobs/:taskId`
- `POST /api/assets/character-animation/import-video`
- `GET /api/assets/character-animation/templates`
编辑器与资产接口门禁:
- `EDITOR_API_ENABLED` 控制 `/api/editor/*`
- `ASSETS_API_ENABLED` 控制 `/api/assets/*`
- 非生产环境默认开启,生产环境默认关闭
## 8. Story 与 Custom World 现状
Story
- Node 后端直接复用前端成熟 prompt 与归一化逻辑
- 服务端走 `src/services/ai.ts` 中的严格版 story 生成链
Custom World
- Node 后端当前已自持 `server-node/src/modules/custom-world/**` 运行时模块
- 已承接 `creator intent` 归一化、`anchorPack / lockState` 推导、framework normalize、runtime profile compile
- `customWorldOrchestrator``customWorldAgentFoundationDraftService` 已不再运行时 import 前端 `src/services/customWorld*.ts``src/types.js`
- `server-node/src/prompts/customWorldPrompts.ts` 已承接 foundation draft 与 scene image 使用的 custom world prompt source
- 上述 prompt 迁移只改变源码归属位置,没有改动提示词正文
- 当前保留 `session + answers + SSE progress/result/error` 协议
- 前端已支持接收真实阶段进度对象
## 9. 前端接入点
鉴权与请求:
- `src/services/apiClient.ts`
- `src/services/authService.ts`
- `src/components/auth/AuthGate.tsx`
运行时服务层:
- `src/services/storageService.ts`
- `src/services/aiService.ts`
编辑器与资产工具层:
- `src/editor/shared/editorApiClient.ts`
- `src/components/preset-editor/characterAssetStudioPersistence.ts`
## 10. 当前 Vite 角色
Vite 当前只负责代理,不再提供本地 API 插件。
当前代理目标:
- `/api/auth`
- `/api/runtime`
- `/api/editor`
- `/api/assets`
- `/api/llm`
- `/api/custom-world/scene-image`
- `/api/ws`
全部转发到 Node 后端。
`scripts/dev-server/` 目录现仅保留 README 作为迁移说明,旧本地 API 实现代码已于 `2026-04-19` 删除。

View File

@@ -1,226 +0,0 @@
# Node 后端测试、观测与部署基线
日期:`2026-04-08`
## 1. 文档目标
这份文档用于落实 `EXPRESS_BACKEND_PARALLEL_WORKSTREAM_PLAN_2026-04-08.md` 中的任务 9
- 给 Node 后端补最小自动回归
- 给请求链路补最小可追踪基线
- 给部署、回滚、迁移补最小操作清单
当前目标不是一次性把监控平台、CI/CD、容器编排全部做完而是先确保
- 后端改动后有脚本能快速验证主链路
- 线上或联调失败时能快速定位到具体请求
- 发布前后有统一检查口径
## 2. 当前基线命令
推荐在仓库根目录执行:
```bash
npm run server-node:db:migrate
npm run server-node:test:baseline
npm run server-node:smoke
npm run server-node:smoke:proxy
```
说明:
- `npm run server-node:db:migrate`
- 用当前 `DATABASE_URL` 主动校验 PostgreSQL 基线是否可连接、可初始化
- 会确保 `schema_migrations` 和运行时基础表已补齐
- `npm run server-node:test:baseline`
- 当前先固定为任务 9 自己维护的观测基线测试
- 已覆盖 `requestId` 回传、访问日志字段、错误日志链路
- `npm run server-node:smoke`
- 启动一套基于 `pg-mem` 的临时 Express 服务
- 不依赖本地 PostgreSQL
- 走真实 HTTP 调用验证 `healthz -> auth -> runtime save/settings -> logout`
- `npm run server-node:smoke:proxy`
- 基于已构建的 `dist + server-node/dist`
- 自动拉起 `server-node + 同域反向代理 harness`
-`pg-mem` 跑同域反向代理链路 smoke
- 验证 `web -> reverse proxy -> /api/* -> server-node` 主链路
如果要一口气跑完整发布前基线,可执行:
```bash
npm run server-node:check:deploy
```
补充说明:
- `npm run server-node:test` 仍然可以继续作为更大范围的后端接口套件入口
- 但它会跟随其他并行任务一起变化,不应替代任务 9 自己的稳定基线
## 2.1 任务 9 对照清单
当前按并行任务规划中的任务 9 逐项对照:
- 后端接口测试:`npm run server-node:test`
- 关键主链路 smoke`npm run server-node:smoke`
- request/response 日志校验:`npm run server-node:test:baseline`
- 同域部署基线:本文第 6 节与 `npm run server-node:smoke:proxy`
- 反向代理 smoke 测试脚本:`scripts/smoke-same-origin-stack.ts`
- 回滚、备份、迁移检查清单:本文第 8 节与 `npm run server-node:db:migrate`
- 发布前一键检查:`npm run server-node:check:deploy`
## 3. 当前 smoke 覆盖范围
当前 smoke 脚本验证以下链路:
- `GET /healthz`
- `POST /api/auth/entry`
- `GET /api/auth/me`
- `PUT /api/runtime/save/snapshot`
- `GET /api/runtime/save/snapshot`
- `PUT /api/runtime/settings`
- `GET /api/runtime/settings`
- `DELETE /api/runtime/save/snapshot`
- `POST /api/auth/logout`
当前代理 smoke 额外验证:
- `GET /`
- `GET /healthz`(本地反向代理健康探针)
- `POST /api/auth/entry` 经反代可用
- `GET /api/auth/me` 经反代可用
- `PUT /api/runtime/save/snapshot` 经反代可用
- `GET /api/runtime/save/snapshot` 经反代可用
- `X-Request-Id` 能穿过反向代理返回给调用方
当前 smoke 的定位是:
- 优先覆盖后端化后最容易断的基础链路
- 优先覆盖前端最依赖的鉴权和持久化能力
- 不把 AI 上游依赖拉进最小回归集,避免把第三方波动误判成主链路回归
## 4. 当前观测基线
当前请求链路至少要满足以下约束:
- 支持读取外部传入的 `X-Request-Id`
- 如果调用方没有传 `X-Request-Id`,后端自动生成
- 响应头回写 `x-request-id`
- 访问日志至少包含:
- `request_id`
- `user_id`
- `method`
- `path`
- `status`
- `latency_ms`
- 错误日志至少包含:
- `request_id`
- `user_id`
- `err`
当前目的很明确:
- 浏览器、反向代理、Node 后端至少有一个共同可追踪的请求标识
- 接口失败后,能从日志里快速找到对应请求
## 5. 部署前检查清单
发布前至少执行一次:
- `npm run check:encoding`
- `npm run server-node:db:migrate`
- `npm run server-node:test:baseline`
- `npm run server-node:smoke`
- `npm run server-node:build`
- `npm run build`
- `npm run server-node:smoke:proxy`
环境变量至少确认:
- `DATABASE_URL`
- `JWT_SECRET`
- `NODE_SERVER_ADDR`
- `LOG_LEVEL`
- `LLM_API_KEY``ARK_API_KEY`
- `DASHSCOPE_API_KEY`
部署前数据库检查:
- 确认目标 PostgreSQL 可连接
- 确认发布账号具备建表或执行初始化所需权限
- 确认已执行 `npm run server-node:db:migrate` 或等效迁移步骤
- 确认现网数据已完成备份
## 6. 同域部署基线
当前推荐仍然是同域部署:
- Web 静态资源:`https://game.example.com/`
- Node API`https://game.example.com/api/*`
最小拓扑:
```text
Browser
-> Nginx / Caddy
-> dist
-> server-node
```
反向代理至少要保留这些头:
- `Host`
- `X-Forwarded-For`
- `X-Forwarded-Proto`
- `X-Request-Id`
流式接口还要确保:
- `proxy_buffering off`
- `X-Accel-Buffering: no`
## 7. 发布后 smoke 清单
发布完成后至少人工或脚本确认一次:
1. `GET /healthz` 返回 `200`
2. 响应头里能看到 `x-request-id`
3. `POST /api/auth/entry` 可正常注册或恢复账号
4. `GET /api/auth/me` 可正常识别 token
5. `PUT /api/runtime/save/snapshot``GET /api/runtime/save/snapshot` 正常
6. 日志中能用同一个 `request_id` 串起访问记录
如果线上使用反向代理生成请求 ID还要额外确认
- 代理传入的 `X-Request-Id` 没有在 Node 层丢失
- 同域入口 `/``/api/*` 可以通过同一个站点域名访问
## 8. 回滚与备份清单
回滚前先确认:
- 当前发布包版本号或 commit 可定位
- 当前数据库备份可恢复
- 当前 `.env` 或 secret 版本可回退
需要回滚时按顺序执行:
1. 停止新版本 Node 进程
2. 切回上一个稳定前端静态包和 Node 构建产物
3. 恢复上一个稳定环境变量版本
4. 如果本次发布包含数据库结构变更,先确认是否需要回滚数据
5. 回滚后重新执行 `healthz + auth + runtime save` 最小 smoke
如果本次发布已经写入了不兼容数据结构:
- 不要只回滚代码不验证数据兼容性
- 必须先确认旧版本代码是否还能读取当前数据
## 9. 后续扩展方向
任务 9 的下一轮可以继续补:
- 把 smoke 纳入 CI
- 为关键 API 增加结构化 contract 测试
- 给上游 AI 调用补 vendor/model/errorCode 维度日志
- 增加数据库迁移前后的自动检查脚本
- 增加反向代理与正式环境的联调 smoke

View File

@@ -1,185 +0,0 @@
# Prompt 目录收口方案2026-04-19
## 1. 这次调整解决什么问题
此前提示词分散在多个后端、前端和工具文件里:
- `server-node/src/modules/ai/**`
- `server-node/src/modules/quest/**`
- `server-node/src/modules/runtime-item/**`
- `server-node/src/services/customWorld*.ts`
- `server-node/src/services/eightAnchorPromptBuilder.ts`
- `server-node/src/modules/assets/characterAssetRoutes.ts`
- `src/services/**`
- `src/components/**`
问题主要有三类:
1. 业务逻辑和 prompt 文本混写,改提示词时容易顺手改坏运行时逻辑。
2. 同一类 prompt 缺少集中入口,排查系统 prompt / user prompt / repair prompt 成本高。
3. 老桥接层、测试和新业务链路同时依赖时,迁移成本高,容易出现导出断裂。
这次收口目标不是“重写全部 AI 链路”,而是把当前正式业务 prompt 主源收到独立目录,业务模块退化成“准备上下文 + 调用 prompt 脚本”。
## 2. 新目录
本轮落地后的目录:
```text
packages/shared/src/prompts/
└─ qwenSprite.ts
server-node/src/prompts/
├─ characterAssetPrompts.ts
├─ chatPromptBuilders.ts
├─ customWorldAgentPrompts.ts
├─ customWorldEntityPrompts.ts
├─ customWorldOrchestratorPrompts.ts
├─ customWorldSceneNpcPrompts.ts
├─ eightAnchorPrompts.ts
├─ questPrompts.ts
├─ runtimeItemPrompts.ts
├─ storyOrchestratorPrompts.ts
└─ storyPromptBuilders.ts
src/prompts/
├─ characterChatPrompts.ts
├─ customWorldEntityActionPrompts.ts
├─ customWorldOrchestratorPrompts.ts
├─ customWorldPrompts.ts
├─ customWorldRolePromptDefaults.ts
├─ questPrompts.ts
├─ runtimeItemPrompts.ts
├─ storyOrchestratorPrompts.ts
└─ storyPromptBuilders.ts
```
当前职责划分:
- `chatPromptBuilders.ts`
- 角色私聊 / NPC 聊天 / 招募对话 prompt
- `storyPromptBuilders.ts`
- 主剧情 system prompt 与 user prompt builder
- `storyOrchestratorPrompts.ts`
- 剧情语言修复 prompt
- `questPrompts.ts`
- 任务意图 system prompt 与 user prompt builder
- `runtimeItemPrompts.ts`
- 运行时物品意图 system prompt 与 user prompt 文本装配
- `customWorldOrchestratorPrompts.ts`
- 自定义世界主编排 JSON 生成与 repair prompt
- `customWorldAgentPrompts.ts`
- 世界草稿 JSON prompt、补角色 / 补地点 prompt
- `customWorldEntityPrompts.ts`
- 世界编辑器角色 / 场景实体生成 prompt
- `customWorldSceneNpcPrompts.ts`
- 世界编辑器场景 NPC 生成 prompt
- `characterAssetPrompts.ts`
- 角色主图 / 动作试片正式生成 prompt
- `eightAnchorPrompts.ts`
- 八锚点状态推断、模式规则与正式单轮共创 prompt
- `src/prompts/customWorldPrompts.ts`
- 自定义世界分阶段生成 prompt 与场景背景图 prompt
- `src/prompts/customWorldRolePromptDefaults.ts`
- 角色资产工作台默认 prompt 种子唯一主源
- `src/prompts/customWorldEntityActionPrompts.ts`
- 编辑器技能动作 prompt
- `packages/shared/src/prompts/qwenSprite.ts`
- 共享资产层的基础角色 prompt 模板
## 3. 落地规则
### 3.1 业务模块只做两件事
1. 整理运行时上下文
2. 调用 `server-node/src/prompts/**` 下的脚本输出 prompt
不要在业务模块里继续直接内联大段 system prompt / repair prompt / user prompt 模板文本。
### 3.2 Prompt 文件只放文本相关职责
允许放:
- system prompt 常量
- user prompt builder
- repair prompt builder
- prompt 专用的文本摘要函数
不建议放:
- 运行时状态 mutation
- 仓储读写
- HTTP 处理
- 与 prompt 无关的领域推导
### 3.3 兼容层保留旧导出
本轮对已有纯 prompt builder 文件采取了兼容迁移,旧路径保留为薄 re-export
- `server-node/src/modules/ai/chatPromptBuilders.ts`
- `server-node/src/modules/ai/storyPromptBuilders.ts`
- `server-node/src/services/eightAnchorPromptBuilder.ts`
- `src/services/prompt.ts`
- `src/services/characterChatPrompt.ts`
- `src/services/questPrompt.ts`
- `src/services/runtimeItemAiPrompt.ts`
- `src/components/asset-studio/customWorldRolePromptDefaults.ts`
- `packages/shared/src/assets/qwenSprite.ts`
对于 `runtimeQuestModule.ts``runtimeItemModule.ts` 这类被桥接层直接引用的模块,本轮保留原导出名,通过 re-export 指向新 prompt 文件,保证兼容性。
## 4. 后续新增 prompt 的写法
新增提示词时按下面顺序处理:
1. 先判断属于后端、前端/编辑器还是共享工具层。
2. 后端正式业务优先补到 `server-node/src/prompts/*.ts`
3. 前端/编辑器 prompt 优先补到 `src/prompts/*.ts`
4. 可复用的共享资产 prompt 优先补到 `packages/shared/src/prompts/*.ts`
5. 业务模块只传入已经整理好的上下文字段,不在模块内部继续拼长文本。
6. 至少补一条该 prompt 的调用链测试或现有测试断言。
建议命名:
- system prompt`XXX_SYSTEM_PROMPT`
- repair prompt`buildXXXRepairPrompt`
- user prompt`buildXXXPrompt`
- 纯文本装配:`buildXXXPromptText`
## 5. 本轮范围与当前状态
本轮已经收口:
- Story
- Chat
- Quest
- Runtime Item
- Custom World 主编排
- Custom World Agent 草稿增补
- Custom World 编辑器角色 / 场景 / 场景 NPC 生成
- Character Asset
- Eight Anchor
- Scene Image
- 前端剧情 / 私聊 / 任务 / 物品 prompt 兼容层
- 编辑器与工具链 prompt 种子
当前状态:
- 正式业务 prompt 主源已经集中到 prompt 目录。
-`services/``tools/``components/` 下保留的相关文件主要是兼容层或调用方。
- 当前没有再发现需要优先继续抽离的大块业务 prompt 正文。
## 6. 验证方式
本轮调整后建议至少执行:
- `npm run check:encoding`
- `npm run server-node:test`
- `npm --prefix server-node run build`
本轮实测结果:
- `npm run check:encoding` 通过
- `npm --prefix server-node run build` 通过
- `npm run build` 通过
- `npm run server-node:test` 143 项全部通过

View File

@@ -4,6 +4,8 @@
## 文档列表 ## 文档列表
- [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 方向只允许作为迁移参考。
- [API_SERVER_DEV_STACK_COLD_BUILD_TIMEOUT_FIX_2026-04-25.md](./API_SERVER_DEV_STACK_COLD_BUILD_TIMEOUT_FIX_2026-04-25.md):记录 `npm run dev:rust` 在 Windows 冷编译/链接阶段误把 `api-server` `/healthz` 等待判定为超时并杀掉 `cargo run` 的根因,以及将 SpacetimeDB 与 api-server 等待窗口拆分的脚本口径。
- [API_ERROR_DETAILS_MESSAGE_DISPLAY_FIX_2026-04-25.md](./API_ERROR_DETAILS_MESSAGE_DISPLAY_FIX_2026-04-25.md):记录统一 API envelope 错误展示优先读取 `error.details.message` 的修复口径,避免 Big Fish 发布校验等业务错误只显示通用“请求参数不合法”。 - [API_ERROR_DETAILS_MESSAGE_DISPLAY_FIX_2026-04-25.md](./API_ERROR_DETAILS_MESSAGE_DISPLAY_FIX_2026-04-25.md):记录统一 API envelope 错误展示优先读取 `error.details.message` 的修复口径,避免 Big Fish 发布校验等业务错误只显示通用“请求参数不合法”。
- [BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md](./BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md):记录大鱼吃小鱼从固定摇杆改为屏幕首触点方向控制,并要求本地直达局在未操作时保持对象运动。 - [BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md](./BIG_FISH_DIRECTION_TOUCH_CONTROL_2026-04-24.md):记录大鱼吃小鱼从固定摇杆改为屏幕首触点方向控制,并要求本地直达局在未操作时保持对象运动。
- [RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md](./RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md):记录 `server-rs` 无参数 `cargo build` 链接 `spacetime-module` 失败的根因,并冻结默认只构建原生 `api-server`、模块产物继续走 `spacetime build` 的命令边界。 - [RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md](./RUST_WORKSPACE_DEFAULT_BUILD_SCOPE_FIX_2026-04-25.md):记录 `server-rs` 无参数 `cargo build` 链接 `spacetime-module` 失败的根因,并冻结默认只构建原生 `api-server`、模块产物继续走 `spacetime build` 的命令边界。
@@ -70,7 +72,7 @@
- [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)`M2` 第三张会话表 `refresh_session` 的 cookie/hash 边界、轮换与吊销语义、索引与迁移规则。 - [SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_REFRESH_SESSION_TABLE_DESIGN_2026-04-21.md)`M2` 第三张会话表 `refresh_session` 的 cookie/hash 边界、轮换与吊销语义、索引与迁移规则。
- [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)`M2` 第二张身份表 `auth_identity` 的 provider 范围、唯一约束、手机号/微信身份写入规则与迁移策略。 - [SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_IDENTITY_TABLE_DESIGN_2026-04-21.md)`M2` 第二张身份表 `auth_identity` 的 provider 范围、唯一约束、手机号/微信身份写入规则与迁移策略。
- [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)`M2` 第一张身份主表 `user_account` 的职责边界、字段、唯一约束、状态迁移、旧 `users` 映射与落地约束。 - [SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md](./SPACETIMEDB_AUTH_USER_ACCOUNT_TABLE_DESIGN_2026-04-21.md)`M2` 第一张身份主表 `user_account` 的职责边界、字段、唯一约束、状态迁移、旧 `users` 映射与落地约束。
- [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于当前 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。 - [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md):基于 Node 后端能力清单,设计用 `SpacetimeDB + Axum + 阿里云 OSS` 重写后端的目标架构、模块映射、数据分层、迁移顺序与验收标准。
- [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md):冻结 M6 剩余的 STS 与服务端上传 helper 落地口径,明确当前上传主链为服务器上传 OSSWeb 端只负责签名读下载。 - [M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md](./M6_OSS_SERVER_UPLOAD_AND_STS_POLICY_2026-04-21.md):冻结 M6 剩余的 STS 与服务端上传 helper 落地口径,明确当前上传主链为服务器上传 OSSWeb 端只负责签名读下载。
- [M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md](./M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md):冻结 M6 第一批内容 hash、版本、manifest、asset job 与强业务资产表的 Stage 1 边界,明确当前使用 `asset_object + asset_entity_binding + OSS manifest + AiTaskService` 闭合,不重复新增表。 - [M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md](./M6_ASSET_METADATA_HASH_VERSION_AND_SPECIALIZED_TABLE_BOUNDARY_2026-04-22.md):冻结 M6 第一批内容 hash、版本、manifest、asset job 与强业务资产表的 Stage 1 边界,明确当前使用 `asset_object + asset_entity_binding + OSS manifest + AiTaskService` 闭合,不重复新增表。
- [M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](./M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md):冻结 M6 旧 `/generated-*` 路径到 OSS 私有读同源代理的兼容口径,保证旧前端仍能直接消费图片、视频、动作帧与 manifest。 - [M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md](./M6_LEGACY_GENERATED_PATH_OSS_READ_COMPAT_2026-04-22.md):冻结 M6 旧 `/generated-*` 路径到 OSS 私有读同源代理的兼容口径,保证旧前端仍能直接消费图片、视频、动作帧与 manifest。
@@ -93,13 +95,11 @@
- [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色动作模板查询与参考视频导入从旧 Node 本地草稿写盘切到 Rust `OSS` 草稿对象的接口 contract、对象键规划与暂不确认 `asset_object` 的边界。 - [M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md](./M6_CHARACTER_ANIMATION_IMPORT_AND_TEMPLATE_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色动作模板查询与参考视频导入从旧 Node 本地草稿写盘切到 Rust `OSS` 草稿对象的接口 contract、对象键规划与暂不确认 `asset_object` 的边界。
- [M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](./M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色资产工作流缓存从旧 Node 本地 `workflow-cache.json` 切到 Rust `OSS` JSON 草稿对象的读写 contract、字段归一化与暂不落正式资产表的边界。 - [M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md](./M6_CHARACTER_WORKFLOW_CACHE_OSS_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色资产工作流缓存从旧 Node 本地 `workflow-cache.json` 切到 Rust `OSS` JSON 草稿对象的读写 contract、字段归一化与暂不落正式资产表的边界。
- [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色主形象 `generate / jobs / publish` 接口从旧本地 `public/generated-*` 真相切到 `OSS + asset_object + asset_entity_binding + AI task` 的最小闭环与兼容 contract。 - [M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md](./M6_CHARACTER_VISUAL_ASSET_OSS_INTEGRATION_STAGE1_2026-04-22.md):冻结 `M6` 第一批角色主形象 `generate / jobs / publish` 接口从旧本地 `public/generated-*` 真相切到 `OSS + asset_object + asset_entity_binding + AI task` 的最小闭环与兼容 contract。
- [M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md](./M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md):冻结 `M7` 联调、回归、部署、观测、双跑对比、灰度切流、回滚和 `spacetime-module` 结构收口的可执行方案。 - [M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md](./M7_TEST_DEPLOY_CUTOVER_EXECUTION_PLAN_2026-04-22.md):冻结 `M7` 联调、回归、部署、观测、Rust 主线灰度、回滚和 `spacetime-module` 结构收口的可执行方案。
- [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 第二批 `browse history` 纵向切片的 `user_browse_history` 表、双路径 facade、宽松归一化、去重排序规则与测试策略。 - [M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md](./M3_BROWSE_HISTORY_AXUM_SPACETIMEDB_DESIGN_2026-04-21.md):冻结 `M3` 第二批 `browse history` 纵向切片的 `user_browse_history` 表、双路径 facade、宽松归一化、去重排序规则与测试策略。
- [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。 - [ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md](./ASSET_ENTITY_BINDING_REDUCER_DESIGN_2026-04-21.md):冻结已确认 `asset_object` 绑定到业务实体槽位的首版 reducer/procedure、通用 `asset_entity_binding` 表与 Axum facade。
- [FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md](./FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md)把鉴权、浏览历史、runtime story 快照、NPC 待接委托与正式生成编排继续后移到 Express 后端的实施方案与验收口径。
- [REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md](./REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md):落实工程清理审计第一阶段后的仓库噪音清理范围、忽略规则闭合点与后续约束。 - [REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md](./REPO_NOISE_CLEANUP_BASELINE_2026-04-19.md):落实工程清理审计第一阶段后的仓库噪音清理范围、忽略规则闭合点与后续约束。
- [ENCODING_CHECK_TRANSIENT_WORKSPACE_FIX_2026-04-22.md](./ENCODING_CHECK_TRANSIENT_WORKSPACE_FIX_2026-04-22.md):冻结编码检查不扫描临时 Cargo / verify 工作区、同时把 Rust 源文件纳入 UTF-8 校验的修复口径。 - [ENCODING_CHECK_TRANSIENT_WORKSPACE_FIX_2026-04-22.md](./ENCODING_CHECK_TRANSIENT_WORKSPACE_FIX_2026-04-22.md):冻结编码检查不扫描临时 Cargo / verify 工作区、同时把 Rust 源文件纳入 UTF-8 校验的修复口径。
- [PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md](./PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md):后端提示词收口到 `server-node/src/prompts/` 的目录方案、兼容策略与后续新增规则。
- [CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md](./CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md):世界草稿生成失败后等待页误显示为“卡在编译草稿卡”的根因拆解、主链与增强链路边界,以及本次修复策略。 - [CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md](./CUSTOM_WORLD_DRAFT_GENERATION_FAILURE_ANALYSIS_AND_FIX_2026-04-20.md):世界草稿生成失败后等待页误显示为“卡在编译草稿卡”的根因拆解、主链与增强链路边界,以及本次修复策略。
- [CUSTOM_WORLD_AUTO_ASSET_VISIBILITY_FIX_2026-04-20.md](./CUSTOM_WORLD_AUTO_ASSET_VISIBILITY_FIX_2026-04-20.md):世界草稿里“资产已生成但结果页看不到”的根因拆解,包含角色主形象展示、分幕背景露出和 fallback 资源格式修复。 - [CUSTOM_WORLD_AUTO_ASSET_VISIBILITY_FIX_2026-04-20.md](./CUSTOM_WORLD_AUTO_ASSET_VISIBILITY_FIX_2026-04-20.md):世界草稿里“资产已生成但结果页看不到”的根因拆解,包含角色主形象展示、分幕背景露出和 fallback 资源格式修复。
- [CUSTOM_WORLD_PHASE4_COUNT_SEMANTICS_ALIGNMENT_2026-04-20.md](./CUSTOM_WORLD_PHASE4_COUNT_SEMANTICS_ALIGNMENT_2026-04-20.md)Phase4 新增角色/地点后草稿作品卡数量统计与测试断言的语义对齐说明。 - [CUSTOM_WORLD_PHASE4_COUNT_SEMANTICS_ALIGNMENT_2026-04-20.md](./CUSTOM_WORLD_PHASE4_COUNT_SEMANTICS_ALIGNMENT_2026-04-20.md)Phase4 新增角色/地点后草稿作品卡数量统计与测试断言的语义对齐说明。
@@ -149,13 +149,6 @@
- [CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-工作包-c前端结果页与编辑器拆分](./CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-%E5%B7%A5%E4%BD%9C%E5%8C%85-c%E5%89%8D%E7%AB%AF%E7%BB%93%E6%9E%9C%E9%A1%B5%E4%B8%8E%E7%BC%96%E8%BE%91%E5%99%A8%E6%8B%86%E5%88%86):记录工作包 C 已完成的结果页壳层拆分、编辑器目标分发与 mapper 收口、角色资产工坊 section/workflow 拆分,以及仍保留的阶段性 shared 实现边界。 - [CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-工作包-c前端结果页与编辑器拆分](./CREATION_FLOW_CHAIN_REFACTOR_EXECUTION_PLAN_2026-04-21.md#93-%E5%B7%A5%E4%BD%9C%E5%8C%85-c%E5%89%8D%E7%AB%AF%E7%BB%93%E6%9E%9C%E9%A1%B5%E4%B8%8E%E7%BC%96%E8%BE%91%E5%99%A8%E6%8B%86%E5%88%86):记录工作包 C 已完成的结果页壳层拆分、编辑器目标分发与 mapper 收口、角色资产工坊 section/workflow 拆分,以及仍保留的阶段性 shared 实现边界。
- [CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md](./CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md):创作页移动端底部 Tab、亮色主题 token 与滚动权责修复记录。 - [CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md](./CREATION_PAGE_MOBILE_UI_FIX_2026-04-21.md):创作页移动端底部 Tab、亮色主题 token 与滚动权责修复记录。
- [TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md](./TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md):把外部仓库 TXT 模式完整迁入当前项目的冻结边界、模块映射、分阶段计划与验收清单。 - [TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md](./TXT_MODE_VISUAL_NOVEL_MIGRATION_EXECUTION_PLAN_2026-04-20.md):把外部仓库 TXT 模式完整迁入当前项目的冻结边界、模块映射、分阶段计划与验收清单。
- [NODE_BACKEND_MODULE_AND_API_INDEX.md](./NODE_BACKEND_MODULE_AND_API_INDEX.md):由 `server-node/src/manifest/backendCapabilityManifest.ts` 生成的 Node 后端模块职责、挂载面与接口索引,后续新增模块/接口时同步更新这一份。
- [NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md](./NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md):当前 Node 运行时后端的技术栈、入口、鉴权、存储与接口知识图谱。
- [EXPRESS_BACKEND_INTEGRATION_FREEZE_2026-04-09.md](./EXPRESS_BACKEND_INTEGRATION_FREEZE_2026-04-09.md)Express 后端当前 contract 冻结版本、热点文件编辑规则与集成窗口清单。
- [EXPRESS_BACKEND_WORKSTREAM_AUDIT_2026-04-09.md](./EXPRESS_BACKEND_WORKSTREAM_AUDIT_2026-04-09.md):按并行工作流文档逐项核对后的完成度审计与剩余收口点。
- [EDITOR_ASSET_API_MIGRATION_2026-04-08.md](./EDITOR_ASSET_API_MIGRATION_2026-04-08.md):编辑器写盘、资产生成、任务查询从 Vite 本地插件迁到 Node 后端的接口与工具链清单。
- [GO_SERVER_RUNTIME_INTEGRATION_2026-04-07.md](./GO_SERVER_RUNTIME_INTEGRATION_2026-04-07.md)Go 服务端接入、运行时持久化迁移与当前进展记录。
- [GO_SERVER_TASKLIST_2026-04-08.md](./GO_SERVER_TASKLIST_2026-04-08.md)Go 服务端已完成与未完成事项的执行清单。
- [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md)AI 生成角色形象与角色动画的技术路线。 - [AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md](./AI_CHARACTER_ANIMATION_TECHNICAL_SOLUTION_2026-04-04.md)AI 生成角色形象与角色动画的技术路线。
- [ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md](./ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md):面向编辑器的阿里云 NPC 形象与动作实验方案,按 4 条生成链路对比。 - [ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md](./ALIYUN_NPC_IMAGE_ANIMATION_EXPERIMENT_2026-04-07.md):面向编辑器的阿里云 NPC 形象与动作实验方案,按 4 条生成链路对比。
- [PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md](./PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md)PixelMotion 产品形态与能力拆解。 - [PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md](./PIXELMOTION_TECHNICAL_BREAKDOWN_2026-04-04.md)PixelMotion 产品形态与能力拆解。
@@ -164,6 +157,5 @@
## 使用建议 ## 使用建议
- 做实现选型时,优先看这一组。 - 做实现选型时,优先看这一组。
- 做后端实现前,先看 `CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`,再进入具体 Rust / SpacetimeDB 方案。
- 做阶段排期时,把这一组和 `docs/planning/``docs/prd/` 一起看,更容易判断先后顺序。 - 做阶段排期时,把这一组和 `docs/planning/``docs/prd/` 一起看,更容易判断先后顺序。

View File

@@ -44,8 +44,7 @@
2. `docs/design/SCENE_CHAPTER_LOOP_AND_FIRST_ENTRY_CHAPTER_QUEST_DESIGN_2026-04-08.md` 2. `docs/design/SCENE_CHAPTER_LOOP_AND_FIRST_ENTRY_CHAPTER_QUEST_DESIGN_2026-04-08.md`
3. `docs/experience/CURRENT_GAME_FULL_FLOW_PLAYTEST_REPORT_2026-04-07.md` 3. `docs/experience/CURRENT_GAME_FULL_FLOW_PLAYTEST_REPORT_2026-04-07.md`
4. `docs/technical/RUNTIME_STORY_BACKEND_BOUNDARY_MIGRATION_2026-04-19.md` 4. `docs/technical/RUNTIME_STORY_BACKEND_BOUNDARY_MIGRATION_2026-04-19.md`
5. `docs/technical/FRONTEND_TO_BACKEND_MIGRATION_EXECUTION_PLAN_2026-04-21.md` 5. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
6. `docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md`
### 1.3 本文刻意不覆盖的链路 ### 1.3 本文刻意不覆盖的链路

View File

@@ -0,0 +1,56 @@
# Runtime NPC 聊天 LLM 迁移设计2026-04-25
## 背景
当前 `server-rs/crates/api-server/src/runtime_chat.rs` 已承接 `POST /api/runtime/chat/npc/turn/stream`但只返回确定性兜底文本。实际游戏聊天里NPC 回复、下一轮建议、好感变化和限轮收束都应该沿用旧 Node 服务器的 LLM 编排。
## 迁移源
本轮只参考旧 Node 已冻结实现,不恢复 `server-node` 服务,也不把前端切回 Express
1. `server-node/src/prompts/chatPromptBuilders.ts`
2. `server-node/src/modules/ai/chatOrchestrator.ts`
3. `server-node/src/services/chatService.ts`
4. `packages/shared/src/contracts/rpgRuntimeChat.ts`
提示词常量必须原样迁移,禁止改写:
1. `NPC_CHAT_TURN_REPLY_SYSTEM_PROMPT`
2. `NPC_CHAT_TURN_SUGGESTION_SYSTEM_PROMPT`
## 本轮落地边界
1. Rust `api-server` 在同一路由内优先调用 `platform-llm`
2. LLM 回复使用旧 Node 的 `buildNpcChatTurnReplyPrompt(...)` 等价构造逻辑,保持输入字段和中文上下文组织一致。
3. LLM 建议使用旧 Node 的 `buildNpcChatTurnSuggestionPrompt(...)` 等价构造逻辑,解析规则保持“最多 3 行、去掉编号/项目符号”。
4. 好感变化沿用旧 Node 的关键词打分规则:
- 正向与负向关键词计分。
- 首聊无明显关键词时给 `+1`
- 单轮变化限制在 `[-3, 3]`
5. `chatDirective.forceExitAfterTurn / closingMode=foreshadow_close` 时不生成建议,返回空数组,并在 `complete.chatDirective.forceExit` 中显式告知前端退出。
6. LLM 未配置或失败时继续返回后端兜底 SSE保证相遇和点击聊天链路不断。
## 暂不落地
1. 暂不迁移 `maybeBuildPendingNpcQuestOffer(...)` 的完整 quest 生成链。
2. 暂不新增 SpacetimeDB reducer本路由属于 Axum 侧 LLM 编排SpacetimeDB reducer 仍保持确定性。
3. 暂不扩展前端 UI 文案。
## 工程落点
1. 新增 `server-rs/crates/api-server/src/runtime_chat_prompt.rs`
- 承载旧 Node 提示词常量。
- 承载 NPC 聊天 prompt builder 与轻量 JSON 读取 helper。
2. 修改 `server-rs/crates/api-server/src/runtime_chat.rs`
- 注入 `State<AppState>`
- 优先 `LlmClient.stream_text(...)` 生成 `reply_delta`
- 再调用 `request_text(...)` 生成建议。
- 计算 `affinityDelta / affinityText / chatDirective` 后输出 `complete`
3. 修改 `server-rs/crates/api-server/src/main.rs`
- 注册 `runtime_chat_prompt` 模块。
## 验收
1. `cargo fmt -p api-server`
2. `cargo check -p api-server`
3. `node scripts/check-encoding.mjs docs/technical/RUNTIME_NPC_CHAT_LLM_MIGRATION_2026-04-25.md server-rs/crates/api-server/src/runtime_chat.rs server-rs/crates/api-server/src/runtime_chat_prompt.rs server-rs/crates/api-server/src/main.rs`

View File

@@ -163,6 +163,6 @@
## 4. 维护规则 ## 4. 维护规则
1. 新增、删除或改名 Rust 路由时,必须同步更新本索引。 1. 新增、删除或改名 Rust 路由时,必须同步更新本索引。
2. 如果 Node 后端 `NODE_BACKEND_MODULE_AND_API_INDEX.md` 的现役能力面发生变化,必须同时更新本索引与对应阶段任务清单。 2. 如果 Rust 后端公开能力面发生变化,必须同时更新本索引、当前后端实现基线与对应阶段任务清单。
3. 任何 breaking route change 都必须先更新阶段设计文档,再改代码。 3. 任何 breaking route change 都必须先更新阶段设计文档,再改代码。
4. 真实切流前,必须用本索引对照代理层、前端调用面和 smoke 清单,避免只完成编译而遗漏外部可访问路径。 4. 真实切流前,必须用本索引对照代理层、前端调用面和 smoke 清单,避免只完成编译而遗漏外部可访问路径。

View File

@@ -38,7 +38,7 @@ npm run dev:rust
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 冲突时清除旧模块数据。 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` 作为本地默认库名。 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>` 7. 等待 `http://127.0.0.1:<api-port>/healthz` 返回 HTTP 响应后再启动 Vite避免前端初始化请求早于 Rust `api-server` 监听完成并在终端刷出 `ECONNREFUSED 127.0.0.1:<api-port>`
8. 注入 `GENARRATIVE_BACKEND_STACK=rust``RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。 8. 注入 `RUST_SERVER_TARGET``GENARRATIVE_RUNTIME_SERVER_TARGET` 后启动 Vite。
9. 任一子进程退出时,脚本回收其余子进程。 9. 任一子进程退出时,脚本回收其余子进程。
Vite 代理覆盖范围: Vite 代理覆盖范围:

View File

@@ -30,7 +30,7 @@
### 3.1 启动前强制 publish ### 3.1 启动前强制 publish
`scripts/dev-node.mjs` 在拉起 Rust `api-server` 前,必须先执行: `scripts/dev-rust-stack.sh` 在拉起 Rust `api-server` 前,必须先执行:
```bash ```bash
spacetime publish xushi-p4wfr --server http://127.0.0.1:3101 --module-path D:\Genarrative\server-rs\crates\spacetime-module --yes spacetime publish xushi-p4wfr --server http://127.0.0.1:3101 --module-path D:\Genarrative\server-rs\crates\spacetime-module --yes

View File

@@ -9,7 +9,7 @@
关联现状: 关联现状:
- [EXPRESS_BACKEND_INTEGRATION_FREEZE_2026-04-09.md](./EXPRESS_BACKEND_INTEGRATION_FREEZE_2026-04-09.md) - [CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md](./CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md)
- [AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md](./AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md) - [AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md](./AUTH_LOGIN_OPTIONS_DESIGN_2026-04-21.md)
- [AUTH_ME_QUERY_DESIGN_2026-04-21.md](./AUTH_ME_QUERY_DESIGN_2026-04-21.md) - [AUTH_ME_QUERY_DESIGN_2026-04-21.md](./AUTH_ME_QUERY_DESIGN_2026-04-21.md)
- [AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](./AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md) - [AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md](./AUTH_SESSIONS_QUERY_DESIGN_2026-04-21.md)

View File

@@ -59,7 +59,7 @@
- 浏览器直连会遇到 CORS - 浏览器直连会遇到 CORS
- 更稳的方案是开发服务器代理,再由前端请求 `/api/llm/...` - 更稳的方案是开发服务器代理,再由前端请求 `/api/llm/...`
[docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md](/E:/Repos/Genarrative/docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-29.md) 也明确指出: 工程审计聚合结论也明确指出:
- 编辑器、运行时、类后端能力全部耦合在 Vite 配置里 - 编辑器、运行时、类后端能力全部耦合在 Vite 配置里
- 未来如果做独立部署、多人协作、远程编辑、权限控制,会非常难迁移 - 未来如果做独立部署、多人协作、远程编辑、权限控制,会非常难迁移

View File

@@ -171,5 +171,5 @@
1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md) 1. [SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md](./SPACETIMEDB_ASSET_OBJECT_STORAGE_DESIGN_2026-04-21.md)
2. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md) 2. [SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md](./SPACETIMEDB_AXUM_OSS_BACKEND_REWRITE_DESIGN_2026-04-20.md)
3. [../../../server-rs/crates/module-assets/README.md](../../../server-rs/crates/module-assets/README.md) 3. [../../server-rs/crates/module-assets/README.md](../../server-rs/crates/module-assets/README.md)
4. [../../../server-rs/crates/spacetime-module/README.md](../../../server-rs/crates/spacetime-module/README.md) 4. [../../server-rs/crates/spacetime-module/README.md](../../server-rs/crates/spacetime-module/README.md)

View File

@@ -17,7 +17,7 @@
## 2. 当前工程必须继承的能力基线 ## 2. 当前工程必须继承的能力基线
`docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md` 当前生成结果为准,现有 Node 后端的能力基线是: 以下能力清单来自 `2026-04-20` 迁移设计时对旧 Node 后端的快照整理,只作为迁移参考,不再作为新功能扩展依据。后续实现方向`docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md` 为准。
- 对外挂载面:`6` - 对外挂载面:`6`
- 已登记路由:`96` - 已登记路由:`96`
@@ -228,10 +228,10 @@ SpacetimeDB 官方文档对自动迁移的限制很强:
从当前版本开始,凡是涉及 `SpacetimeDB` 的设计、实现、脚本、调试与前端接入,统一要求显式使用以下 skill 作为执行依据: 从当前版本开始,凡是涉及 `SpacetimeDB` 的设计、实现、脚本、调试与前端接入,统一要求显式使用以下 skill 作为执行依据:
1. [$spacetimedb-cli](.codex\\skills\\spacetimedb-cli\\SKILL.md) 1. [$spacetimedb-cli](../../.codex/skills/spacetimedb-cli/SKILL.md)
2. [$spacetimedb-rust](.codex\\skills\\spacetimedb-rust\\SKILL.md) 2. [$spacetimedb-rust](../../.codex/skills/spacetimedb-rust/SKILL.md)
3. [$spacetimedb-concepts](.codex\\skills\\spacetimedb-concepts\\SKILL.md) 3. [$spacetimedb-concepts](../../.codex/skills/spacetimedb-concepts/SKILL.md)
4. [$spacetimedb-typescript](.codex\\skills\\spacetimedb-typescript\\SKILL.md) 4. [$spacetimedb-typescript](../../.codex/skills/spacetimedb-typescript/SKILL.md)
执行要求: 执行要求:
@@ -895,8 +895,8 @@ SpacetimeDB 自动迁移更适合“增量追加”,不适合高频改列结
5. `server-node/src/modules/story/storyActionRoutes.ts` 5. `server-node/src/modules/story/storyActionRoutes.ts`
6. `server-node/src/repositories/runtimeRepository.ts` 6. `server-node/src/repositories/runtimeRepository.ts`
7. `server-node/src/services/customWorldAgentOrchestrator.ts` 7. `server-node/src/services/customWorldAgentOrchestrator.ts`
8. `docs/technical/NODE_BACKEND_MODULE_AND_API_INDEX.md` 8. 本文第 2 节保留的旧 Node 能力快照
9. `docs/technical/NODE_SERVER_KNOWLEDGE_GRAPH_2026-04-08.md` 9. `docs/technical/CURRENT_BACKEND_IMPLEMENTATION_BASELINE_2026-04-25.md`
## 17. 外部技术依据 ## 17. 外部技术依据

View File

@@ -872,9 +872,9 @@ TXT 模式不得直接改坏当前 `runtime story` 主链。
## 10.3 与 prompt 管理规范兼容 ## 10.3 与 prompt 管理规范兼容
必须遵守 [PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md](./PROMPT_DIRECTORY_MANAGEMENT_2026-04-19.md) 中已冻结的规则 必须遵守当前 Rust 后端提示词迁移口径
1. prompt 主源收口到 `server-node/src/prompts/` 1. prompt 主源收口到 `server-rs` 对应生成与编排模块
2. 业务模块只装配上下文 2. 业务模块只装配上下文
3. 兼容层按需保留,但不新增业务内联 prompt 3. 兼容层按需保留,但不新增业务内联 prompt

1422
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,27 +4,16 @@
"version": "0.0.0", "version": "0.0.0",
"type": "module", "type": "module",
"scripts": { "scripts": {
"dev": "node scripts/dev-node.mjs", "dev": "node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh",
"dev:rust": "node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh", "dev:rust": "node scripts/run-bash-script.mjs scripts/dev-rust-stack.sh",
"dev:rust:logs": "node scripts/run-bash-script.mjs scripts/spacetime-logs-local.sh", "dev:rust:logs": "node scripts/run-bash-script.mjs scripts/spacetime-logs-local.sh",
"dev:web": "node scripts/dev-web-rust.mjs", "dev:web": "node scripts/dev-web-rust.mjs",
"dev:node": "node scripts/dev-node.mjs",
"spacetime:publish:maincloud": "node scripts/run-bash-script.mjs scripts/spacetime-publish-maincloud.sh", "spacetime:publish:maincloud": "node scripts/run-bash-script.mjs scripts/spacetime-publish-maincloud.sh",
"api-server:maincloud": "node scripts/api-server-maincloud.mjs", "api-server:maincloud": "node scripts/api-server-maincloud.mjs",
"deploy:rust:remote": "node scripts/run-bash-script.mjs scripts/deploy-rust-remote.sh", "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", "build:rust:ubuntu": "node scripts/run-bash-script.mjs scripts/deploy-rust-remote.sh",
"serve:caddy": "node scripts/run-caddy-dev.mjs", "serve:caddy": "node scripts/run-caddy-dev.mjs",
"server-node:dev": "node scripts/server-node-frozen.mjs",
"server-node:build": "node scripts/server-node-frozen.mjs",
"server-node:db:migrate": "node scripts/server-node-frozen.mjs",
"server-node:manifest:backend": "node scripts/server-node-frozen.mjs",
"server-node:test": "node scripts/server-node-frozen.mjs",
"server-node:test:baseline": "node scripts/server-node-frozen.mjs",
"server-node:smoke": "node scripts/server-node-frozen.mjs",
"server-node:smoke:proxy": "node scripts/server-node-frozen.mjs",
"server-node:check:deploy": "node scripts/server-node-frozen.mjs",
"server-rs:m7:preflight": "powershell -ExecutionPolicy Bypass -File server-rs/scripts/m7-preflight.ps1", "server-rs:m7:preflight": "powershell -ExecutionPolicy Bypass -File server-rs/scripts/m7-preflight.ps1",
"m7:api-compare": "node scripts/run-tsx.cjs scripts/m7-api-compare.ts",
"build": "node scripts/build-gate.mjs", "build": "node scripts/build-gate.mjs",
"build:raw": "node scripts/vite-cli.mjs build", "build:raw": "node scripts/vite-cli.mjs build",
"preview": "node scripts/vite-cli.mjs preview", "preview": "node scripts/vite-cli.mjs preview",
@@ -34,7 +23,7 @@
"lint:guardrails": "npm run lint:eslint", "lint:guardrails": "npm run lint:eslint",
"typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit", "typecheck": "tsc -p tsconfig.typecheck-guardrails.json --noEmit",
"typecheck:guardrails": "npm run typecheck", "typecheck:guardrails": "npm run typecheck",
"lint": "npm run check:encoding && npm run check:server-node-freeze && npm run lint:eslint && npm run typecheck", "lint": "npm run check:encoding && npm run lint:eslint && npm run typecheck",
"lint:fix": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --fix && prettier --write .", "lint:fix": "eslint . --ext .ts,.tsx,.js,.mjs,.cjs --fix && prettier --write .",
"format": "prettier --write .", "format": "prettier --write .",
"format:check": "prettier --check .", "format:check": "prettier --check .",
@@ -44,14 +33,12 @@
"check:data": "node scripts/run-tsx.cjs scripts/validate-content.ts", "check:data": "node scripts/run-tsx.cjs scripts/validate-content.ts",
"check:overrides": "node scripts/run-tsx.cjs scripts/validate-overrides.ts", "check:overrides": "node scripts/run-tsx.cjs scripts/validate-overrides.ts",
"check:smoke": "node scripts/run-tsx.cjs scripts/smoke-content.ts", "check:smoke": "node scripts/run-tsx.cjs scripts/smoke-content.ts",
"check:content": "npm run check:data && npm run check:overrides && npm run check:smoke", "check:content": "npm run check:data && npm run check:overrides && npm run check:smoke"
"check:server-node-freeze": "node scripts/check-server-node-freeze.mjs"
}, },
"dependencies": { "dependencies": {
"@tailwindcss/vite": "^4.1.14", "@tailwindcss/vite": "^4.1.14",
"@vitejs/plugin-react": "^5.0.4", "@vitejs/plugin-react": "^5.0.4",
"dotenv": "^17.2.3", "dotenv": "^17.2.3",
"express": "^4.21.2",
"lucide-react": "^0.546.0", "lucide-react": "^0.546.0",
"motion": "^12.23.24", "motion": "^12.23.24",
"react": "^19.0.0", "react": "^19.0.0",
@@ -61,7 +48,6 @@
"devDependencies": { "devDependencies": {
"@testing-library/react": "^16.3.2", "@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1", "@testing-library/user-event": "^14.6.1",
"@types/express": "^4.17.21",
"@types/node": "^22.14.0", "@types/node": "^22.14.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",

View File

@@ -7,9 +7,8 @@
* - 给后端角色动作视频生成链路提供标准动作 prompt 骨架 * - 给后端角色动作视频生成链路提供标准动作 prompt 骨架
* *
* 当前角色资产主链中的关系是: * 当前角色资产主链中的关系是:
* 1. 前端或后端先拿到一段较短的描述文本 * 1. 前端或 Rust 后端先拿到一段较短的描述文本
* 2. server-node/src/prompts/characterAssetPrompts.ts * 2. 当前角色资产链路调用本文件 buildMasterPrompt / buildVideoActionPrompt
* 再调用本文件 buildMasterPrompt / buildVideoActionPrompt
* 把短描述扩成正式给模型吃的 prompt * 把短描述扩成正式给模型吃的 prompt
* *
* 因此本文件不要承载“角色卡字段挑选”或“UI 默认值”职责, * 因此本文件不要承载“角色卡字段挑选”或“UI 默认值”职责,

View File

@@ -1,127 +0,0 @@
#!/usr/bin/env node
import { createHash } from 'node:crypto';
import { existsSync, readdirSync, readFileSync } from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
const baselinePath = path.join(repoRoot, 'scripts', 'server-node-freeze-baseline.json');
const needle = 'server-node';
const ignoredDirectories = new Set([
'.git',
'.codex',
'.codex-temp',
'.idea',
'node_modules',
'dist',
'build',
'coverage',
'target',
'logs',
]);
const ignoredFiles = new Set([
'scripts/check-server-node-freeze.mjs',
'scripts/server-node-freeze-baseline.json',
'scripts/server-node-frozen.mjs',
'docs/audits/engineering/SERVER_NODE_FREEZE_AND_DEPRECATION_2026-04-24.md',
]);
const allowedExtensions = new Set([
'.cjs',
'.js',
'.json',
'.md',
'.mjs',
'.ps1',
'.rs',
'.toml',
'.ts',
'.tsx',
'.yaml',
'.yml',
]);
function toRepoPath(absolutePath) {
return path.relative(repoRoot, absolutePath).replaceAll(path.sep, '/');
}
function hashLine(line) {
return createHash('sha256').update(line.trim()).digest('hex');
}
function walk(directory, output) {
for (const entry of readdirSync(directory, { withFileTypes: true })) {
if (entry.isDirectory()) {
if (!ignoredDirectories.has(entry.name)) {
walk(path.join(directory, entry.name), output);
}
continue;
}
const absolutePath = path.join(directory, entry.name);
const repoPath = toRepoPath(absolutePath);
if (ignoredFiles.has(repoPath)) {
continue;
}
if (!allowedExtensions.has(path.extname(entry.name).toLowerCase())) {
continue;
}
output.push(absolutePath);
}
}
function collectReferences() {
const files = [];
walk(repoRoot, files);
const references = new Map();
for (const file of files) {
const repoPath = toRepoPath(file);
const content = readFileSync(file, 'utf8');
const lines = content.split(/\r?\n/u);
for (const line of lines) {
if (!line.toLowerCase().includes(needle)) {
continue;
}
const key = `${repoPath}\u0000${hashLine(line)}`;
references.set(key, (references.get(key) || 0) + 1);
}
}
return references;
}
function parseBaseline() {
if (!existsSync(baselinePath)) {
return new Map();
}
const baseline = JSON.parse(readFileSync(baselinePath, 'utf8'));
return new Map(Object.entries(baseline.references || {}));
}
const currentReferences = collectReferences();
const baselineReferences = parseBaseline();
const newReferences = [];
for (const [key, count] of currentReferences.entries()) {
const allowedCount = baselineReferences.get(key) || 0;
if (count > allowedCount) {
const [repoPath] = key.split('\u0000');
newReferences.push({ repoPath, count: count - allowedCount });
}
}
if (newReferences.length > 0) {
console.error('检测到冻结后新增的 server-node 引用,请迁移到 server-rs 或更新废弃审计后再处理:');
for (const reference of newReferences.slice(0, 50)) {
console.error(`- ${reference.repoPath} (+${reference.count})`);
}
if (newReferences.length > 50) {
console.error(`... 另有 ${newReferences.length - 50}`);
}
process.exit(1);
}
console.log('server-node freeze guard passed: 未发现冻结后新增引用。');

View File

@@ -1,71 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
用法:
./scripts/deploy.sh <backend_dir>
示例:
./scripts/deploy.sh /work/server-node
说明:
1. 进入指定后端目录
2. 构建后端
3. 重启已有的 genarrative-server
4. 如果 PM2 进程不存在,则使用 ecosystem.config.cjs 创建
注意:
- 不会执行 git pull
- 不会同步文件
- 不会构建前端
EOF
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "[deploy] 缺少命令: $command_name" >&2
exit 1
fi
}
BACKEND_DIR="${1:-}"
if [[ -z "${BACKEND_DIR}" || "${BACKEND_DIR}" == "-h" || "${BACKEND_DIR}" == "--help" ]]; then
usage
if [[ -z "${BACKEND_DIR}" ]]; then
exit 1
fi
exit 0
fi
require_command npm
require_command pm2
if [[ ! -d "${BACKEND_DIR}" ]]; then
echo "[deploy] 后端目录不存在: ${BACKEND_DIR}" >&2
exit 1
fi
if [[ ! -f "${BACKEND_DIR}/ecosystem.config.cjs" ]]; then
echo "[deploy] 缺少 PM2 配置文件: ${BACKEND_DIR}/ecosystem.config.cjs" >&2
exit 1
fi
echo "[deploy] 后端目录: ${BACKEND_DIR}"
cd "${BACKEND_DIR}"
# 重新构建后端产物。
echo "[deploy] 构建后端"
npm run build
# 优先重启;如果进程还不存在,就直接创建。
echo "[deploy] 重启或创建 PM2 服务"
pm2 restart genarrative-server --update-env \
|| pm2 start ecosystem.config.cjs
echo "[deploy] 完成"

View File

@@ -1,548 +0,0 @@
import {spawn, spawnSync} from 'node:child_process';
import {existsSync, readFileSync} from 'node:fs';
import net from 'node:net';
import path from 'node:path';
import {fileURLToPath, pathToFileURL} from 'node:url';
const repoRoot = fileURLToPath(new URL('../', import.meta.url));
const serverRoot = fileURLToPath(new URL('../server-node/', import.meta.url));
const serverRsRoot = fileURLToPath(new URL('../server-rs/', import.meta.url));
const viteCliPath = fileURLToPath(new URL('./vite-cli.mjs', import.meta.url));
const serverTsxCliPath = fileURLToPath(
new URL('../server-node/node_modules/tsx/dist/cli.mjs', import.meta.url),
);
const serverTsxLoaderPath = fileURLToPath(
new URL('../server-node/node_modules/tsx/dist/loader.mjs', import.meta.url),
);
const serverTsxLoaderUrl = pathToFileURL(serverTsxLoaderPath).href;
const envExamplePath = fileURLToPath(new URL('../.env.example', import.meta.url));
const envLocalPath = fileURLToPath(new URL('../.env.local', import.meta.url));
const spacetimeConfigPath = fileURLToPath(new URL('../spacetime.json', import.meta.url));
const spacetimeLocalConfigPath = fileURLToPath(new URL('../spacetime.local.json', import.meta.url));
const bundledNodePath = fileURLToPath(
new URL('../.tools/node-v22.22.2-win-x64/node.exe', import.meta.url),
);
const bundledNpmCliPath = fileURLToPath(
new URL('../.tools/node-v22.22.2-win-x64/node_modules/npm/bin/npm-cli.js', import.meta.url),
);
const npmCommand = process.platform === 'win32' ? 'npm.cmd' : 'npm';
const DEFAULT_DEV_DATABASE_URL = 'postgresql://postgres:postgres@127.0.0.1:5432/genarrative';
const DEV_MEMORY_DATABASE_URL = 'pg-mem://genarrative-dev';
const DEFAULT_RUST_API_HOST = '127.0.0.1';
const DEFAULT_RUST_API_PORT = '3100';
const DEFAULT_SPACETIME_SERVER_URL = 'http://127.0.0.1:3001';
const DEFAULT_SPACETIME_DATABASE = 'genarrative-dev';
const DEFAULT_INTERNAL_API_SECRET = 'genarrative-dev-internal-bridge';
const spacetimeModulePath = path.join(serverRsRoot, 'crates', 'spacetime-module');
const spacetimeRustBindingsOutDir = path.join(
serverRsRoot,
'crates',
'spacetime-client',
'src',
'module_bindings',
);
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 readJsonFile(filePath) {
if (!existsSync(filePath)) {
return null;
}
try {
return JSON.parse(readFileSync(filePath, 'utf8'));
} catch {
return null;
}
}
function resolveDatabaseProbeTarget(databaseUrl) {
const trimmed = databaseUrl.trim();
if (!trimmed || !/^postgres(?:ql)?:\/\//u.test(trimmed)) {
return null;
}
try {
const url = new URL(trimmed);
return {
host: url.hostname === '0.0.0.0' ? '127.0.0.1' : url.hostname,
port: Number(url.port || 5432),
};
} catch {
return null;
}
}
function checkTcpReachable(target, timeoutMs = 1500) {
return new Promise((resolve) => {
const socket = net.createConnection(target);
let settled = false;
const finish = (result) => {
if (settled) {
return;
}
settled = true;
socket.destroy();
resolve(result);
};
socket.setTimeout(timeoutMs);
socket.once('connect', () => finish(true));
socket.once('timeout', () => finish(false));
socket.once('error', () => finish(false));
});
}
function resolveServerTarget(serverAddr) {
const trimmed = serverAddr.trim();
if (!trimmed) {
return 'http://127.0.0.1:8081';
}
if (/^https?:\/\//u.test(trimmed)) {
try {
const url = new URL(trimmed);
if (url.hostname === '0.0.0.0') {
url.hostname = '127.0.0.1';
}
return url.toString().replace(/\/$/u, '');
} catch {
return trimmed.replace(/\/$/u, '');
}
}
if (trimmed.startsWith(':')) {
return `http://127.0.0.1${trimmed}`;
}
if (trimmed.startsWith('0.0.0.0:')) {
return `http://127.0.0.1:${trimmed.slice('0.0.0.0:'.length)}`;
}
return `http://${trimmed}`;
}
function redactDatabaseUrl(databaseUrl) {
const trimmed = `${databaseUrl || ''}`.trim();
if (!trimmed) {
return '[missing]';
}
if (trimmed.startsWith('pg-mem://')) {
return trimmed;
}
try {
const url = new URL(trimmed);
const databaseName = url.pathname.replace(/^\/+/u, '') || 'postgres';
const portSuffix = url.port ? `:${url.port}` : '';
return `${url.protocol}//${url.hostname}${portSuffix}/${databaseName}`;
} catch {
return '[configured]';
}
}
function resolvePathEnvKey(envMap) {
return Object.keys(envMap).find((key) => key.toLowerCase() === 'path') || 'PATH';
}
function prependEnvPath(envMap, nextEntry) {
const pathKey = resolvePathEnvKey(envMap);
const currentValue = envMap[pathKey] || '';
const normalizedEntry = path.resolve(nextEntry);
const segments = currentValue
.split(path.delimiter)
.map((entry) => entry.trim())
.filter(Boolean)
.filter((entry) => {
try {
return path.resolve(entry) !== normalizedEntry;
} catch {
return entry !== nextEntry;
}
});
envMap[pathKey] = [nextEntry, ...segments].join(path.delimiter);
}
function resolveSpacetimeCommand() {
const command = process.platform === 'win32' ? 'where' : 'which';
const result = spawnSync(command, ['spacetime'], {
cwd: repoRoot,
env: mergedEnv,
encoding: 'utf8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (result.status !== 0) {
return null;
}
const firstLine = `${result.stdout || ''}`
.split(/\r?\n/u)
.map((line) => line.trim())
.find(Boolean);
return firstLine || 'spacetime';
}
function runRequiredCommand(command, args, label) {
const result = spawnSync(command, args, {
cwd: repoRoot,
env: mergedEnv,
stdio: 'inherit',
});
if (result.status !== 0) {
console.error(`[dev:node] ${label} failed with exit code ${result.status ?? 1}`);
process.exit(result.status ?? 1);
}
}
function ensureSpacetimeSchemaReady() {
const spacetimeCommand = resolveSpacetimeCommand();
if (!spacetimeCommand) {
console.error(
'[dev:node] Missing `spacetime` CLI. Install or expose it in PATH before starting local dev.',
);
process.exit(1);
}
const spacetimeServerUrl = `${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL || ''}`.trim();
const spacetimeDatabase = `${mergedEnv.GENARRATIVE_SPACETIME_DATABASE || ''}`.trim();
if (!spacetimeServerUrl || !spacetimeDatabase) {
console.error(
'[dev:node] Missing GENARRATIVE_SPACETIME_SERVER_URL or GENARRATIVE_SPACETIME_DATABASE, cannot publish local schema.',
);
process.exit(1);
}
console.log(
`[dev:node] Publishing spacetime-module to ${spacetimeDatabase} (${spacetimeServerUrl}) before Rust api-server starts...`,
);
runRequiredCommand(
spacetimeCommand,
[
'publish',
spacetimeDatabase,
'--server',
spacetimeServerUrl,
'--module-path',
spacetimeModulePath,
'--yes',
...(mergedEnv.GENARRATIVE_SPACETIME_DELETE_DATA_ON_CONFLICT === '1'
? ['--delete-data=on-conflict']
: []),
],
'spacetime publish',
);
console.log('[dev:node] Generating Rust Spacetime bindings before Rust api-server starts...');
runRequiredCommand(
spacetimeCommand,
[
'generate',
'--no-config',
'--lang',
'rust',
'--out-dir',
spacetimeRustBindingsOutDir,
'--module-path',
spacetimeModulePath,
'--include-private',
'--yes',
],
'spacetime generate (rust)',
);
}
const exampleEnv = readEnvFile(envExamplePath);
const localEnv = readEnvFile(envLocalPath);
const spacetimeConfig = readJsonFile(spacetimeConfigPath);
const spacetimeLocalConfig = readJsonFile(spacetimeLocalConfigPath);
const mergedEnv = {
...exampleEnv,
...localEnv,
...process.env,
};
const runtimeNodePath = existsSync(bundledNodePath)
? bundledNodePath
: process.execPath;
const runtimeNpmCliPath = existsSync(bundledNpmCliPath)
? bundledNpmCliPath
: '';
const runtimeNodeDir = path.dirname(runtimeNodePath);
mergedEnv.PROJECT_ROOT = mergedEnv.PROJECT_ROOT || repoRoot;
mergedEnv.NODE_SERVER_ADDR = mergedEnv.NODE_SERVER_ADDR || ':8081';
mergedEnv.NODE_SERVER_TARGET =
mergedEnv.NODE_SERVER_TARGET || resolveServerTarget(mergedEnv.NODE_SERVER_ADDR);
mergedEnv.GENARRATIVE_API_HOST =
mergedEnv.GENARRATIVE_API_HOST || DEFAULT_RUST_API_HOST;
mergedEnv.GENARRATIVE_API_PORT =
mergedEnv.GENARRATIVE_API_PORT || DEFAULT_RUST_API_PORT;
mergedEnv.GENARRATIVE_API_TARGET =
mergedEnv.GENARRATIVE_API_TARGET ||
`http://${mergedEnv.GENARRATIVE_API_HOST}:${mergedEnv.GENARRATIVE_API_PORT}`;
mergedEnv.GENARRATIVE_INTERNAL_API_SECRET =
mergedEnv.GENARRATIVE_INTERNAL_API_SECRET || DEFAULT_INTERNAL_API_SECRET;
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL =
mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL || DEFAULT_SPACETIME_SERVER_URL;
mergedEnv.GENARRATIVE_SPACETIME_DATABASE =
mergedEnv.GENARRATIVE_SPACETIME_DATABASE ||
spacetimeLocalConfig?.database ||
spacetimeConfig?.database ||
DEFAULT_SPACETIME_DATABASE;
mergedEnv.DATABASE_URL =
mergedEnv.DATABASE_URL || DEFAULT_DEV_DATABASE_URL;
mergedEnv.VITE_DEV_HOST = mergedEnv.VITE_DEV_HOST || '127.0.0.1';
prependEnvPath(mergedEnv, runtimeNodeDir);
mergedEnv.npm_config_scripts_prepend_node_path = 'true';
const exampleDatabaseUrl = `${exampleEnv.DATABASE_URL || ''}`.trim();
const localDatabaseUrl = `${localEnv.DATABASE_URL || ''}`.trim();
const processDatabaseUrl = `${process.env.DATABASE_URL || ''}`.trim();
const exampleSpacetimeDatabase = `${exampleEnv.GENARRATIVE_SPACETIME_DATABASE || ''}`.trim();
const localSpacetimeDatabase = `${localEnv.GENARRATIVE_SPACETIME_DATABASE || ''}`.trim();
const processSpacetimeDatabase = `${process.env.GENARRATIVE_SPACETIME_DATABASE || ''}`.trim();
const hasExplicitDatabaseUrl =
Boolean(processDatabaseUrl) ||
(Boolean(localDatabaseUrl) && localDatabaseUrl !== exampleDatabaseUrl);
const hasExplicitSpacetimeDatabase =
Boolean(processSpacetimeDatabase) ||
(Boolean(localSpacetimeDatabase) && localSpacetimeDatabase !== exampleSpacetimeDatabase);
// 本地开发默认跟随仓库当前的 Spacetime 数据库名,只有显式覆盖时才尊重环境变量。
if (!hasExplicitSpacetimeDatabase) {
mergedEnv.GENARRATIVE_SPACETIME_DATABASE =
spacetimeLocalConfig?.database ||
spacetimeConfig?.database ||
DEFAULT_SPACETIME_DATABASE;
}
if (!hasExplicitDatabaseUrl) {
const databaseProbeTarget = resolveDatabaseProbeTarget(mergedEnv.DATABASE_URL);
if (databaseProbeTarget) {
const isReachable = await checkTcpReachable(databaseProbeTarget);
if (!isReachable) {
console.warn(
`[dev:node] PostgreSQL unavailable at ${databaseProbeTarget.host}:${databaseProbeTarget.port}; falling back to ${DEV_MEMORY_DATABASE_URL} for local dev.`,
);
console.warn(
'[dev:node] Current session will use in-memory persistence only. Set DATABASE_URL in .env.local to restore PostgreSQL-backed runtime data.',
);
mergedEnv.DATABASE_URL = DEV_MEMORY_DATABASE_URL;
}
}
}
console.log(`[dev:node] PROJECT_ROOT=${mergedEnv.PROJECT_ROOT}`);
console.log(`[dev:node] NODE_SERVER_ADDR=${mergedEnv.NODE_SERVER_ADDR}`);
console.log(`[dev:node] NODE_SERVER_TARGET=${mergedEnv.NODE_SERVER_TARGET}`);
console.log(`[dev:node] GENARRATIVE_API_TARGET=${mergedEnv.GENARRATIVE_API_TARGET}`);
console.log('[dev:node] GENARRATIVE_INTERNAL_API_SECRET=[configured]');
console.log(
`[dev:node] GENARRATIVE_SPACETIME_SERVER_URL=${mergedEnv.GENARRATIVE_SPACETIME_SERVER_URL}`,
);
console.log(
`[dev:node] GENARRATIVE_SPACETIME_DATABASE=${mergedEnv.GENARRATIVE_SPACETIME_DATABASE}`,
);
console.log(`[dev:node] DATABASE_URL=${redactDatabaseUrl(mergedEnv.DATABASE_URL)}`);
console.log(`[dev:node] VITE_DEV_HOST=${mergedEnv.VITE_DEV_HOST}`);
console.log(`[dev:node] NODE_RUNTIME=${runtimeNodePath}`);
ensureSpacetimeSchemaReady();
const children = new Set();
let shuttingDown = false;
let pendingExitCode = 0;
function stopChild(child) {
if (!child || child.exitCode !== null) {
return;
}
child.kill('SIGTERM');
setTimeout(() => {
if (child.exitCode === null) {
child.kill('SIGKILL');
}
}, 2000).unref();
}
function stopAllChildren() {
for (const child of children) {
stopChild(child);
}
}
function finalizeExit(code = 0) {
pendingExitCode = code;
if (children.size === 0) {
process.exit(pendingExitCode);
}
}
function requestShutdown(code = 0) {
if (!shuttingDown) {
shuttingDown = true;
pendingExitCode = code;
stopAllChildren();
}
finalizeExit(pendingExitCode);
}
function registerChild(name, child, siblingProvider) {
children.add(child);
child.on('error', (error) => {
console.error(`[dev:node] ${name} failed to start`, error);
requestShutdown(1);
});
child.on('exit', (code, signal) => {
children.delete(child);
if (!shuttingDown) {
const resolvedExitCode = code ?? 1;
const signalSuffix = signal ? ` (${signal})` : '';
console.error(
`[dev:node] ${name} exited with code ${resolvedExitCode}${signalSuffix}`,
);
const sibling = siblingProvider();
if (sibling) {
stopChild(sibling);
}
requestShutdown(resolvedExitCode);
return;
}
finalizeExit(pendingExitCode);
});
}
const serverProcess = existsSync(serverTsxLoaderPath)
? spawn(runtimeNodePath, ['--watch', '--import', serverTsxLoaderUrl, 'src/server.ts'], {
cwd: serverRoot,
env: mergedEnv,
stdio: 'inherit',
})
: existsSync(serverTsxCliPath)
? spawn(runtimeNodePath, [serverTsxCliPath, 'watch', 'src/server.ts'], {
cwd: serverRoot,
env: mergedEnv,
stdio: 'inherit',
})
: runtimeNpmCliPath
? spawn(runtimeNodePath, [runtimeNpmCliPath, 'run', 'dev'], {
cwd: serverRoot,
env: mergedEnv,
stdio: 'inherit',
})
: spawn(npmCommand, ['run', 'dev'], {
cwd: serverRoot,
env: mergedEnv,
shell: process.platform === 'win32',
stdio: 'inherit',
});
const rustApiProcess = process.platform === 'win32'
? spawn(
'powershell',
[
'-NoProfile',
'-ExecutionPolicy',
'Bypass',
'-File',
path.join(serverRsRoot, 'scripts', 'dev.ps1'),
'-ApiHost',
mergedEnv.GENARRATIVE_API_HOST,
'-Port',
mergedEnv.GENARRATIVE_API_PORT,
],
{
cwd: repoRoot,
env: mergedEnv,
stdio: 'inherit',
},
)
: spawn(
'bash',
[
path.join(serverRsRoot, 'scripts', 'dev.sh'),
],
{
cwd: repoRoot,
env: mergedEnv,
stdio: 'inherit',
},
);
const viteProcess = spawn(
runtimeNodePath,
[viteCliPath, '--port=3000', `--host=${mergedEnv.VITE_DEV_HOST}`],
{
cwd: repoRoot,
env: mergedEnv,
stdio: 'inherit',
},
);
registerChild('node server', serverProcess, () => viteProcess);
registerChild('rust api-server', rustApiProcess, () => viteProcess);
registerChild('vite dev server', viteProcess, () => serverProcess);
process.on('SIGINT', () => {
console.log('[dev:node] received SIGINT, shutting down...');
requestShutdown(0);
});
process.on('SIGTERM', () => {
console.log('[dev:node] received SIGTERM, shutting down...');
requestShutdown(0);
});

View File

@@ -7,6 +7,7 @@ usage() {
用法: 用法:
npm run dev:rust npm run dev:rust
./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110 ./scripts/dev-rust-stack.sh --api-port 8090 --spacetime-port 3110
./scripts/dev-rust-stack.sh --api-timeout-seconds 600
./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish ./scripts/dev-rust-stack.sh --skip-spacetime --skip-publish
./scripts/dev-rust-stack.sh --preserve-database ./scripts/dev-rust-stack.sh --preserve-database
npm run dev:rust:logs -- --follow npm run dev:rust:logs -- --follow
@@ -183,6 +184,7 @@ SPACETIME_ROOT_DIR="${SERVER_RS_DIR}/.spacetimedb/local"
DATABASE="" DATABASE=""
API_LOG="info,tower_http=info" API_LOG="info,tower_http=info"
SPACETIME_TIMEOUT_SECONDS="60" SPACETIME_TIMEOUT_SECONDS="60"
API_SERVER_TIMEOUT_SECONDS="300"
SKIP_SPACETIME=0 SKIP_SPACETIME=0
SKIP_PUBLISH=0 SKIP_PUBLISH=0
PRESERVE_DATABASE=0 PRESERVE_DATABASE=0
@@ -256,6 +258,10 @@ while [[ $# -gt 0 ]]; do
SPACETIME_TIMEOUT_SECONDS="${2:?缺少 --spacetime-timeout-seconds 的值}" SPACETIME_TIMEOUT_SECONDS="${2:?缺少 --spacetime-timeout-seconds 的值}"
shift 2 shift 2
;; ;;
--api-timeout-seconds)
API_SERVER_TIMEOUT_SECONDS="${2:?缺少 --api-timeout-seconds 的值}"
shift 2
;;
--skip-spacetime) --skip-spacetime)
SKIP_SPACETIME=1 SKIP_SPACETIME=1
shift shift
@@ -322,6 +328,7 @@ echo "[dev:rust] rust api: ${RUST_SERVER_TARGET}"
echo "[dev:rust] spacetime: ${SPACETIME_SERVER}" echo "[dev:rust] spacetime: ${SPACETIME_SERVER}"
echo "[dev:rust] database: ${DATABASE}" echo "[dev:rust] database: ${DATABASE}"
echo "[dev:rust] spacetime root: ${SPACETIME_ROOT_DIR}" echo "[dev:rust] spacetime root: ${SPACETIME_ROOT_DIR}"
echo "[dev:rust] api timeout: ${API_SERVER_TIMEOUT_SECONDS}s"
if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then if [[ "${SKIP_SPACETIME}" -ne 1 ]]; then
mkdir -p "${SPACETIME_ROOT_DIR}" mkdir -p "${SPACETIME_ROOT_DIR}"
@@ -375,12 +382,11 @@ PIDS+=("${API_PID}")
NAMES+=("api-server") NAMES+=("api-server")
echo "[dev:rust] 等待 api-server 就绪" echo "[dev:rust] 等待 api-server 就绪"
wait_for_api_server "${RUST_SERVER_TARGET}/healthz" "${SPACETIME_TIMEOUT_SECONDS}" "${API_PID}" wait_for_api_server "${RUST_SERVER_TARGET}/healthz" "${API_SERVER_TIMEOUT_SECONDS}" "${API_PID}"
echo "[dev:rust] 启动 vite" echo "[dev:rust] 启动 vite"
( (
cd "${REPO_ROOT}" cd "${REPO_ROOT}"
GENARRATIVE_BACKEND_STACK="rust" \
RUST_SERVER_TARGET="${RUST_SERVER_TARGET}" \ RUST_SERVER_TARGET="${RUST_SERVER_TARGET}" \
GENARRATIVE_RUNTIME_SERVER_TARGET="${RUST_SERVER_TARGET}" \ GENARRATIVE_RUNTIME_SERVER_TARGET="${RUST_SERVER_TARGET}" \
VITE_DEV_HOST="${WEB_HOST}" \ VITE_DEV_HOST="${WEB_HOST}" \

View File

@@ -4,10 +4,10 @@
当前正式开发入口统一为: 当前正式开发入口统一为:
- `node scripts/dev-node.mjs` - `npm run dev`
- `server-node/src/modules/editor/**` - `scripts/dev-rust-stack.sh`
- `server-node/src/modules/assets/**` - `server-rs/crates/api-server/**`
- `src/editor/shared/editorApiClient.ts` - `server-rs/crates/spacetime-module/**`
该目录只保留本说明文件,作为迁移结果标记。 该目录只保留本说明文件,作为迁移结果标记。

View File

@@ -2,7 +2,6 @@ import {spawn} from 'node:child_process';
const mergedEnv = { const mergedEnv = {
...process.env, ...process.env,
GENARRATIVE_BACKEND_STACK: process.env.GENARRATIVE_BACKEND_STACK || 'rust',
RUST_SERVER_TARGET: RUST_SERVER_TARGET:
process.env.RUST_SERVER_TARGET || process.env.RUST_SERVER_TARGET ||
process.env.GENARRATIVE_API_TARGET || process.env.GENARRATIVE_API_TARGET ||

View File

@@ -1,170 +0,0 @@
import assert from 'node:assert/strict';
type HttpMethod = 'GET';
interface CompareCase {
method: HttpMethod;
path: string;
}
interface CompareResult {
path: string;
nodeStatus: number;
rustStatus: number;
matched: boolean;
reason?: string;
}
const DEFAULT_NODE_BASE_URL = 'http://127.0.0.1:8081';
const DEFAULT_RUST_BASE_URL = 'http://127.0.0.1:3000';
function readEnv(name: string, fallback: string): string {
const value = process.env[name]?.trim();
return value ? value : fallback;
}
function buildCases(): CompareCase[] {
const rawPaths = process.env.M7_COMPARE_PATHS?.trim();
const paths = rawPaths
? rawPaths.split(',').map((value) => value.trim()).filter(Boolean)
: ['/healthz', '/api/auth/login-options'];
return paths.map((path) => ({
method: 'GET',
path: path.startsWith('/') ? path : `/${path}`,
}));
}
async function fetchJson(baseUrl: string, testCase: CompareCase, requestId: string) {
const url = new URL(testCase.path, baseUrl);
const response = await fetch(url, {
method: testCase.method,
headers: {
'x-request-id': requestId,
'x-genarrative-response-envelope': '1',
},
});
const text = await response.text();
const json = text ? JSON.parse(text) : null;
return {
status: response.status,
json: normalizeVolatileJson(json),
};
}
function normalizeVolatileJson(value: unknown): unknown {
if (Array.isArray(value)) {
return value.map(normalizeVolatileJson);
}
if (!value || typeof value !== 'object') {
return value;
}
const record = value as Record<string, unknown>;
const normalized: Record<string, unknown> = {};
for (const [key, child] of Object.entries(record)) {
if (['requestId', 'timestamp', 'latencyMs'].includes(key)) {
continue;
}
normalized[key] = normalizeVolatileJson(child);
}
return normalized;
}
function stableStringify(value: unknown): string {
if (Array.isArray(value)) {
return `[${value.map(stableStringify).join(',')}]`;
}
if (!value || typeof value !== 'object') {
return JSON.stringify(value);
}
const entries = Object.entries(value as Record<string, unknown>)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, child]) => `${JSON.stringify(key)}:${stableStringify(child)}`);
return `{${entries.join(',')}}`;
}
async function compareCase(
nodeBaseUrl: string,
rustBaseUrl: string,
testCase: CompareCase,
): Promise<CompareResult> {
const requestId = `m7-api-compare-${testCase.path.replaceAll('/', '-')}`;
const [nodeResponse, rustResponse] = await Promise.all([
fetchJson(nodeBaseUrl, testCase, requestId),
fetchJson(rustBaseUrl, testCase, requestId),
]);
if (nodeResponse.status !== rustResponse.status) {
return {
path: testCase.path,
nodeStatus: nodeResponse.status,
rustStatus: rustResponse.status,
matched: false,
reason: 'status 不一致',
};
}
const nodeBody = stableStringify(nodeResponse.json);
const rustBody = stableStringify(rustResponse.json);
if (nodeBody !== rustBody) {
return {
path: testCase.path,
nodeStatus: nodeResponse.status,
rustStatus: rustResponse.status,
matched: false,
reason: `body 不一致\nnode=${nodeBody}\nrust=${rustBody}`,
};
}
return {
path: testCase.path,
nodeStatus: nodeResponse.status,
rustStatus: rustResponse.status,
matched: true,
};
}
async function main() {
const nodeBaseUrl = readEnv('M7_NODE_BASE_URL', DEFAULT_NODE_BASE_URL);
const rustBaseUrl = readEnv('M7_RUST_BASE_URL', DEFAULT_RUST_BASE_URL);
const strict = process.env.M7_COMPARE_STRICT?.trim() !== 'false';
const cases = buildCases();
console.log(`[m7:api-compare] node=${nodeBaseUrl}`);
console.log(`[m7:api-compare] rust=${rustBaseUrl}`);
console.log(`[m7:api-compare] cases=${cases.map((item) => item.path).join(', ')}`);
const results = await Promise.all(
cases.map((testCase) => compareCase(nodeBaseUrl, rustBaseUrl, testCase)),
);
for (const result of results) {
const label = result.matched ? 'OK' : 'DIFF';
console.log(
`[m7:api-compare] ${label} ${result.path} node=${result.nodeStatus} rust=${result.rustStatus}`,
);
if (result.reason) {
console.log(result.reason);
}
}
const failures = results.filter((result) => !result.matched);
if (strict) {
assert.equal(failures.length, 0, '存在 Node/Rust API contract 差异');
}
}
main().catch((error) => {
console.error('[m7:api-compare] failed');
console.error(error);
process.exitCode = 1;
});

View File

@@ -53,9 +53,10 @@ function normalizePathForCaddy(filePath) {
function resolveApiUpstream(env) { function resolveApiUpstream(env) {
return ( return (
env.CADDY_API_UPSTREAM env.CADDY_API_UPSTREAM ||
|| env.NODE_SERVER_TARGET env.GENARRATIVE_API_TARGET ||
|| 'http://127.0.0.1:8081' env.RUST_SERVER_TARGET ||
'http://127.0.0.1:3100'
); );
} }

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +0,0 @@
#!/usr/bin/env node
const command = process.env.npm_lifecycle_event || 'server-node:*';
console.error(`[server-node frozen] ${command} 已冻结。`);
console.error('后端主线已切换到 server-rsRust + SpacetimeDB禁止继续运行或扩展 server-node。');
console.error('如需开发后端能力,请使用 npm run dev:rust 或 server-rs/scripts/*。');
process.exit(1);

View File

@@ -1,436 +0,0 @@
import assert from 'node:assert/strict';
import { spawn, type ChildProcess } from 'node:child_process';
import fs from 'node:fs';
import http from 'node:http';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { httpRequest } from '../server-node/src/testHttp.ts';
const scriptPath = fileURLToPath(import.meta.url);
const repoRoot = path.resolve(path.dirname(scriptPath), '..');
const bundledNodePath = path.join(
repoRoot,
'.tools',
'node-v22.22.2-win-x64',
process.platform === 'win32' ? 'node.exe' : 'bin/node',
);
const runtimeNodePath = fs.existsSync(bundledNodePath)
? bundledNodePath
: process.execPath;
const serverBuildPath = path.join(repoRoot, 'server-node', 'dist', 'server.cjs');
const webBuildPath = path.join(repoRoot, 'dist', 'index.html');
const publicRoot = path.join(repoRoot, 'public');
const proxyPort = 18080;
const nodePort = 18081;
const proxyBaseUrl = `http://127.0.0.1:${proxyPort}`;
const nodeBaseUrl = `http://127.0.0.1:${nodePort}`;
type ManagedChild = {
name: string;
process: ChildProcess;
};
function assertBuildArtifacts() {
if (!fs.existsSync(serverBuildPath)) {
throw new Error(
'server-node/dist/server.cjs 不存在,请先运行 npm run server-node:build',
);
}
if (!fs.existsSync(webBuildPath)) {
throw new Error('dist/index.html 不存在,请先运行 npm run build');
}
}
function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
async function waitForReady(
label: string,
url: string,
validate: (bodyText: string, status: number) => void,
timeoutMs = 20000,
) {
const startedAt = Date.now();
let lastError: unknown = null;
while (Date.now() - startedAt < timeoutMs) {
try {
const response = await httpRequest(url);
const bodyText = await response.text();
validate(bodyText, response.status);
return;
} catch (error) {
lastError = error;
await sleep(250);
}
}
throw new Error(
`[smoke:proxy] ${label} 未在 ${timeoutMs}ms 内就绪: ${lastError instanceof Error ? lastError.message : String(lastError)}`,
);
}
function spawnManagedChild(
name: string,
command: string,
args: string[],
env: NodeJS.ProcessEnv,
): ManagedChild {
const child = spawn(command, args, {
cwd: repoRoot,
env,
stdio: 'inherit',
shell: false,
});
child.on('error', (error) => {
console.error(`[smoke:proxy] ${name} 启动失败`, error);
});
return {
name,
process: child,
};
}
async function stopChild(child: ManagedChild | null) {
if (!child || child.process.exitCode !== null) {
return;
}
child.process.kill('SIGTERM');
await Promise.race([
new Promise<void>((resolve) => {
child.process.once('exit', () => resolve());
}),
sleep(2000),
]);
if (child.process.exitCode === null) {
child.process.kill('SIGKILL');
await new Promise<void>((resolve) => {
child.process.once('exit', () => resolve());
});
}
}
function contentTypeFor(filePath: string) {
if (filePath.endsWith('.html')) {
return 'text/html; charset=utf-8';
}
if (filePath.endsWith('.js')) {
return 'text/javascript; charset=utf-8';
}
if (filePath.endsWith('.css')) {
return 'text/css; charset=utf-8';
}
if (filePath.endsWith('.json')) {
return 'application/json; charset=utf-8';
}
if (filePath.endsWith('.png')) {
return 'image/png';
}
if (filePath.endsWith('.jpg') || filePath.endsWith('.jpeg')) {
return 'image/jpeg';
}
if (filePath.endsWith('.webp')) {
return 'image/webp';
}
if (filePath.endsWith('.svg')) {
return 'image/svg+xml; charset=utf-8';
}
return 'application/octet-stream';
}
function resolveStaticFile(urlPath: string) {
const cleanPath = decodeURIComponent(urlPath.split('?')[0] || '/');
const normalizedPath = cleanPath === '/' ? '/index.html' : cleanPath;
const trimmedRelativePath = normalizedPath.replace(/^\/+/u, '');
const distRoot = path.resolve(repoRoot, 'dist');
const publicCandidatePath = path.resolve(publicRoot, trimmedRelativePath);
const distCandidatePath = path.resolve(distRoot, trimmedRelativePath);
if (
publicCandidatePath.startsWith(publicRoot) &&
fs.existsSync(publicCandidatePath) &&
fs.statSync(publicCandidatePath).isFile()
) {
return publicCandidatePath;
}
if (
distCandidatePath.startsWith(distRoot) &&
fs.existsSync(distCandidatePath) &&
fs.statSync(distCandidatePath).isFile()
) {
return distCandidatePath;
}
return webBuildPath;
}
async function startSameOriginProxy() {
const server = http.createServer((request, response) => {
const requestUrl = request.url || '/';
if (requestUrl === '/healthz') {
response.writeHead(200, {
'Content-Type': 'text/plain; charset=utf-8',
});
response.end('ok');
return;
}
if (requestUrl.startsWith('/api/')) {
const upstream = http.request(
{
hostname: '127.0.0.1',
port: nodePort,
path: requestUrl,
method: request.method,
headers: {
...request.headers,
host: `127.0.0.1:${nodePort}`,
},
},
(upstreamResponse) => {
response.writeHead(
upstreamResponse.statusCode ?? 502,
upstreamResponse.headers,
);
upstreamResponse.pipe(response);
},
);
upstream.on('error', (error) => {
response.writeHead(502, {
'Content-Type': 'application/json; charset=utf-8',
});
response.end(
JSON.stringify({
error: {
message:
error instanceof Error ? error.message : 'proxy upstream failed',
},
}),
);
});
request.pipe(upstream);
return;
}
const filePath = resolveStaticFile(requestUrl);
response.writeHead(200, {
'Content-Type': contentTypeFor(filePath),
});
fs.createReadStream(filePath).pipe(response);
});
await new Promise<void>((resolve, reject) => {
server.once('error', reject);
server.listen(proxyPort, '127.0.0.1', () => resolve());
});
return server;
}
async function stopProxyServer(server: http.Server | null) {
if (!server) {
return;
}
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
}
async function authEntry(baseUrl: string) {
const requestId = 'proxy-smoke-auth-entry';
const username = `proxy_${Date.now().toString(36)}`;
const response = await httpRequest(`${baseUrl}/api/auth/entry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-Id': requestId,
},
body: JSON.stringify({
username,
password: 'proxy-secret-123',
}),
});
const payload = (await response.json()) as {
token: string;
user: {
id: string;
username: string;
};
};
assert.equal(response.status, 200);
assert.equal(response.headers.get('x-request-id'), requestId);
assert.equal(payload.user.username, username);
assert.ok(payload.token);
return payload;
}
async function main() {
assertBuildArtifacts();
let serverChild: ManagedChild | null = null;
let proxyServer: http.Server | null = null;
try {
console.log('[smoke:proxy] starting built node server');
serverChild = spawnManagedChild(
'server-node',
runtimeNodePath,
[serverBuildPath],
{
...process.env,
PROJECT_ROOT: repoRoot,
NODE_ENV: 'test',
NODE_SERVER_ADDR: `:${nodePort}`,
DATABASE_URL: 'pg-mem://genarrative-proxy-smoke',
LOG_LEVEL: 'silent',
JWT_SECRET: 'proxy-smoke-secret',
JWT_ISSUER: 'genarrative-proxy-smoke',
LLM_API_KEY: '',
DASHSCOPE_API_KEY: '',
},
);
await waitForReady(
'node server',
`${nodeBaseUrl}/healthz`,
(bodyText, status) => {
assert.equal(status, 200);
const payload = JSON.parse(bodyText) as {
ok: boolean;
service: string;
};
assert.equal(payload.ok, true);
assert.equal(payload.service, 'genarrative-node-server');
},
);
console.log('[smoke:proxy] node server ready');
console.log('[smoke:proxy] starting same-origin reverse proxy harness');
proxyServer = await startSameOriginProxy();
await waitForReady(
'reverse proxy',
`${proxyBaseUrl}/healthz`,
(bodyText, status) => {
assert.equal(status, 200);
assert.equal(bodyText.trim(), 'ok');
},
);
console.log('[smoke:proxy] reverse proxy ready');
const homeResponse = await httpRequest(`${proxyBaseUrl}/`);
const homeHtml = await homeResponse.text();
assert.equal(homeResponse.status, 200);
assert.match(homeHtml, /<div id="root"><\/div>/u);
console.log('[smoke:proxy] static web entry ok');
const entry = await authEntry(proxyBaseUrl);
console.log('[smoke:proxy] proxied auth entry ok');
const meResponse = await httpRequest(`${proxyBaseUrl}/api/auth/me`, {
headers: {
Authorization: `Bearer ${entry.token}`,
},
});
const mePayload = (await meResponse.json()) as {
user: {
username: string;
};
};
assert.equal(meResponse.status, 200);
assert.equal(mePayload.user.username, entry.user.username);
console.log('[smoke:proxy] proxied auth me ok');
const saveResponse = await httpRequest(
`${proxyBaseUrl}/api/runtime/save/snapshot`,
{
method: 'PUT',
headers: {
Authorization: `Bearer ${entry.token}`,
'Content-Type': 'application/json',
'X-Genarrative-Response-Envelope': 'v1',
},
body: JSON.stringify({
gameState: {
worldType: 'WUXIA',
chapter: 2,
},
bottomTab: 'adventure',
currentStory: {
text: 'proxy smoke story',
},
}),
},
);
const savePayload = (await saveResponse.json()) as {
ok: true;
data: {
gameState: {
chapter: number;
};
};
meta: {
requestId: string;
operation: string;
};
};
assert.equal(saveResponse.status, 200);
assert.equal(savePayload.ok, true);
assert.equal(savePayload.data.gameState.chapter, 2);
assert.equal(savePayload.meta.operation, 'runtime.snapshot.put');
assert.ok(savePayload.meta.requestId);
console.log('[smoke:proxy] proxied runtime save ok');
const getResponse = await httpRequest(
`${proxyBaseUrl}/api/runtime/save/snapshot`,
{
headers: {
Authorization: `Bearer ${entry.token}`,
},
},
);
const getPayload = (await getResponse.json()) as {
gameState: {
chapter: number;
};
bottomTab: string;
};
assert.equal(getResponse.status, 200);
assert.equal(getPayload.gameState.chapter, 2);
assert.equal(getPayload.bottomTab, 'adventure');
console.log('[smoke:proxy] proxied runtime snapshot read ok');
console.log('[smoke:proxy] all checks passed');
} finally {
await stopProxyServer(proxyServer);
await stopChild(serverChild);
}
}
void main().catch((error) => {
console.error('[smoke:proxy] failed');
console.error(error);
process.exit(1);
});

View File

@@ -1,406 +0,0 @@
import assert from 'node:assert/strict';
import fs from 'node:fs';
import type { AddressInfo } from 'node:net';
import os from 'node:os';
import path from 'node:path';
import { createApp } from '../server-node/src/app.ts';
import type { AppConfig } from '../server-node/src/config.ts';
import { createAppContext } from '../server-node/src/server.ts';
import { httpRequest, type TestRequestInit } from '../server-node/src/testHttp.ts';
function createSmokeConfig(): AppConfig {
const tempRoot = fs.mkdtempSync(
path.join(os.tmpdir(), 'genarrative-server-node-smoke-'),
);
return {
nodeEnv: 'test',
projectRoot: tempRoot,
publicDir: path.join(tempRoot, 'public'),
logsDir: path.join(tempRoot, 'logs'),
dataDir: path.join(tempRoot, 'data'),
rawEnv: {},
databaseUrl: 'pg-mem://genarrative-smoke',
serverAddr: ':0',
logLevel: 'silent',
editorApiEnabled: true,
assetsApiEnabled: true,
jwtSecret: 'test-secret',
jwtExpiresIn: '7d',
jwtIssuer: 'genarrative-server-node-smoke',
llm: {
baseUrl: 'https://example.invalid',
apiKey: '',
model: 'test-model',
},
dashScope: {
baseUrl: 'https://example.invalid',
apiKey: '',
imageModel: 'test-image-model',
requestTimeoutMs: 1000,
},
smsAuth: {
enabled: true,
provider: 'mock',
endpoint: 'dypnsapi.aliyuncs.com',
accessKeyId: '',
accessKeySecret: '',
signName: 'Test Sign',
templateCode: '100001',
templateParamKey: 'code',
countryCode: '86',
schemeName: '',
codeLength: 6,
codeType: 1,
validTimeSeconds: 300,
intervalSeconds: 60,
duplicatePolicy: 1,
caseAuthPolicy: 1,
returnVerifyCode: false,
mockVerifyCode: '123456',
maxSendPerPhonePerDay: 20,
maxSendPerIpPerHour: 30,
maxVerifyFailuresPerPhonePerHour: 12,
maxVerifyFailuresPerIpPerHour: 24,
captchaTtlSeconds: 180,
captchaTriggerVerifyFailuresPerPhone: 3,
captchaTriggerVerifyFailuresPerIp: 5,
blockPhoneFailureThreshold: 6,
blockIpFailureThreshold: 10,
blockPhoneDurationMinutes: 30,
blockIpDurationMinutes: 30,
},
wechatAuth: {
enabled: true,
provider: 'mock',
appId: '',
appSecret: '',
authorizeEndpoint: 'https://open.weixin.qq.com/connect/qrconnect',
accessTokenEndpoint: 'https://api.weixin.qq.com/sns/oauth2/access_token',
userInfoEndpoint: 'https://api.weixin.qq.com/sns/userinfo',
callbackPath: '/api/auth/wechat/callback',
defaultRedirectPath: '/',
mockUserId: 'mock_wechat_user',
mockUnionId: 'mock_wechat_union',
mockDisplayName: '微信旅人',
mockAvatarUrl: '',
},
authSession: {
refreshCookieName: 'genarrative_refresh_session',
refreshSessionTtlDays: 30,
refreshCookieSecure: false,
refreshCookieSameSite: 'Lax',
refreshCookiePath: '/api/auth',
},
};
}
async function withSmokeServer<T>(
run: (options: { baseUrl: string }) => Promise<T>,
) {
const context = await createAppContext(createSmokeConfig());
const app = createApp(context);
const server = await new Promise<import('node:http').Server>((resolve) => {
const nextServer = app.listen(0, '127.0.0.1', () => resolve(nextServer));
});
try {
const address = server.address() as AddressInfo;
return await run({
baseUrl: `http://127.0.0.1:${address.port}`,
});
} finally {
await new Promise<void>((resolve, reject) => {
server.close((error) => {
if (error) {
reject(error);
return;
}
resolve();
});
});
await context.db.close();
}
}
function withBearer(token: string, init: TestRequestInit = {}) {
return {
...init,
headers: {
...(init.headers ?? {}),
Authorization: `Bearer ${token}`,
'Content-Type': 'application/json',
},
} satisfies TestRequestInit;
}
async function authEntry(baseUrl: string) {
const username = `smoke_${Date.now().toString(36)}`;
const response = await httpRequest(`${baseUrl}/api/auth/entry`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
username,
password: 'smoke-secret-123',
}),
});
const payload = (await response.json()) as {
token: string;
user: {
id: string;
username: string;
};
};
assert.equal(response.status, 200);
assert.ok(payload.token);
assert.equal(payload.user.username, username);
return payload;
}
async function sendPhoneCode(baseUrl: string, phone: string) {
const response = await httpRequest(`${baseUrl}/api/auth/phone/send-code`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone,
scene: 'login',
}),
});
const payload = (await response.json()) as {
ok: boolean;
cooldownSeconds: number;
expiresInSeconds: number;
};
assert.equal(response.status, 200);
assert.equal(payload.ok, true);
assert.equal(payload.cooldownSeconds, 60);
assert.equal(payload.expiresInSeconds, 300);
}
async function phoneAuthEntry(baseUrl: string) {
const phone = '13800138000';
await sendPhoneCode(baseUrl, phone);
const response = await httpRequest(`${baseUrl}/api/auth/phone/login`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
phone,
code: '123456',
}),
});
const payload = (await response.json()) as {
token: string;
user: {
id: string;
username: string;
loginMethod: string;
phoneNumberMasked: string | null;
};
};
assert.equal(response.status, 200);
assert.ok(payload.token);
assert.equal(payload.user.loginMethod, 'phone');
assert.equal(payload.user.phoneNumberMasked, '138****8000');
return payload;
}
async function main() {
console.log('[server-node:smoke] booting ephemeral Express server');
await withSmokeServer(async ({ baseUrl }) => {
const healthzRequestId = 'smoke-healthz-request';
const healthzResponse = await httpRequest(`${baseUrl}/healthz`, {
headers: {
'X-Request-Id': healthzRequestId,
},
});
const healthzPayload = (await healthzResponse.json()) as {
ok: boolean;
service: string;
};
assert.equal(healthzResponse.status, 200);
assert.equal(healthzResponse.headers.get('x-request-id'), healthzRequestId);
assert.equal(healthzPayload.ok, true);
assert.equal(healthzPayload.service, 'genarrative-node-server');
console.log('[server-node:smoke] healthz ok');
const entry = await authEntry(baseUrl);
console.log('[server-node:smoke] password auth entry ok');
const phoneEntry = await phoneAuthEntry(baseUrl);
console.log('[server-node:smoke] phone auth entry ok');
const meResponse = await httpRequest(`${baseUrl}/api/auth/me`, {
headers: {
Authorization: `Bearer ${phoneEntry.token}`,
},
});
const mePayload = (await meResponse.json()) as {
user: {
id: string;
username: string;
loginMethod: string;
};
};
assert.equal(meResponse.status, 200);
assert.equal(mePayload.user.username, phoneEntry.user.username);
assert.equal(mePayload.user.loginMethod, 'phone');
console.log('[server-node:smoke] auth me ok');
const putSnapshotResponse = await httpRequest(
`${baseUrl}/api/runtime/save/snapshot`,
withBearer(phoneEntry.token, {
method: 'PUT',
body: JSON.stringify({
gameState: {
worldType: 'WUXIA',
chapter: 1,
},
bottomTab: 'adventure',
currentStory: {
text: 'smoke story',
},
}),
}),
);
const putSnapshotPayload = (await putSnapshotResponse.json()) as {
version: number;
bottomTab: string;
gameState: {
chapter: number;
};
};
assert.equal(putSnapshotResponse.status, 200);
assert.equal(putSnapshotPayload.version, 2);
assert.equal(putSnapshotPayload.bottomTab, 'adventure');
assert.equal(putSnapshotPayload.gameState.chapter, 1);
const getSnapshotResponse = await httpRequest(
`${baseUrl}/api/runtime/save/snapshot`,
{
headers: {
Authorization: `Bearer ${phoneEntry.token}`,
},
},
);
const getSnapshotPayload = (await getSnapshotResponse.json()) as {
bottomTab: string;
gameState: {
chapter: number;
};
};
assert.equal(getSnapshotResponse.status, 200);
assert.equal(getSnapshotPayload.bottomTab, 'adventure');
assert.equal(getSnapshotPayload.gameState.chapter, 1);
console.log('[server-node:smoke] runtime snapshot roundtrip ok');
const putSettingsResponse = await httpRequest(
`${baseUrl}/api/runtime/settings`,
withBearer(phoneEntry.token, {
method: 'PUT',
body: JSON.stringify({
musicVolume: 0.3,
platformTheme: 'light',
}),
}),
);
const putSettingsPayload = (await putSettingsResponse.json()) as {
musicVolume: number;
platformTheme: string;
};
assert.equal(putSettingsResponse.status, 200);
assert.equal(putSettingsPayload.musicVolume, 0.3);
assert.equal(putSettingsPayload.platformTheme, 'light');
const getSettingsResponse = await httpRequest(`${baseUrl}/api/runtime/settings`, {
headers: {
Authorization: `Bearer ${phoneEntry.token}`,
},
});
const getSettingsPayload = (await getSettingsResponse.json()) as {
musicVolume: number;
platformTheme: string;
};
assert.equal(getSettingsResponse.status, 200);
assert.equal(getSettingsPayload.musicVolume, 0.3);
assert.equal(getSettingsPayload.platformTheme, 'light');
console.log('[server-node:smoke] runtime settings roundtrip ok');
const deleteSnapshotResponse = await httpRequest(
`${baseUrl}/api/runtime/save/snapshot`,
withBearer(phoneEntry.token, {
method: 'DELETE',
}),
);
const deleteSnapshotPayload = (await deleteSnapshotResponse.json()) as {
ok: boolean;
};
assert.equal(deleteSnapshotResponse.status, 200);
assert.equal(deleteSnapshotPayload.ok, true);
const emptySnapshotResponse = await httpRequest(
`${baseUrl}/api/runtime/save/snapshot`,
{
headers: {
Authorization: `Bearer ${phoneEntry.token}`,
},
},
);
const emptySnapshotPayload = await emptySnapshotResponse.json();
assert.equal(emptySnapshotResponse.status, 200);
assert.equal(emptySnapshotPayload, null);
console.log('[server-node:smoke] runtime snapshot delete ok');
const logoutResponse = await httpRequest(
`${baseUrl}/api/auth/logout`,
withBearer(phoneEntry.token, {
method: 'POST',
}),
);
const logoutPayload = (await logoutResponse.json()) as {
ok: boolean;
};
assert.equal(logoutResponse.status, 200);
assert.equal(logoutPayload.ok, true);
const expiredTokenResponse = await httpRequest(`${baseUrl}/api/auth/me`, {
headers: {
Authorization: `Bearer ${phoneEntry.token}`,
},
});
assert.equal(expiredTokenResponse.status, 401);
console.log('[server-node:smoke] logout invalidation ok');
});
console.log('[server-node:smoke] all checks passed');
}
void main().catch((error) => {
console.error('[server-node:smoke] failed');
console.error(error);
process.exit(1);
});

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
usage() {
cat <<'EOF'
用法:
./scripts/update.sh
说明:
1. 对当前仓库执行 git pull
2. 只构建前端
3. 固定同步前端 dist 到 /work/dist
4. 固定同步 server-node 到 /work/server-node
注意:
- server-node 同步时会排除 dist 和 node_modules
- 不会构建后端
- 不会执行 npm ci
- 不会重启 PM2
EOF
}
require_command() {
local command_name="$1"
if ! command -v "$command_name" >/dev/null 2>&1; then
echo "[update] 缺少命令: $command_name" >&2
exit 1
fi
}
if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then
usage
exit 0
fi
require_command git
require_command npm
require_command rsync
SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd -- "${SCRIPT_DIR}/.." && pwd)"
CLIENT_TARGET_DIR="/work/dist"
SERVER_TARGET_DIR="/work/server-node"
echo "[update] 仓库目录: ${REPO_ROOT}"
echo "[update] 前端目标目录: ${CLIENT_TARGET_DIR}"
echo "[update] 后端目标目录: ${SERVER_TARGET_DIR}"
cd "${REPO_ROOT}"
# 先拉取当前分支的最新代码。
echo "[update] 拉取当前分支最新代码"
git pull
# 只构建前端,不处理后端构建。
echo "[update] 构建前端"
npm run build
# 固定创建 /work 下的目标目录。
echo "[update] 创建目标目录"
mkdir -p "${CLIENT_TARGET_DIR}" "${SERVER_TARGET_DIR}"
# 同步前端构建产物。
echo "[update] 同步前端 dist -> ${CLIENT_TARGET_DIR}"
rsync -a --delete "${REPO_ROOT}/dist/" "${CLIENT_TARGET_DIR}/"
# 同步 server-node 源码和配置,但保留目标目录自己的 dist 和 node_modules。
echo "[update] 同步 server-node -> ${SERVER_TARGET_DIR}"
rsync -a --delete \
--exclude 'dist/' \
--exclude 'node_modules/' \
"${REPO_ROOT}/server-node/" "${SERVER_TARGET_DIR}/"
echo "[update] 完成"

View File

@@ -1,16 +0,0 @@
import esbuild from 'esbuild';
await esbuild.build({
entryPoints: ['src/server.ts'],
bundle: true,
platform: 'node',
format: 'cjs',
target: 'node22',
outfile: 'dist/server.cjs',
sourcemap: true,
packages: 'external',
tsconfig: 'tsconfig.json',
define: {
'import.meta.url': 'undefined',
},
});

View File

@@ -1,18 +0,0 @@
module.exports = {
apps: [
{
name: 'genarrative-server',
script: 'dist/server.cjs',
cwd: __dirname,
instances: 1,
exec_mode: 'fork',
watch: false,
env: {
NODE_ENV: 'production',
},
error_file: 'logs/error.log',
out_file: 'logs/out.log',
time: true,
},
],
};

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1,40 +0,0 @@
{
"name": "genarrative-server-node",
"private": true,
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "tsx watch src/server.ts",
"build": "node build.mjs",
"start": "node dist/server.cjs",
"test": "node test.mjs",
"db:migrate": "tsx src/migrate.ts",
"manifest:backend": "tsx scripts/generateBackendCapabilityArtifacts.ts"
},
"dependencies": {
"@alicloud/dypnsapi20170525": "^2.0.0",
"@alicloud/openapi-client": "^0.4.15",
"@alicloud/tea-util": "^1.4.11",
"@node-rs/argon2": "^2.0.2",
"cors": "^2.8.5",
"express": "^4.21.2",
"jose": "^6.1.0",
"pg": "^8.16.3",
"pino": "^9.9.5",
"pino-http": "^10.5.0",
"pino-roll": "^3.1.0",
"pngjs": "^7.0.0",
"sharp": "^0.34.5",
"zod": "^4.1.8"
},
"devDependencies": {
"@types/cors": "^2.8.18",
"@types/express": "^5.0.3",
"@types/node": "^24.6.0",
"@types/pg": "^8.20.0",
"esbuild": "^0.28.0",
"pg-mem": "^3.0.14",
"tsx": "^4.21.0",
"typescript": "^5.9.3"
}
}

View File

@@ -1,372 +0,0 @@
import { mkdir, readFile, readdir, writeFile } from 'node:fs/promises';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import {
BACKEND_CAPABILITY_MANIFEST,
type BackendCapabilityManifest,
type BackendDomainModule,
type BackendRouteCapability,
type BackendRouteSurface,
} from '../src/manifest/backendCapabilityManifest.js';
type GeneratedSummary = {
surfaceCount: number;
routeCount: number;
moduleCount: number;
publicRouteCount: number;
jwtRouteCount: number;
envSwitchRouteCount: number;
streamRouteCount: number;
};
type GeneratedArtifact = {
generatedAt: string;
manifestVersion: string;
generatedCommand: string;
outputTargets: BackendCapabilityManifest['outputTargets'];
summary: GeneratedSummary;
surfaces: Array<
BackendRouteSurface & {
routeCount: number;
routeIds: string[];
}
>;
modules: Array<
BackendDomainModule & {
routeCount: number;
routeIds: string[];
}
>;
routes: BackendRouteCapability[];
maintenanceRules: string[];
};
const currentFilePath = fileURLToPath(import.meta.url);
const scriptDirectory = path.dirname(currentFilePath);
const repoRoot = path.resolve(scriptDirectory, '..', '..');
/**
* 统一把 repo 相对路径转成绝对路径,避免不同工作目录下解析不一致。
*/
function resolveRepoPath(relativePath: string) {
return path.resolve(repoRoot, relativePath);
}
function sortById<T extends { id: string }>(items: T[]) {
return [...items].sort((left, right) => left.id.localeCompare(right.id, 'zh-Hans-CN'));
}
function sortRoutes(routes: BackendRouteCapability[]) {
return [...routes].sort((left, right) => {
if (left.path === right.path) {
return left.method.localeCompare(right.method, 'en-US');
}
return left.path.localeCompare(right.path, 'en-US');
});
}
/**
* 用最小约束校验 manifest 的唯一性与引用完整性,确保生成结果可维护。
*/
function assertUniqueIds(items: Array<{ id: string }>, label: string) {
const seen = new Set<string>();
const duplicates: string[] = [];
items.forEach((item) => {
if (seen.has(item.id)) {
duplicates.push(item.id);
return;
}
seen.add(item.id);
});
if (duplicates.length > 0) {
throw new Error(`${label} 存在重复 id${duplicates.join('、')}`);
}
}
async function assertSourceFileContains(params: {
sourceFile: string;
sourceHint: string;
routeId: string;
}) {
const absolutePath = resolveRepoPath(params.sourceFile);
const content = await readFile(absolutePath, 'utf8');
if (!content.includes(params.sourceHint)) {
throw new Error(
`路由 ${params.routeId} 的 sourceHint 未命中源码:${params.sourceFile} -> ${params.sourceHint}`,
);
}
}
async function validateModuleCoverage(modules: BackendDomainModule[]) {
const modulesRoot = resolveRepoPath('server-node/src/modules');
const directoryEntries = await readdir(modulesRoot, { withFileTypes: true });
const actualDirectories = directoryEntries
.filter((entry) => entry.isDirectory())
.map((entry) => `server-node/src/modules/${entry.name}`)
.sort((left, right) => left.localeCompare(right, 'en-US'));
const manifestDirectories = modules
.map((moduleItem) => moduleItem.directory)
.sort((left, right) => left.localeCompare(right, 'en-US'));
const missingInManifest = actualDirectories.filter(
(directory) => !manifestDirectories.includes(directory),
);
const staleInManifest = manifestDirectories.filter(
(directory) => !actualDirectories.includes(directory),
);
if (missingInManifest.length > 0) {
throw new Error(
`以下模块目录尚未进入能力 manifest${missingInManifest.join('、')}`,
);
}
if (staleInManifest.length > 0) {
throw new Error(
`manifest 中存在已失效的模块目录:${staleInManifest.join('、')}`,
);
}
}
async function validateManifest(manifest: BackendCapabilityManifest) {
assertUniqueIds(manifest.surfaces, '挂载面');
assertUniqueIds(manifest.modules, '内部模块');
assertUniqueIds(manifest.routes, '路由');
const surfaceIds = new Set(manifest.surfaces.map((surface) => surface.id));
const moduleIds = new Set(manifest.modules.map((moduleItem) => moduleItem.id));
for (const surface of manifest.surfaces) {
for (const relatedModuleId of surface.relatedModuleIds) {
if (!moduleIds.has(relatedModuleId)) {
throw new Error(
`挂载面 ${surface.id} 引用了未定义的模块:${relatedModuleId}`,
);
}
}
for (const mount of surface.mounts) {
const absoluteEntryPath = resolveRepoPath(mount.entryFile);
const content = await readFile(absoluteEntryPath, 'utf8');
if (!content.includes(mount.routeFactory)) {
throw new Error(
`挂载面 ${surface.id} 的入口文件缺少工厂引用:${mount.entryFile} -> ${mount.routeFactory}`,
);
}
}
}
for (const moduleItem of manifest.modules) {
for (const surfaceId of moduleItem.exposedBySurfaceIds) {
if (!surfaceIds.has(surfaceId)) {
throw new Error(
`模块 ${moduleItem.id} 引用了未定义的挂载面:${surfaceId}`,
);
}
}
const absoluteDirectory = resolveRepoPath(moduleItem.directory);
const statsEntries = await readdir(absoluteDirectory);
if (statsEntries.length === 0) {
throw new Error(`模块目录为空,无法作为能力边界:${moduleItem.directory}`);
}
}
for (const route of manifest.routes) {
if (!surfaceIds.has(route.surfaceId)) {
throw new Error(`路由 ${route.id} 引用了未定义的挂载面:${route.surfaceId}`);
}
for (const moduleId of route.domainModuleIds) {
if (!moduleIds.has(moduleId)) {
throw new Error(`路由 ${route.id} 引用了未定义的模块:${moduleId}`);
}
}
await assertSourceFileContains({
sourceFile: route.sourceFile,
sourceHint: route.sourceHint,
routeId: route.id,
});
}
await validateModuleCoverage(manifest.modules);
}
function buildSummary(routes: BackendRouteCapability[]): GeneratedSummary {
return {
surfaceCount: BACKEND_CAPABILITY_MANIFEST.surfaces.length,
routeCount: routes.length,
moduleCount: BACKEND_CAPABILITY_MANIFEST.modules.length,
publicRouteCount: routes.filter((route) => route.access === '公开').length,
jwtRouteCount: routes.filter((route) => route.access === 'JWT').length,
envSwitchRouteCount: routes.filter((route) => route.access.startsWith('开关:')).length,
streamRouteCount: routes.filter((route) => route.responseMode === 'stream').length,
};
}
function buildArtifact(manifest: BackendCapabilityManifest): GeneratedArtifact {
const routes = sortRoutes(manifest.routes);
const summary = buildSummary(routes);
const surfaces = sortById(manifest.surfaces).map((surface) => {
const surfaceRoutes = routes.filter((route) => route.surfaceId === surface.id);
return {
...surface,
routeCount: surfaceRoutes.length,
routeIds: surfaceRoutes.map((route) => route.id),
};
});
const modules = sortById(manifest.modules).map((moduleItem) => {
const moduleRoutes = routes.filter((route) =>
route.domainModuleIds.includes(moduleItem.id),
);
return {
...moduleItem,
routeCount: moduleRoutes.length,
routeIds: moduleRoutes.map((route) => route.id),
};
});
return {
generatedAt: new Date().toISOString(),
manifestVersion: manifest.version,
generatedCommand: manifest.generatedCommand,
outputTargets: manifest.outputTargets,
summary,
surfaces,
modules,
routes,
maintenanceRules: manifest.maintenanceRules,
};
}
function renderMarkdown(artifact: GeneratedArtifact) {
const lines: string[] = [];
lines.push('# Node 后端模块与接口索引');
lines.push('');
lines.push('> 该文档由 `server-node/src/manifest/backendCapabilityManifest.ts` 自动生成。');
lines.push(`> 生成命令:\`${artifact.generatedCommand}\``);
lines.push(`> 生成时间:\`${artifact.generatedAt}\``);
lines.push('');
lines.push('## 总览');
lines.push('');
lines.push(`- 对外挂载面:${artifact.summary.surfaceCount}`);
lines.push(`- 已登记路由:${artifact.summary.routeCount}`);
lines.push(`- 内部模块目录:${artifact.summary.moduleCount}`);
lines.push(`- 公开接口:${artifact.summary.publicRouteCount}`);
lines.push(`- JWT 接口:${artifact.summary.jwtRouteCount}`);
lines.push(`- 受环境开关控制的接口:${artifact.summary.envSwitchRouteCount}`);
lines.push(`- 流式接口:${artifact.summary.streamRouteCount}`);
lines.push('');
lines.push('## 产物');
lines.push('');
lines.push(`- JSON 清单:\`${artifact.outputTargets.json}\``);
lines.push(`- Markdown 索引:\`${artifact.outputTargets.markdown}\``);
lines.push(`- Manifest 源:\`server-node/src/manifest/backendCapabilityManifest.ts\``);
lines.push('');
lines.push('## 对外挂载面');
lines.push('');
artifact.surfaces.forEach((surface) => {
lines.push(`### ${surface.title}`);
lines.push('');
lines.push(`- 标识:\`${surface.id}\``);
lines.push(`- 路由数:${surface.routeCount}`);
lines.push(`- 入口:${surface.mounts.map((mount) => `\`${mount.entryFile} -> ${mount.mountPath} -> ${mount.routeFactory}\``).join('')}`);
lines.push(`- 关联模块:${surface.relatedModuleIds.length > 0 ? surface.relatedModuleIds.map((moduleId) => `\`${moduleId}\``).join('、') : '无'}`);
lines.push('- 责任:');
surface.responsibilities.forEach((item) => {
lines.push(` - ${item}`);
});
lines.push('- 主要服务边界:');
surface.primaryServiceBoundaries.forEach((item) => {
lines.push(` - ${item}`);
});
lines.push('');
});
lines.push('## 接口索引');
lines.push('');
lines.push('| 方法 | 路径 | 访问 | 响应 | 挂载面 | 内部模块 | 说明 |');
lines.push('| --- | --- | --- | --- | --- | --- | --- |');
artifact.routes.forEach((route) => {
const moduleLabel =
route.domainModuleIds.length > 0
? route.domainModuleIds.map((moduleId) => `\`${moduleId}\``).join('、')
: '无';
lines.push(
`| ${route.method} | \`${route.path}\` | ${route.access} | ${route.responseMode} | \`${route.surfaceId}\` | ${moduleLabel} | ${route.summary} |`,
);
});
lines.push('');
lines.push('## 内部模块边界');
lines.push('');
artifact.modules.forEach((moduleItem) => {
lines.push(`### ${moduleItem.title}`);
lines.push('');
lines.push(`- 标识:\`${moduleItem.id}\``);
lines.push(`- 目录:\`${moduleItem.directory}\``);
lines.push(`- 对外可见面:${moduleItem.exposedBySurfaceIds.map((surfaceId) => `\`${surfaceId}\``).join('、')}`);
lines.push(`- 关联路由数:${moduleItem.routeCount}`);
lines.push('- 职责:');
moduleItem.responsibilities.forEach((item) => {
lines.push(` - ${item}`);
});
lines.push('- 主要服务边界:');
moduleItem.primaryServiceBoundaries.forEach((item) => {
lines.push(` - ${item}`);
});
lines.push('- 关键文件:');
moduleItem.keyFiles.forEach((filePath) => {
lines.push(` - \`${filePath}\``);
});
lines.push('');
});
lines.push('## 维护规则');
lines.push('');
artifact.maintenanceRules.forEach((rule) => {
lines.push(`- ${rule}`);
});
lines.push('');
return `${lines.join('\n')}`;
}
async function writeArtifactFile(relativePath: string, content: string) {
const absolutePath = resolveRepoPath(relativePath);
await mkdir(path.dirname(absolutePath), { recursive: true });
await writeFile(absolutePath, content, 'utf8');
}
async function main() {
await validateManifest(BACKEND_CAPABILITY_MANIFEST);
const artifact = buildArtifact(BACKEND_CAPABILITY_MANIFEST);
const jsonContent = `${JSON.stringify(artifact, null, 2)}\n`;
const markdownContent = renderMarkdown(artifact);
await writeArtifactFile(artifact.outputTargets.json, jsonContent);
await writeArtifactFile(artifact.outputTargets.markdown, markdownContent);
console.log(
[
`backend capability artifacts generated`,
`json=${artifact.outputTargets.json}`,
`markdown=${artifact.outputTargets.markdown}`,
`routes=${artifact.summary.routeCount}`,
`modules=${artifact.summary.moduleCount}`,
].join(' | '),
);
}
void main().catch((error) => {
console.error(error);
process.exit(1);
});

View File

@@ -1,5 +0,0 @@
CREATE TABLE IF NOT EXISTS schema_migrations (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
applied_at TEXT NOT NULL
);

View File

@@ -1,16 +0,0 @@
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL,
token_version INTEGER NOT NULL DEFAULT 1,
display_name TEXT NOT NULL,
login_provider TEXT NOT NULL DEFAULT 'password',
account_status TEXT NOT NULL DEFAULT 'active',
phone_number TEXT,
phone_verified_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE UNIQUE INDEX IF NOT EXISTS users_phone_number_unique_idx
ON users (phone_number);

View File

@@ -1,10 +0,0 @@
CREATE TABLE IF NOT EXISTS save_snapshots (
user_id TEXT PRIMARY KEY,
version INTEGER NOT NULL,
saved_at TEXT NOT NULL,
bottom_tab TEXT NOT NULL,
game_state_json JSONB NOT NULL,
current_story_json JSONB,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS runtime_settings (
user_id TEXT PRIMARY KEY,
music_volume REAL NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

View File

@@ -1,24 +0,0 @@
CREATE TABLE IF NOT EXISTS custom_world_profiles (
user_id TEXT NOT NULL,
profile_id TEXT NOT NULL,
payload_json JSONB NOT NULL,
visibility TEXT NOT NULL DEFAULT 'draft',
published_at TEXT,
author_display_name TEXT NOT NULL DEFAULT '玩家',
world_name TEXT NOT NULL DEFAULT '',
subtitle TEXT NOT NULL DEFAULT '',
summary_text TEXT NOT NULL DEFAULT '',
cover_image_src TEXT,
theme_mode TEXT NOT NULL DEFAULT 'mythic',
playable_npc_count INTEGER NOT NULL DEFAULT 0,
landmark_count INTEGER NOT NULL DEFAULT 0,
updated_at TEXT NOT NULL,
PRIMARY KEY (user_id, profile_id),
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS custom_world_profiles_user_updated_idx
ON custom_world_profiles (user_id, updated_at DESC);
CREATE INDEX IF NOT EXISTS custom_world_profiles_published_idx
ON custom_world_profiles (visibility, published_at DESC, updated_at DESC);

View File

@@ -1,24 +0,0 @@
CREATE TABLE IF NOT EXISTS auth_identities (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
provider TEXT NOT NULL,
provider_uid TEXT NOT NULL,
provider_unionid TEXT,
display_name TEXT,
avatar_url TEXT,
is_verified BOOLEAN NOT NULL DEFAULT TRUE,
meta_json JSONB,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identities_provider_uid_unique_idx
ON auth_identities (provider, provider_uid);
CREATE UNIQUE INDEX IF NOT EXISTS auth_identities_provider_unionid_unique_idx
ON auth_identities (provider, provider_unionid)
WHERE provider_unionid IS NOT NULL;
CREATE INDEX IF NOT EXISTS auth_identities_user_idx
ON auth_identities (user_id, provider);

View File

@@ -1,17 +0,0 @@
CREATE TABLE IF NOT EXISTS user_sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
refresh_token_hash TEXT NOT NULL UNIQUE,
client_type TEXT NOT NULL,
user_agent TEXT,
ip TEXT,
expires_at TEXT NOT NULL,
revoked_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
last_seen_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS user_sessions_user_idx
ON user_sessions (user_id, expires_at DESC);

View File

@@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS auth_audit_logs (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL,
event_type TEXT NOT NULL,
detail TEXT NOT NULL,
ip TEXT,
user_agent TEXT,
meta_json JSONB,
created_at TEXT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS auth_audit_logs_user_created_idx
ON auth_audit_logs (user_id, created_at DESC);

View File

@@ -1,29 +0,0 @@
CREATE TABLE IF NOT EXISTS sms_auth_events (
id TEXT PRIMARY KEY,
phone_number TEXT NOT NULL,
scene TEXT NOT NULL,
action TEXT NOT NULL,
success BOOLEAN NOT NULL,
ip TEXT,
user_agent TEXT,
provider TEXT,
provider_request_id TEXT,
provider_biz_id TEXT,
provider_out_id TEXT,
delivery_status TEXT NOT NULL DEFAULT 'pending',
delivery_report_raw_json JSONB,
delivery_reported_at TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS sms_auth_events_phone_created_idx
ON sms_auth_events (phone_number, created_at DESC);
CREATE INDEX IF NOT EXISTS sms_auth_events_ip_created_idx
ON sms_auth_events (ip, created_at DESC);
CREATE INDEX IF NOT EXISTS sms_auth_events_provider_biz_id_idx
ON sms_auth_events (provider_biz_id);
CREATE INDEX IF NOT EXISTS sms_auth_events_provider_out_id_idx
ON sms_auth_events (provider_out_id);

View File

@@ -1,13 +0,0 @@
CREATE TABLE IF NOT EXISTS auth_risk_blocks (
id TEXT PRIMARY KEY,
scope_type TEXT NOT NULL,
scope_key TEXT NOT NULL,
reason TEXT NOT NULL,
expires_at TEXT NOT NULL,
lifted_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS auth_risk_blocks_scope_idx
ON auth_risk_blocks (scope_type, scope_key, expires_at DESC);

View File

@@ -1,8 +0,0 @@
# Final Schema SQL
This folder contains the final PostgreSQL table definitions, one table per file.
Notes:
- These files keep only the final schema shape.
- They do not preserve historical migration steps.
- The current runtime migration logic in `server-node/src/db/migrations.ts` is unchanged.

File diff suppressed because it is too large Load Diff

View File

@@ -1,240 +0,0 @@
import express from 'express';
import pinoHttp from 'pino-http';
import type { AppContext } from './context.js';
import { notFound } from './errors.js';
import { buildApiLogContext, withRouteMeta } from './http.js';
import { errorHandler } from './middleware/errorHandler.js';
import { requestIdMiddleware } from './middleware/requestId.js';
import { responseEnvelopeMiddleware } from './middleware/responseEnvelope.js';
import { createCharacterAssetRoutes } from './modules/assets/characterAssetRoutes.js';
import { createEditorRoutes } from './modules/editor/editorRoutes.js';
import { createAuthRoutes } from './routes/authRoutes.js';
import { createBigFishProxyRoutes } from './routes/bigFishProxyRoutes.js';
import { createCustomWorldAgentRoutes } from './routes/customWorldAgent.js';
import { createPuzzleProxyRoutes } from './routes/puzzleProxyRoutes.js';
import { createRpgEntrySaveRoutes } from './routes/rpg-entry/rpgEntrySaveRoutes.js';
import { createRpgWorldLibraryRoutes } from './routes/rpg-entry/rpgWorldLibraryRoutes.js';
import { createRpgProfileRoutes } from './routes/rpg-profile/rpgProfileRoutes.js';
import { createRpgRuntimeAiAssistRoutes } from './routes/rpg-runtime/rpgRuntimeAiAssistRoutes.js';
import { createRpgRuntimeStoryRoutes } from './routes/rpg-runtime/rpgRuntimeStoryRoutes.js';
function matchesRoutePrefix(
request: express.Request,
prefixes: readonly string[],
) {
const requestPath = request.path || request.originalUrl || request.url || '/';
return prefixes.some((prefix) => {
const normalizedPrefix = prefix.endsWith('/') ? prefix.slice(0, -1) : prefix;
return (
requestPath === normalizedPrefix ||
requestPath.startsWith(`${normalizedPrefix}/`)
);
});
}
function scopeToPrefixes(
prefixes: readonly string[],
handler: express.RequestHandler,
): express.RequestHandler {
return (request, response, next) => {
if (!matchesRoutePrefix(request, prefixes)) {
next();
return;
}
handler(request, response, next);
};
}
export function createApp(context: AppContext) {
const app = express();
const createHttpLogger = pinoHttp as unknown as (
options: Record<string, unknown>,
) => express.RequestHandler;
app.disable('x-powered-by');
app.use(requestIdMiddleware);
app.use(
createHttpLogger({
logger: context.logger,
genReqId: (request: express.Request) => request.requestId,
customSuccessObject: (
request: express.Request,
response: express.Response,
baseObject: Record<string, unknown> & { responseTime?: number },
) => ({
...baseObject,
...buildApiLogContext(request, response),
user_id: request.userId ?? null,
method: request.method,
path: request.url,
status: response.statusCode,
latency_ms: baseObject.responseTime,
}),
customErrorObject: (
request: express.Request,
response: express.Response,
error: unknown,
baseObject: Record<string, unknown> & { responseTime?: number },
) => ({
...baseObject,
...buildApiLogContext(request, response),
user_id: request.userId ?? null,
method: request.method,
path: request.url,
status: response.statusCode,
latency_ms: baseObject.responseTime,
err: error,
}),
}),
);
app.use(express.json({ limit: '10mb' }));
app.use(responseEnvelopeMiddleware);
app.get(
'/healthz',
withRouteMeta({ operation: 'health.check' }),
(_request, response) => {
response.json({
ok: true,
service: 'genarrative-node-server',
});
},
);
app.use(
scopeToPrefixes(
['/api/editor'],
withRouteMeta({ routeVersion: '2026-04-08', operation: 'editor.api' }),
),
);
app.use(scopeToPrefixes(['/api/editor'], createEditorRoutes(context.config)));
app.use(
scopeToPrefixes(
['/api/assets'],
withRouteMeta({ routeVersion: '2026-04-08', operation: 'assets.api' }),
),
);
app.use(
scopeToPrefixes(
['/api/assets'],
createCharacterAssetRoutes(context.config, context.llmClient),
),
);
app.use(
'/api',
scopeToPrefixes(
['/runtime/profile', '/profile', '/runtime/settings'],
withRouteMeta({ routeVersion: '2026-04-21', operation: 'rpg.profile.api' }),
),
createRpgProfileRoutes(context),
);
app.use(
'/api',
scopeToPrefixes(
['/runtime/save', '/runtime/profile/save-archives', '/profile/save-archives'],
withRouteMeta({ routeVersion: '2026-04-21', operation: 'rpg.entry.save.api' }),
),
createRpgEntrySaveRoutes(context),
);
app.use(
'/api',
scopeToPrefixes(
['/runtime/custom-world-gallery', '/runtime/custom-world/works', '/runtime/custom-world-library'],
withRouteMeta({
routeVersion: '2026-04-21',
operation: 'rpg.entry.worldLibrary.api',
}),
),
createRpgWorldLibraryRoutes(context),
);
app.use(
'/api/auth',
withRouteMeta({ routeVersion: '2026-04-08' }),
createAuthRoutes(context),
);
app.use(
'/api/runtime/story',
withRouteMeta({ routeVersion: '2026-04-21' }),
createRpgRuntimeStoryRoutes(context),
);
app.use(
scopeToPrefixes(
[
'/llm/chat/completions',
'/custom-world/cover-image',
'/custom-world/cover-upload',
'/custom-world/scene-image',
'/custom-world/entity',
'/custom-world/scene-npc',
'/runtime/custom-world/entity',
'/runtime/custom-world/scene-npc',
'/runtime/custom-world/profile',
'/runtime/story/initial',
'/runtime/story/continue',
'/runtime/chat',
'/runtime/items',
'/runtime/quests',
'/ws/health',
],
withRouteMeta({
routeVersion: '2026-04-21',
operation: 'rpg.runtime.aiAssist.api',
}),
),
);
app.use(
'/api',
scopeToPrefixes(
[
'/llm/chat/completions',
'/custom-world/cover-image',
'/custom-world/cover-upload',
'/custom-world/scene-image',
'/custom-world/entity',
'/custom-world/scene-npc',
'/runtime/custom-world/entity',
'/runtime/custom-world/scene-npc',
'/runtime/custom-world/profile',
'/runtime/story/initial',
'/runtime/story/continue',
'/runtime/chat',
'/runtime/items',
'/runtime/quests',
'/ws/health',
],
createRpgRuntimeAiAssistRoutes(context),
),
);
app.use(
'/api/runtime/custom-world/agent',
withRouteMeta({ routeVersion: '2026-04-21', operation: 'rpg.creation.agent.api' }),
createCustomWorldAgentRoutes(context),
);
app.use(
'/api/runtime/big-fish',
withRouteMeta({ routeVersion: '2026-04-22', operation: 'bigFish.runtime.proxy.api' }),
createBigFishProxyRoutes(context),
);
app.use(
'/api/runtime/puzzle',
withRouteMeta({ routeVersion: '2026-04-22', operation: 'puzzle.runtime.proxy.api' }),
createPuzzleProxyRoutes(context),
);
app.use(
express.static(context.config.publicDir, {
fallthrough: true,
index: false,
}),
);
app.use((request, _response, next) => {
next(notFound(`接口不存在:${request.method} ${request.originalUrl}`));
});
app.use(errorHandler);
return app;
}

View File

@@ -1,15 +0,0 @@
import type { Request } from 'express';
export type AuthRequestContext = {
clientType: string;
userAgent: string | null;
ip: string | null;
};
export function buildAuthRequestContext(request: Request): AuthRequestContext {
return {
clientType: 'browser',
userAgent: request.header('user-agent')?.trim() || null,
ip: request.ip || null,
};
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,16 +0,0 @@
import { Algorithm, hash, verify } from '@node-rs/argon2';
export async function hashPassword(password: string) {
return hash(password, {
algorithm: Algorithm.Argon2id,
memoryCost: 19456,
timeCost: 2,
parallelism: 1,
});
}
export async function verifyPassword(passwordHash: string, password: string) {
return verify(passwordHash, password, {
algorithm: Algorithm.Argon2id,
});
}

View File

@@ -1,55 +0,0 @@
import { badRequest } from '../errors.js';
export type NormalizedPhoneNumber = {
countryCode: string;
nationalNumber: string;
e164: string;
maskedNationalNumber: string;
};
function stripPhoneInput(input: string) {
return input.replace(/[^\d+]/gu, '').trim();
}
export function maskNationalPhoneNumber(phoneNumber: string) {
if (phoneNumber.length < 7) {
return phoneNumber;
}
return `${phoneNumber.slice(0, 3)}****${phoneNumber.slice(-4)}`;
}
export function normalizeMainlandChinaPhoneNumber(
phoneInput: string,
): NormalizedPhoneNumber {
const trimmed = stripPhoneInput(phoneInput);
if (!trimmed) {
throw badRequest('请输入手机号');
}
let nationalNumber = trimmed;
if (nationalNumber.startsWith('+86')) {
nationalNumber = nationalNumber.slice(3);
} else if (nationalNumber.startsWith('86') && nationalNumber.length === 13) {
nationalNumber = nationalNumber.slice(2);
}
if (!/^1\d{10}$/u.test(nationalNumber)) {
throw badRequest('请输入正确的中国大陆手机号');
}
return {
countryCode: '86',
nationalNumber,
e164: `+86${nationalNumber}`,
maskedNationalNumber: maskNationalPhoneNumber(nationalNumber),
};
}
export function validateSmsVerifyCode(verifyCode: string) {
const normalizedVerifyCode = verifyCode.trim();
if (!/^[A-Za-z0-9]{4,8}$/u.test(normalizedVerifyCode)) {
throw badRequest('请输入正确的验证码');
}
return normalizedVerifyCode;
}

View File

@@ -1,111 +0,0 @@
import crypto from 'node:crypto';
import type { Request, Response } from 'express';
import type { AppConfig } from '../config.js';
export type RefreshSessionRequestContext = {
clientType: string;
userAgent: string | null;
ip: string | null;
};
function buildCookieParts(
config: AppConfig,
value: string,
options: {
maxAgeSeconds: number;
},
) {
const parts = [
`${config.authSession.refreshCookieName}=${encodeURIComponent(value)}`,
`Path=${config.authSession.refreshCookiePath}`,
'HttpOnly',
`SameSite=${config.authSession.refreshCookieSameSite}`,
`Max-Age=${Math.max(0, Math.floor(options.maxAgeSeconds))}`,
];
if (config.authSession.refreshCookieSecure) {
parts.push('Secure');
}
return parts.join('; ');
}
function appendSetCookieHeader(response: Response, cookieValue: string) {
const currentHeader = response.getHeader('Set-Cookie');
if (!currentHeader) {
response.setHeader('Set-Cookie', cookieValue);
return;
}
if (Array.isArray(currentHeader)) {
response.setHeader('Set-Cookie', [...currentHeader, cookieValue]);
return;
}
response.setHeader('Set-Cookie', [String(currentHeader), cookieValue]);
}
export function hashRefreshSessionToken(token: string) {
return crypto.createHash('sha256').update(token).digest('hex');
}
export function createRefreshSessionToken() {
return crypto.randomBytes(32).toString('base64url');
}
export function setRefreshSessionCookie(
response: Response,
config: AppConfig,
token: string,
maxAgeSeconds: number,
) {
appendSetCookieHeader(
response,
buildCookieParts(config, token, {
maxAgeSeconds,
}),
);
}
export function clearRefreshSessionCookie(response: Response, config: AppConfig) {
appendSetCookieHeader(
response,
buildCookieParts(config, '', {
maxAgeSeconds: 0,
}),
);
}
export function readRefreshSessionToken(request: Request, config: AppConfig) {
const cookieHeader = request.header('cookie')?.trim() || '';
if (!cookieHeader) {
return '';
}
const cookieEntries = cookieHeader.split(';');
for (const entry of cookieEntries) {
const [rawName, ...valueParts] = entry.split('=');
const name = rawName?.trim();
if (name !== config.authSession.refreshCookieName) {
continue;
}
const rawValue = valueParts.join('=').trim();
return rawValue ? decodeURIComponent(rawValue) : '';
}
return '';
}
export function buildRefreshSessionRequestContext(
request: Request,
): RefreshSessionRequestContext {
const userAgent = request.header('user-agent')?.trim() || null;
return {
clientType: 'browser',
userAgent,
ip: request.ip || null,
};
}

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