Files
Genarrative/docs/audits/FUNCTION_DESIGN_AUDIT_2026-04-03.md
kdletters cbc27bad4a
Some checks failed
CI / verify (push) Has been cancelled
init with react+axum+spacetimedb
2026-04-26 18:06:23 +08:00

14 KiB
Raw Permalink Blame History

当前 Function 设计审计2026-04-03

审计范围

本次审计重点阅读并对照了这些位置:

  • docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.md
  • docs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.md
  • docs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.md
  • docs/audits/engineering/README.md
  • src/data/stateFunctions.ts
  • src/data/npcInteractions.ts
  • src/data/treasureInteractions.ts
  • src/hooks/useStoryGeneration.ts
  • src/hooks/story/npcEncounterActions.ts
  • src/hooks/story/npcInteraction.ts
  • src/hooks/story/progressionActions.ts
  • src/hooks/story/storyGenerationState.ts
  • src/services/ai.ts
  • src/services/prompt.ts
  • src/components/AdventurePanel.tsx
  • src/components/StateFunctionEditor.tsx

先说结论

当前运行时的“function”不是单一体系而是至少分成了 4 层:

  1. stateFunctions.ts 里的基础战斗 / 空闲 function。
  2. npcInteractions.ts 里的 NPC 交互 function。
  3. treasureInteractions.ts 里的宝藏交互 function。
  4. useStoryGeneration.ts / npcInteraction.ts / 背包装备锻造里额外加出来的“流程控制型 functionId”。

“选完选项触发 function 后没有触发剧情推理”这类现象,当前主要有 3 种来源:

  1. 按设计先走本地分流,不会在第一次点击时立刻推理。
  2. 剧情推理其实已经完成,但被“继续冒险”这道 UI 中转门挡住了。
  3. 真正的可见性 bugstory_continue_adventure 的文案常量已经乱码,导致 UI 提示失效,用户很容易误判为没继续推理。

1. 当前 function 体系分层

1.1 基础状态 functionsrc/data/stateFunctions.ts

这一层才是严格意义上的“状态 function 注册表”,由 resolveFunctionOption 统一解析。

当前运行时实际启用 12 个:

  • 战斗类 7 个:
    • battle_all_in_crush
    • battle_guard_break
    • battle_probe_pressure
    • battle_feint_step
    • battle_recover_breath
    • battle_finisher_window
    • battle_escape_breakout
  • 空闲类 5 个:
    • idle_explore_forward
    • idle_travel_next_scene
    • idle_rest_focus
    • idle_observe_signs
    • idle_call_out

关键设计点:

  • 定义源头是 BATTLE_FUNCTIONS / IDLE_FUNCTIONS
  • 最终运行时集合由 buildStateFunctionDefinitions 产出。
  • 可执行过滤走 getExecutableFunctions
  • 文案和视觉包装走 resolveFunctionOption
  • 默认选项池走 getDefaultFunctionIdsForContext

注意:

  • idle_follow_clue 仍然留在源码和 prompt 描述里,但在 applyRuntimeFunctionAdjustments 中被直接过滤掉,不会进入运行时 function 集合。
  • src/components/game-shell/useSceneTransitionModel.ts 里仍然保留了 idle_follow_clue 的转场映射,这说明当前 function 清单并不完全一致。

1.2 NPC 交互 functionsrc/data/npcInteractions.ts

这一层不是通过 resolveFunctionOption 生成的,而是 buildNpcEncounterStoryMoment 直接拼出 StoryOption,并挂上 interaction.kind = 'npc'

按功能类型看,当前会出现这些 functionId

  • npc_preview_talk
  • npc_trade
  • npc_fight
  • npc_spar
  • npc_help
  • npc_chat(可重复出现 2 个以上,代表不同聊天话题)
  • npc_gift
  • npc_recruit
  • npc_quest_accept
  • npc_quest_turn_in
  • npc_leave

关键设计点:

  • 这一层的 function 是“眼前 NPC 交互目录”,不是基础状态机目录。
  • 真正执行分流不在 stateFunctions.ts,而在 handleNpcInteraction / resolveNpcInteractionDecision
  • prompt 层通过 availableOptions 把这些 function 当作“固定可选项列表”交给模型,要求模型保留数量和 functionId

1.3 宝藏交互 functionsrc/data/treasureInteractions.ts

这一层同样不是 resolveFunctionOption 体系,而是直接构造带 interaction.kind = 'treasure' 的选项。

当前有 3 个:

  • treasure_secure
  • treasure_inspect
  • treasure_leave

执行时由 useTreasureFlow.ts 接管,最终再回到 commitGeneratedState 继续剧情推理。

1.4 流程控制 / 面板动作 functionId

这类 functionId 会进入 lastFunctionIdcommitGeneratedState,但不在 stateFunctions.ts 注册表里:

  • 流程控制:
    • story_continue_adventure
    • camp_travel_home_scene
    • story_opening_camp_dialogue
  • 面板动作:
    • inventory_use
    • equipment_equip
    • equipment_unequip
    • forge_craft
    • forge_dismantle
    • forge_reforge

