14 KiB
当前 Function 设计审计(2026-04-03)
审计范围
本次审计重点阅读并对照了这些位置:
docs/experience/ADVENTURE_RUNTIME_DEV_EXPERIENCE.mddocs/experience/PROJECT_WORK_EXPERIENCE_PLAYBOOK.mddocs/experience/PROJECT_DEVELOPMENT_EXPERIENCE.mddocs/audits/engineering/README.mdsrc/data/stateFunctions.tssrc/data/npcInteractions.tssrc/data/treasureInteractions.tssrc/hooks/useStoryGeneration.tssrc/hooks/story/npcEncounterActions.tssrc/hooks/story/npcInteraction.tssrc/hooks/story/progressionActions.tssrc/hooks/story/storyGenerationState.tssrc/services/ai.tssrc/services/prompt.tssrc/components/AdventurePanel.tsxsrc/components/StateFunctionEditor.tsx
先说结论
当前运行时的“function”不是单一体系,而是至少分成了 4 层:
stateFunctions.ts里的基础战斗 / 空闲 function。npcInteractions.ts里的 NPC 交互 function。treasureInteractions.ts里的宝藏交互 function。useStoryGeneration.ts/npcInteraction.ts/ 背包装备锻造里额外加出来的“流程控制型 functionId”。
“选完选项触发 function 后没有触发剧情推理”这类现象,当前主要有 3 种来源:
- 按设计先走本地分流,不会在第一次点击时立刻推理。
- 剧情推理其实已经完成,但被“继续冒险”这道 UI 中转门挡住了。
- 真正的可见性 bug:
story_continue_adventure的文案常量已经乱码,导致 UI 提示失效,用户很容易误判为没继续推理。
1. 当前 function 体系分层
1.1 基础状态 function:src/data/stateFunctions.ts
这一层才是严格意义上的“状态 function 注册表”,由 resolveFunctionOption 统一解析。
当前运行时实际启用 12 个:
- 战斗类 7 个:
battle_all_in_crushbattle_guard_breakbattle_probe_pressurebattle_feint_stepbattle_recover_breathbattle_finisher_windowbattle_escape_breakout
- 空闲类 5 个:
idle_explore_forwardidle_travel_next_sceneidle_rest_focusidle_observe_signsidle_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 交互 function:src/data/npcInteractions.ts
这一层不是通过 resolveFunctionOption 生成的,而是 buildNpcEncounterStoryMoment 直接拼出 StoryOption,并挂上 interaction.kind = 'npc'。
按功能类型看,当前会出现这些 functionId:
npc_preview_talknpc_tradenpc_fightnpc_sparnpc_helpnpc_chat(可重复出现 2 个以上,代表不同聊天话题)npc_giftnpc_recruitnpc_quest_acceptnpc_quest_turn_innpc_leave
关键设计点:
- 这一层的 function 是“眼前 NPC 交互目录”,不是基础状态机目录。
- 真正执行分流不在
stateFunctions.ts,而在handleNpcInteraction/resolveNpcInteractionDecision。 - prompt 层通过
availableOptions把这些 function 当作“固定可选项列表”交给模型,要求模型保留数量和functionId。
1.3 宝藏交互 function:src/data/treasureInteractions.ts
这一层同样不是 resolveFunctionOption 体系,而是直接构造带 interaction.kind = 'treasure' 的选项。
当前有 3 个:
treasure_securetreasure_inspecttreasure_leave
执行时由 useTreasureFlow.ts 接管,最终再回到 commitGeneratedState 继续剧情推理。
1.4 流程控制 / 面板动作 functionId
这类 functionId 会进入 lastFunctionId 或 commitGeneratedState,但不在 stateFunctions.ts 注册表里:
- 流程控制:
story_continue_adventurecamp_travel_home_scenestory_opening_camp_dialogue
- 面板动作:
inventory_useequipment_equipequipment_unequipforge_craftforge_dismantleforge_reforge
这说明当前项目里“functionId”已经同时承担了 3 个角色:
- 运行时基础状态动作 ID
- NPC / 宝藏交互动作 ID
- 流程 / 面板事件 ID
这能跑,但可追踪性已经开始分裂。
2. 当前剧情推理触发链路
2.1 会立刻触发剧情推理的主链
这类点击后会直接走 AI 续推:
- 普通战斗 / 空闲 function
handleChoicebuildResolvedChoiceStateplayResolvedChoicegenerateNextStep
- 这些 NPC 交互
npc_preview_talknpc_helpnpc_chatnpc_fightnpc_sparnpc_quest_acceptnpc_quest_turn_innpc_leave
- 宝藏交互确认后
treasure_securetreasure_inspecttreasure_leave
- 面板动作确认后
inventory_useequipment_equipequipment_unequipforge_*
共性是:
- 要么直接在
handleChoice里调用generateNextStep。 - 要么先走
commitGeneratedState/commitGeneratedStateWithEncounterEntry,再统一调用generateStoryForState。
2.2 第一次点击不会立刻触发剧情推理的分流
这类最容易被误判成“function 触发了,但剧情没继续”:
npc_tradenpc_giftnpc_recruit(队伍满时必进 modal;队伍未满时会先进招募对话流)
原因不是漏调,而是当前设计明确先走:
resolveNpcInteractionDecisiontrade_modalgift_modalrecruit_modalrecruit_immediate
也就是说:
- 第一次点击只是打开模态框 / 进入招募流程。
- 真正推进剧情推理,要等到:
confirmTradeconfirmGiftconfirmRecruitexecuteRecruitment
如果产品预期是“用户每点一次选项就必须立即看到剧情继续”,这一层现在不满足。
2.3 npc_chat 是“先推理,再延迟展示选项”
npc_chat 不是普通的“生成下一幕 + 立刻给选项”,而是单独走这条链:
commitNpcChatState先流式生成聊天正文。- 聊天结束后,再调用一次
generateNextStep。 - 新一轮冒险选项不直接显示,而是塞进
deferredOptions。 - 当前界面只先显示一个
story_continue_adventure。 - 用户再点一次,才把
deferredOptions放出来。
所以这里常见的误判是:
- 剧情推理其实已经做完了。
- 只是 UI 先让用户“继续冒险”一次,才把新选项展示出来。
3. 本次排查发现的重点问题
3.1 真正最像“没触发剧情推理”的地方:模态框型 NPC function
定位:
src/hooks/story/npcEncounterActions.ts:427-449src/hooks/story/storyGenerationState.ts:41-80src/hooks/story/npcInteraction.ts:427-585
现象:
- 点击
npc_trade/npc_gift/npc_recruit后,故事文本区通常不会立刻变化。 - 当前故事也不会马上追加一段新的
storyText。
原因:
- 这些 function 的第一次点击只做本地 UI 分流。
- 直到用户在 modal 里确认,才会真正调用
commitGeneratedState继续推理。
判断:
- 这不是单纯 bug,而是当前设计本身如此。
- 但如果玩家从“选项即剧情推进”的心智出发,会非常像“点了没反应”。
建议优先级:高。
3.2 最关键的实际 bug:story_continue_adventure 文案乱码,导致“已推理但未显式提示”
定位:
src/hooks/useStoryGeneration.ts:92-96src/hooks/story/npcEncounterActions.ts:281-400src/components/AdventurePanel.tsx:534src/components/AdventurePanel.tsx:860src/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-21src/components/StateFunctionEditor.tsx:488src/components/StateFunctionEditor.tsx:772-838src/components/StateFunctionEditor.tsx:992-1211
现象:
- 编辑器预览只接了
buildStateFunctionDefinitions/getAllStateFunctionDefinitions/resolveFunctionOption。 - 也就是它只能预览
stateFunctions.ts那 12 个基础 function。
覆盖不到的关键分支:
npc_*treasure_*npc_preview_talkstory_continue_adventure- modal 分流
npc_chat的deferredOptions
判断:
- 这不是运行时 bug,但它解释了为什么这类问题很难在编辑器里提前暴露。
- 当前“Function 编辑器”并没有覆盖到最复杂、最容易让用户感知为异常的 function 链路。
建议优先级:高。
3.4 function 清单存在“源码有、运行时无、别处还在引用”的分裂
定位:
src/data/stateFunctions.ts:350src/data/stateFunctions.ts:429-441src/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_modalrecruit_modal- 地图切场景
未覆盖:
npc_chat的deferredOptions -> story_continue_adventure -> 真正显示新选项npc_trade/npc_gift/npc_recruit确认后是否一定调用commitGeneratedStatestory_continue_adventure文案与 UI 特判是否一致npc_preview_talk -> enterNpcInteraction -> generateStoryForState- 宝藏分支确认后是否稳定续推
判断:
- 这类问题之所以能长期存在,一个核心原因就是最关键的续推分支没有测试兜底。
建议优先级:高。
4. 按“首次点击是否立即触发剧情推理”整理
4.1 首次点击就会继续推理
battle_*idle_*npc_preview_talknpc_helpnpc_chatnpc_fightnpc_sparnpc_quest_acceptnpc_quest_turn_innpc_leavetreasure_*inventory_useequipment_*forge_*
4.2 首次点击不会立刻继续推理
npc_tradenpc_giftnpc_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 个点收拢:
- 把“首次点击只分流、不推理”的 function 明确标出来。
- 把
npc_chat -> story_continue_adventure -> deferredOptions这条延迟展示链做清楚。 - 先修掉
story_continue_adventure的乱码和基于文案的 UI 判断。
如果不先处理这几个点,用户会持续把“按设计延迟”与“真实漏调剧情推理”混在一起感知,后面 function 越多,这类问题会越难排查。