Files
Genarrative/docs/audits/engineering/ENGINEERING_OPTIMIZATION_REVIEW_2026-03-30.md
高物 ddcb5d5c8c
Some checks failed
CI / verify (push) Has been cancelled
Rework story engine flow and reorganize project docs
2026-04-06 23:19:00 +08:00

291 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 工程优化审查报告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 门禁变硬、再顺手压缩运行时大模块。只要这一步补上,后续加剧情、加编辑器能力、加自定义世界都会轻很多。