这说明当前项目里“functionId”已经同时承担了 3 个角色:

  • 运行时基础状态动作 ID
  • NPC / 宝藏交互动作 ID
  • 流程 / 面板事件 ID

这能跑,但可追踪性已经开始分裂。


2. 当前剧情推理触发链路

2.1 会立刻触发剧情推理的主链

这类点击后会直接走 AI 续推:

  • 普通战斗 / 空闲 function
    • handleChoice
    • buildResolvedChoiceState
    • playResolvedChoice
    • generateNextStep
  • 这些 NPC 交互
    • npc_preview_talk
    • npc_help
    • npc_chat
    • npc_fight
    • npc_spar
    • npc_quest_accept
    • npc_quest_turn_in
    • npc_leave
  • 宝藏交互确认后
    • treasure_secure
    • treasure_inspect
    • treasure_leave
  • 面板动作确认后
    • inventory_use
    • equipment_equip
    • equipment_unequip
    • forge_*

共性是:

  • 要么直接在 handleChoice 里调用 generateNextStep
  • 要么先走 commitGeneratedState / commitGeneratedStateWithEncounterEntry,再统一调用 generateStoryForState

2.2 第一次点击不会立刻触发剧情推理的分流

这类最容易被误判成“function 触发了,但剧情没继续”:

  • npc_trade
  • npc_gift
  • npc_recruit(队伍满时必进 modal队伍未满时会先进招募对话流

原因不是漏调,而是当前设计明确先走:

  • resolveNpcInteractionDecision
  • trade_modal
  • gift_modal
  • recruit_modal
  • recruit_immediate

也就是说:

  • 第一次点击只是打开模态框 / 进入招募流程
  • 真正推进剧情推理,要等到:
    • confirmTrade
    • confirmGift
    • confirmRecruit
    • executeRecruitment

如果产品预期是“用户每点一次选项就必须立即看到剧情继续”,这一层现在不满足。

2.3 npc_chat 是“先推理,再延迟展示选项”

npc_chat 不是普通的“生成下一幕 + 立刻给选项”,而是单独走这条链:

  1. commitNpcChatState 先流式生成聊天正文。
  2. 聊天结束后,再调用一次 generateNextStep
  3. 新一轮冒险选项不直接显示,而是塞进 deferredOptions
  4. 当前界面只先显示一个 story_continue_adventure
  5. 用户再点一次,才把 deferredOptions 放出来。

所以这里常见的误判是:

  • 剧情推理其实已经做完了。
  • 只是 UI 先让用户“继续冒险”一次,才把新选项展示出来。

3. 本次排查发现的重点问题

3.1 真正最像“没触发剧情推理”的地方:模态框型 NPC function

定位:

  • src/hooks/story/npcEncounterActions.ts:427-449
  • src/hooks/story/storyGenerationState.ts:41-80
  • src/hooks/story/npcInteraction.ts:427-585

现象:

  • 点击 npc_trade / npc_gift / npc_recruit 后,故事文本区通常不会立刻变化。
  • 当前故事也不会马上追加一段新的 storyText

原因:

  • 这些 function 的第一次点击只做本地 UI 分流。
  • 直到用户在 modal 里确认,才会真正调用 commitGeneratedState 继续推理。

判断:

  • 这不是单纯 bug而是当前设计本身如此。
  • 但如果玩家从“选项即剧情推进”的心智出发,会非常像“点了没反应”。

建议优先级:高。

3.2 最关键的实际 bugstory_continue_adventure 文案乱码,导致“已推理但未显式提示”

定位:

  • src/hooks/useStoryGeneration.ts:92-96
  • src/hooks/story/npcEncounterActions.ts:281-400
  • src/components/AdventurePanel.tsx:534
  • src/components/AdventurePanel.tsx:860
  • src/components/AdventurePanel.tsx:879

现象:

  • npc_chat 完成后,系统本应展示一个“继续冒险”按钮,提示“剧情推理完成,继续后显示新的冒险选项”。
  • 但当前 CONTINUE_ADVENTURE_ACTION_TEXT 常量已经写成了乱码:缁х画鍐掗櫓
  • AdventurePanel 又是靠 option.actionText === '继续冒险' 来识别这个特殊按钮。

直接后果:

  • 特殊提示 UI 不会出现。
  • 玩家只会看到一个普通且乱码的单选项。
  • 实际上 deferredOptions 已经算好了,但用户极容易误会为“剧情没有继续推理”。

判断:

  • 这是本次排查里最明确的实现级 bug。
  • 它不会阻止底层推理发生,但会严重破坏“推理已完成”的可见性。

建议优先级:最高。

3.3 StateFunctionEditor 覆盖不到真正最容易出问题的 function

定位:

  • src/components/StateFunctionEditor.tsx:13-21
  • src/components/StateFunctionEditor.tsx:488
  • src/components/StateFunctionEditor.tsx:772-838
  • src/components/StateFunctionEditor.tsx:992-1211

现象:

  • 编辑器预览只接了 buildStateFunctionDefinitions / getAllStateFunctionDefinitions / resolveFunctionOption
  • 也就是它只能预览 stateFunctions.ts 那 12 个基础 function。

覆盖不到的关键分支:

  • npc_*
  • treasure_*
  • npc_preview_talk
  • story_continue_adventure
  • modal 分流
  • npc_chatdeferredOptions

判断:

  • 这不是运行时 bug但它解释了为什么这类问题很难在编辑器里提前暴露。
  • 当前“Function 编辑器”并没有覆盖到最复杂、最容易让用户感知为异常的 function 链路。

建议优先级:高。

3.4 function 清单存在“源码有、运行时无、别处还在引用”的分裂

定位:

  • src/data/stateFunctions.ts:350
  • src/data/stateFunctions.ts:429-441
  • src/components/game-shell/useSceneTransitionModel.ts:19-25

现象:

  • idle_follow_clue 仍在定义表、提示词描述、若干 switch 分支中存在。
  • 但最终在 applyRuntimeFunctionAdjustments 被过滤掉,不会进入运行时 function 集合。
  • useSceneTransitionModel 里却还保留着它的转场模式映射。

判断:

  • 这不是“没触发剧情推理”的直接原因。
  • 但会让维护者误判当前真实 function 清单,也会增加后续继续扩 function 时的混乱。

建议优先级:中。

3.5 自动化测试几乎没有覆盖“最容易让人误会没推理”的链路

定位:

  • 当前已有测试主要是 src/hooks/story/storyGenerationState.test.ts

已覆盖:

  • trade_modal
  • recruit_modal
  • 地图切场景

未覆盖:

  • npc_chatdeferredOptions -> story_continue_adventure -> 真正显示新选项
  • npc_trade / npc_gift / npc_recruit 确认后是否一定调用 commitGeneratedState
  • story_continue_adventure 文案与 UI 特判是否一致
  • npc_preview_talk -> enterNpcInteraction -> generateStoryForState
  • 宝藏分支确认后是否稳定续推

判断:

  • 这类问题之所以能长期存在,一个核心原因就是最关键的续推分支没有测试兜底

建议优先级:高。


4. 按“首次点击是否立即触发剧情推理”整理

4.1 首次点击就会继续推理

  • battle_*
  • idle_*
  • npc_preview_talk
  • npc_help
  • npc_chat
  • npc_fight
  • npc_spar
  • npc_quest_accept
  • npc_quest_turn_in
  • npc_leave
  • treasure_*
  • inventory_use
  • equipment_*
  • forge_*

4.2 首次点击不会立刻继续推理

  • npc_trade
  • npc_gift
  • npc_recruit(至少会先进入确认 / 对话流)
  • story_continue_adventure

这里要特别分清:

  • npc_trade / npc_gift / npc_recruit先分流,后确认,确认后再推理
  • story_continue_adventure推理已经完成,只是先把结果选项延后展示

5. 建议的修正顺序

P0

  • 修掉 CONTINUE_ADVENTURE_ACTION_TEXT 的乱码。
  • AdventurePanel 不要再靠 actionText === '继续冒险' 判定特殊按钮,改成按 functionId === 'story_continue_adventure' 判定。

P1

  • 明确产品规则:
    • npc_trade / npc_gift / npc_recruit 第一次点击是否就应该写入一条“进入交易 / 送礼 / 招募确认”的剧情反馈。
  • 如果答案是“应该”,那这些 modal 型 function 需要补一层轻量 story feedback而不是只弹框。

P1

  • npc_chat 补自动化测试:
    • 聊天后必须出现 story_continue_adventure
    • 第二次点击后必须能展示 deferredOptions

P1

  • npc_trade / npc_gift / npc_recruit 补自动化测试:
    • 第一次点击只分流
    • 确认后一定触发 commitGeneratedState
    • 后续一定能拿到新的 StoryMoment

P2

  • 扩展 StateFunctionEditor 或补新的“交互 function 预览器”,把 npc_* / treasure_* / story_continue_adventure 也纳入可预演范围。

P2

  • 清理 idle_follow_clue 这类“半退场”的 function保证
    • 运行时集合
    • prompt 描述
    • 编辑器
    • 转场映射
    • 测试

保持一致。


最后结论

当前最值得优先处理的,不是再继续加新的 function而是先把这 3 个点收拢:

  1. 把“首次点击只分流、不推理”的 function 明确标出来。
  2. npc_chat -> story_continue_adventure -> deferredOptions 这条延迟展示链做清楚。
  3. 先修掉 story_continue_adventure 的乱码和基于文案的 UI 判断。

如果不先处理这几个点,用户会持续把“按设计延迟”与“真实漏调剧情推理”混在一起感知,后面 function 越多,这类问题会越难排查。