# 前端应迁后端逻辑审计(2026-04-21) 更新时间:`2026-04-21` ## 0. 审计目标 这份文档只回答一个问题: **当前前端代码里,哪些逻辑已经明显越过“前端只做表现,Express 后端负责逻辑、数据与存储”的边界,应该继续迁到后端。** 本轮不改业务代码,只做: 1. 基于当前仓库状态给出高置信度候选点 2. 标明代码证据 3. 给出迁移优先级 4. 说明迁移后前端应该保留什么、移走什么 --- ## 1. 结论先行 结合当前代码与已有边界文档,前端里仍有 6 类逻辑应该继续后移: 1. **运行时快照前置写入与本地镜像解释** 2. **鉴权 token 的浏览器本地真相** 3. **平台浏览历史的本地真相与迁移状态** 4. **NPC 待接委托“换单”仍由前端直接触发正式生成** 5. **quest/runtime item 的双环境混合编排** 6. **浏览器侧大型 AI orchestration 与 prompt/repair/fallback 主链** 一句话判断: **当前前端已经不是最早那种“大量主算”的状态,但仍然保留了运行时镜像、生成编排和部分正式真相。后端边界还需要再收一轮,前端才算真正退回表现层。** --- ## 2. 审计依据 ### 2.1 文档依据 1. `docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md` 2. `docs/planning/EXPRESS_BACKEND_REFACTOR_PLAN_2026-04-08.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` 5. `docs/audits/engineering/CURRENT_ENGINEERING_OPTIMIZATION_OPPORTUNITIES_2026-04-20.md` ### 2.2 当前代码依据 1. `src/hooks/story/runtimeStoryCoordinator.ts` 2. `src/services/apiClient.ts` 3. `src/services/platformBrowseHistory.ts` 4. `src/components/game-shell/PreGameSelectionFlow.tsx` 5. `src/hooks/story/npcEncounterActions.ts` 6. `src/services/questDirector.ts` 7. `src/services/runtimeItemAiDirector.ts` 8. `src/services/ai.ts` --- ## 3. 当前高置信度应后移逻辑 ## 3.1 运行时快照前置写入仍在前端 ### 代码证据 `src/hooks/story/runtimeStoryCoordinator.ts` 当前仍存在以下链路: 1. `syncRuntimeSnapshot(...)` 2. `syncRuntimeSnapshot(...)` 内部直接调用 `putSaveSnapshot(...)` 3. `loadServerRuntimeOptionCatalog(...)` 在请求 `getRuntimeStoryState(...)` 之前先写本地快照 4. `resolveServerRuntimeChoice(...)` 在请求 `resolveRuntimeStoryAction(...)` 之前先写本地快照 对应位置: 1. `src/hooks/story/runtimeStoryCoordinator.ts:21` 2. `src/hooks/story/runtimeStoryCoordinator.ts:25` 3. `src/hooks/story/runtimeStoryCoordinator.ts:36` 4. `src/hooks/story/runtimeStoryCoordinator.ts:99` ### 当前问题 这意味着运行时正式动作发起前,前端仍会先落一份自己的快照真相,再去请求后端。 这条链的问题不是“有没有缓存”,而是: 1. 前端仍在承担正式提交前的状态镜像 2. 快照解释权没有完全收回到后端 3. 运行时主链仍处于“本地镜像 + 服务端会话”并存状态 ### 迁移建议 后端继续承接: 1. 运行时快照写入 2. 快照版本解释 3. 动作提交前的状态一致性校验 前端只保留: 1. 当前展示用的 view model 2. 可选的只读恢复缓存 3. 纯表现态的 loading / transition / animation state ### 优先级 `P0` --- ## 3.2 鉴权 token 仍由前端 localStorage 持有真相 ### 代码证据 `src/services/apiClient.ts` 当前仍直接访问 `window.localStorage` 保存 access token: 1. `getStoredAccessToken()` 2. `setStoredAccessToken(...)` 3. `clearStoredAccessToken(...)` 4. `withAuthorizationHeaders(...)` 直接从本地 token 组装请求头 对应位置: 1. `src/services/apiClient.ts:333` 2. `src/services/apiClient.ts:341` 3. `src/services/apiClient.ts:362` 4. `src/services/apiClient.ts:382` ### 当前问题 第三批清理已经收掉了“自动登录用户名/密码”本地真相,但 access token 仍然由浏览器长期持有。 这在当前项目边界下仍有两个问题: 1. 正式鉴权真相仍没有完全收回后端 session 边界 2. 前端 SDK 仍然负担 token 生命周期的关键部分 ### 迁移建议 后端继续承接: 1. session / refresh / cookie 真相 2. 鉴权状态续期 3. token 更新与失效策略 前端只保留: 1. 当前是否已登录的展示态 2. 统一的请求封装 3. 401 后的 UI 响应 ### 优先级 `P0` --- ## 3.3 平台浏览历史仍是“前端本地历史 + 后端回填”的双真相 ### 代码证据 `src/services/platformBrowseHistory.ts` 当前仍维护一整套本地历史真相: 1. `readPlatformBrowseHistory(...)` 2. `writePlatformBrowseHistory(...)` 3. `hasPendingPlatformBrowseHistoryMigration(...)` 4. `markPlatformBrowseHistoryMigrated(...)` 对应位置: 1. `src/services/platformBrowseHistory.ts:77` 2. `src/services/platformBrowseHistory.ts:103` 3. `src/services/platformBrowseHistory.ts:151` 4. `src/services/platformBrowseHistory.ts:164` `src/components/game-shell/PreGameSelectionFlow.tsx` 当前仍显式做: 1. 先 `writePlatformBrowseHistory(...)` 2. 再调用 `upsertProfileBrowseHistory(...)` 3. 同步成功后 `markPlatformBrowseHistoryMigrated(...)` 4. 启动阶段读取 `readPlatformBrowseHistory(...)` 5. 根据 `hasPendingPlatformBrowseHistoryMigration(...)` 决定是否补同步 对应位置: 1. `src/components/game-shell/PreGameSelectionFlow.tsx:383` 2. `src/components/game-shell/PreGameSelectionFlow.tsx:392` 3. `src/components/game-shell/PreGameSelectionFlow.tsx:394` 4. `src/components/game-shell/PreGameSelectionFlow.tsx:433` 5. `src/components/game-shell/PreGameSelectionFlow.tsx:466` ### 当前问题 这条链已经不是单纯缓存,而是: 1. 本地历史存储 2. 本地同步标记 3. 后端历史持久化 三套状态同时存在。 ### 迁移建议 后端继续承接: 1. 浏览历史唯一持久化真相 2. 历史去重、排序、截断 3. 迁移完成标记 前端只保留: 1. 展示缓存 2. 弱网下的临时 optimistic UI 3. 刷新后重新拉取远端结果 ### 优先级 `P1` --- ## 3.4 NPC 待接委托“换单”仍由前端直接发起正式生成 ### 代码证据 `src/hooks/story/npcEncounterActions.ts` 当前仍保留: 1. `replacePendingNpcQuestOffer = async () => { ... }` 2. 内部直接调用 `generateQuestForNpcEncounter(...)` 对应位置: 1. `src/hooks/story/npcEncounterActions.ts:1561` 2. `src/hooks/story/npcEncounterActions.ts:1595` ### 当前问题 聊天后是否挂出待接委托已经后移,但“换一份委托”这条分支仍然是: 1. 前端组装上下文 2. 前端决定调用生成 3. 前端直接把结果写回当前 story UI 这仍属于正式运行时任务编排没有收干净。 ### 迁移建议 后端继续承接: 1. NPC 待接委托换单决策 2. 是否允许换单 3. 换单后的任务草案生成 4. 对应聊天态快照回填 前端只保留: 1. 点击“换一份委托” 2. loading / error 展示 3. 消费后端返回的新 pending quest offer ### 优先级 `P0` --- ## 3.5 questDirector 仍是前端 SDK 与生成编排混合体 ### 代码证据 `src/services/questDirector.ts` 当前同时承担: 1. `generateQuestForNpcEncounter(...)` 2. 浏览器路径 `requestJson('/api/runtime/quests/generate')` 3. 非浏览器路径 `requestChatMessageContent(...)` 4. 本地 `compileQuestIntentToQuest(...)` fallback 对应位置: 1. `src/services/questDirector.ts:213` 2. `src/services/questDirector.ts:242` 3. `src/services/questDirector.ts:267` 4. `src/services/questDirector.ts:256` 5. `src/services/questDirector.ts:281` 6. `src/services/questDirector.ts:293` ### 当前问题 这类文件虽然浏览器正式路径已经优先走后端,但职责仍混在一起: 1. 前端 SDK 2. Quest prompt 编排 3. Quest intent 解析 4. deterministic fallback compile 这会导致边界长期模糊,也让前端仍像“半个服务端”。 ### 迁移建议 后端继续承接: 1. quest intent 生成 2. prompt 组装 3. JSON 解析 4. fallback compile 前端只保留: 1. `requestGenerateQuest(...)` 这类轻量 SDK 2. 请求参数组装 3. 结果消费 ### 优先级 `P1` --- ## 3.6 runtimeItemAiDirector 仍是前端 SDK 与意图生成混合体 ### 代码证据 `src/services/runtimeItemAiDirector.ts` 当前同时承担: 1. `generateRuntimeItemAiIntents(...)` 2. 浏览器路径 `requestJson('/api/runtime/items/runtime-intent')` 3. 非浏览器路径 `requestChatMessageContent(...)` 4. 本地 `buildRuntimeItemAiIntent(...)` fallback 对应位置: 1. `src/services/runtimeItemAiDirector.ts:84` 2. `src/services/runtimeItemAiDirector.ts:94` 3. `src/services/runtimeItemAiDirector.ts:118` ### 当前问题 它和 `questDirector` 是同类问题: 1. 正式浏览器路径已经走后端 2. 但前端文件仍然承担完整生成逻辑认知 3. 文件职责仍然是双环境混合 ### 迁移建议 后端继续承接: 1. runtime item intent prompt 2. 模型调用 3. 结果解析与 fallback 前端只保留: 1. 轻量请求 SDK 2. 结果到 UI 的映射 ### 优先级 `P1` --- ## 3.7 `src/services/ai.ts` 仍是浏览器侧正式 AI orchestration 热点 ### 代码证据 当前 `src/services/ai.ts` 仍直接承担以下正式链路: 1. `requestChatMessageContent(...)` 2. `requestPlainTextCompletionFromClient(...)` 3. `streamPlainTextCompletionFromClient(...)` 4. `generateCustomWorldProfile(...)` 5. `generateInitialStory(...)` 6. `generateNextStep(...)` 7. `streamNpcChatDialogue(...)` 8. `streamNpcRecruitDialogue(...)` 对应位置: 1. `src/services/ai.ts:1732` 2. `src/services/ai.ts:1868` 3. `src/services/ai.ts:2038` 4. `src/services/ai.ts:2339` 5. `src/services/ai.ts:2447` 6. `src/services/ai.ts:2487` 7. `src/services/ai.ts:2529` 8. `src/services/ai.ts:2570` 并且文件内仍保留: 1. JSON repair 2. prompt 组装 3. response normalize 4. fallback/offline 响应 5. 角色聊天建议与摘要生成 ### 当前问题 这说明浏览器端并不只是“请求一个后端接口”,而是还在承担: 1. prompt source 2. 生成策略 3. 错误修复 4. fallback 编排 5. 多类业务场景的正式 AI 出口 这与“前端只做表现”存在明确冲突。 ### 迁移建议 后端继续承接: 1. story / npc / recruit / custom-world 的 prompt 编排 2. JSON repair 3. fallback 策略 4. streaming orchestration 5. 模型调用与日志 前端只保留: 1. 轻量 AI SDK 2. SSE 文本流展示 3. UI fallback 呈现 ### 优先级 `P0` --- ## 4. 可以暂时保留在前端的部分 下面这些内容即使和上述模块同文件出现,也不属于必须后移的对象: 1. 面板开关、loading、error、streaming 文本展示 2. 动画时间线、过场状态、临时 UI 回显 3. 表单草稿、筛选词、排序选项 4. 只影响表现、不影响正式真相的 view model 拼接 迁移时要注意: **不是把所有前端代码都往后端搬,而是把“正式状态解释、规则裁决、生成编排、持久化真相”搬走。** --- ## 5. 推荐迁移顺序 ## 5.1 第一阶段 先收最危险的正式真相: 1. `runtimeStoryCoordinator.ts` 2. `apiClient.ts` 3. `npcEncounterActions.ts` 里的 quest replace 分支 原因: 1. 这三处最直接影响运行时真相和动作主链 2. 不先收这些,前端仍然不是纯表现层 ## 5.2 第二阶段 再拆双环境混合服务: 1. `questDirector.ts` 2. `runtimeItemAiDirector.ts` 3. `platformBrowseHistory.ts` 原因: 1. 这几处已经有后端承接基础 2. 迁移成本相对可控 ## 5.3 第三阶段 最后继续压缩浏览器 AI orchestration: 1. `src/services/ai.ts` 2. 相关 prompt builder / repair helper / offline fallback 原因: 1. 这部分体量大 2. 链路多 3. 更适合在前两阶段把 contract 稳住后集中拆 --- ## 6. 建议产出物 如果后续按这份文档继续落地,建议每一批都至少同步产出: 1. 一份落地文档,说明迁移了哪条链 2. 一组 contract/route 变更说明 3. 一组前端 SDK 收缩说明 4. 一组防回退测试 --- ## 7. 一句话结论 当前前端最需要继续后移的,不是零散小工具,而是: **运行时快照前置写入、鉴权 token、本地浏览历史真相、NPC 委托换单、quest/runtime item 双环境混合编排,以及 `src/services/ai.ts` 里仍然留在浏览器的正式 AI orchestration。**