From 3a1333d4009d491a16714a9fa528c8470003ab4b Mon Sep 17 00:00:00 2001 From: 1sjp Date: Sat, 16 May 2026 14:25:14 +0800 Subject: [PATCH] refactor(noname): thin command orchestration helpers --- ...60\347\212\266\350\257\204\344\274\260.md" | 12 +- ...55\350\267\257\347\272\277\345\233\276.md" | 11 +- ...73\345\212\241\345\215\241\347\211\207.md" | 20 + src-tauri/src/lib.rs | 1 + src-tauri/src/noname_apply.rs | 543 +++++++++++ src-tauri/src/noname_command_support.rs | 558 +++++++++++ src-tauri/src/noname_runtime.rs | 75 ++ src-tauri/src/tauri_commands.rs | 870 ++---------------- 8 files changed, 1278 insertions(+), 812 deletions(-) create mode 100644 src-tauri/src/noname_command_support.rs diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" index 701665c..86f1885 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/01-\347\216\260\347\212\266\350\257\204\344\274\260.md" @@ -33,7 +33,17 @@ - human review intent、人工复核结果写回与 second guardrail 预校验 / 决策记录已下沉到 `noname_apply.rs` - `apply_noname_reviewed_output` 的 request input 组装也已回收到 `noname_apply.rs` -因此 A2/A3 在当前主开发线可以视为阶段性完成。后续风险重点不再是 reviewed apply 本身,而是其他低风险 apply 与剧情生成主链是否继续膨胀。 +低风险 apply 编排也已完成第一轮下沉:NoName apply scope 计划、目标段匹配、重复提示跳过、章节摘要提示 / 选项偏置提示写入与 plot text hint 应用已进入 `noname_apply.rs`,并补充了模块内直接单测。 + +reviewed lifecycle 命令壳层也完成了第二个小切片:`NoNameRuntime` 已提供 `get_trace_by_id` 与事务式 `update_trace_by_id`,命令层不再重复手写 recent trace 查找与 replace 判断。 + +命令层纯支持逻辑也开始拆出:行动摘要与观察诊断拼接已进入 `noname_command_support.rs`,避免继续把展示 / 诊断类 helper 混在 Tauri 命令文件里。 + +Director observe turn 的 context input、guardrail input 与 runtime 调用编排也已进入 `noname_command_support.rs`,命令层只保留 entity/memory/runtime 锁获取和调用。 + +NoName turn 输出收束也已进入 `noname_command_support.rs`,pending plot augmentation trace 标记、low-risk 输出应用与观察诊断拼接不再散落在命令主链里。 + +因此 A2/A3 在当前主开发线可以视为阶段性完成,C4 已完成第一轮低风险 apply 减压、一层 runtime trace helper 收口、一层 command support helper 拆分、Director observe turn 编排下沉,以及 NoName turn 输出收束下沉。后续风险重点不再是 reviewed apply 本身,而是剧情生成主链中仍与 runtime / engine 绑定过紧的编排是否继续膨胀。 ### 3.2 前后端 mock 语义漂移 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" index e32c595..8601ee0 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/02-\345\220\216\347\273\255\350\267\257\347\272\277\345\233\276.md" @@ -56,17 +56,24 @@ - 重点: - reviewed apply request 构造 - review / second guardrail / manual apply 共用逻辑 + - low-risk apply 计划与执行 helper - 尽量让命令层只做参数接收与结果返回 当前状态: -- 已完成第五轮下沉 +- 已完成第五轮 reviewed apply 下沉,以及第一轮 low-risk apply 下沉 - reviewed apply 的 request builder 与 plotText manual apply helper 已进入 `noname_apply.rs` - `tauri_commands.rs` 在 reviewed apply 路径上已减少内联快照构造与包装逻辑 - reviewed apply 命令已开始复用共享的 trace 读取、plot state 更新和 trace 回写壳层 - human review intent、人工复核结果写回与 second guardrail 预校验 / 决策记录已进入 `noname_apply.rs` - `apply_noname_reviewed_output` 的 request input 组装已回收到 `noname_apply.rs` -- 本轮后 A2/A3 可视为阶段性完成;下一步若继续减压,应优先评估低风险 apply 或剧情生成主链中仍重复的编排,而不是扩大新的 apply scope +- low-risk apply 的计划排序、scope/target_segment 判断、plot text hint、章节摘要提示和选项偏置提示已进入 `noname_apply.rs` +- 本轮已补充 `noname_apply` 直接单测,不只依赖 `tauri_commands` 集成测试 +- `NoNameRuntime` 已补充 `get_trace_by_id` 与事务式 `update_trace_by_id`,review / second guardrail 命令不再重复手写 trace 查找和 replace 判断 +- `noname_command_support.rs` 已承接行动摘要与观察诊断拼接 helper,命令层不再内联这类纯支持逻辑 +- `noname_command_support.rs` 已承接 Director observe turn 的 context input、guardrail input 与 runtime 调用编排,命令层只保留 entity/memory/runtime 锁获取和调用 +- `noname_command_support.rs` 已承接 NoName turn 输出收束,统一处理 pending plot augmentation trace 标记、low-risk 输出应用与观察诊断拼接 +- 本轮后 A2/A3 可视为阶段性完成,C4 已完成第一轮低风险 apply 减压、一层 runtime trace helper 收口、一层 command support helper 拆分、Director observe turn 编排下沉与 NoName turn 输出收束下沉;下一步若继续减压,应优先评估剧情生成主链中仍重复的 runtime / engine 编排,而不是扩大新的 apply scope ## P2:收口后继续推进 diff --git "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" index 448d03c..aba7716 100644 --- "a/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" +++ "b/docs/\351\241\271\347\233\256\346\226\207\346\241\243/03-\350\247\204\345\210\222/06-\350\277\221\346\234\237\345\274\200\345\217\221\344\273\273\345\212\241\345\215\241\347\211\207.md" @@ -154,6 +154,26 @@ - 提高 reviewed apply 逻辑的可测试性和可维护性 - 为后续高风险接口评估保留清晰的受控边界 +当前状态: + +- 第一轮低风险 apply 下沉已完成 +- `noname_apply.rs` 现已承接 NoName apply scope 计划、目标段匹配、重复提示跳过、低风险摘要提示 / 选项偏置提示写入,以及 plot text hint 应用 +- `tauri_commands.rs` 已移除对应的内联 helper,只保留生成主链中的计划记录、runtime trace 回写和 engine orchestration +- 已补充 `noname_apply` 模块内直接单测,覆盖计划排序 / forbidden scope skip、正文提示应用、低风险摘要与诊断写入 +- 第二个小切片已完成:`NoNameRuntime` 新增 `get_trace_by_id` 与事务式 `update_trace_by_id`,`mark_noname_controlled_output_review` / `resolve_noname_second_guardrail` 不再手写 recent trace 查找与 replace 判断 +- 第三个小切片已完成:`noname_command_support.rs` 承接命令层行动摘要与观察诊断拼接 helper,`tauri_commands.rs` 不再内联这类纯支持逻辑 +- 第四个小切片已完成:`noname_command_support.rs` 承接 Director observe turn 的 context input、guardrail input 与 runtime 调用编排,命令层只保留 entity/memory/runtime 锁获取和调用 +- 第五个小切片已完成:`noname_command_support.rs` 承接 NoName turn 输出收束,统一处理 pending plot augmentation trace 标记、low-risk 输出应用与观察诊断拼接 +- 本轮没有新增 apply scope,也没有扩张 second guardrail;下一轮若继续 C4,应优先评估生成主链中仍与 runtime/engine 绑定过紧的编排边界 + +已验证: + +- `cargo test --manifest-path E:\Nobody\_c4_command_thinning\src-tauri\Cargo.toml noname_command_support -- --nocapture`,6 pass +- `cargo test --manifest-path E:\Nobody\_c4_command_thinning\src-tauri\Cargo.toml noname_apply -- --nocapture` +- `cargo test --manifest-path E:\Nobody\_c4_command_thinning\src-tauri\Cargo.toml noname_runtime -- --nocapture` +- `cargo test --manifest-path E:\Nobody\_c4_command_thinning\src-tauri\Cargo.toml noname_ -- --nocapture`,171 pass +- `git -C E:\Nobody\_c4_command_thinning diff --check` + ### 卡片 C5:建立 `webRuntime` 与后端字段漂移的守卫式验证 - 优先级: diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index a87bafa..ccdf104 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -17,6 +17,7 @@ pub mod noname_agent_registry; pub mod noname_apply; pub mod noname_capability_base; pub mod noname_capability_registry; +pub mod noname_command_support; pub mod noname_config; pub mod noname_context_builder; pub mod noname_context_types; diff --git a/src-tauri/src/noname_apply.rs b/src-tauri/src/noname_apply.rs index 35d85ce..c5396fa 100644 --- a/src-tauri/src/noname_apply.rs +++ b/src-tauri/src/noname_apply.rs @@ -59,6 +59,20 @@ pub struct NoNameReviewedApplyOutcome { pub plot_state: PlotState, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NoNameApplyTargetDecision { + Apply, + Skip { outcome: &'static str, note: String }, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NoNameApplyTargetPlan { + pub target: &'static str, + pub priority: u32, + pub order: u32, + pub decision: NoNameApplyTargetDecision, +} + pub fn build_manual_plot_text_apply_request( request_id: impl Into, chapter_index: u32, @@ -162,6 +176,409 @@ fn priority_for_apply_scope(scope: NoNameApplyScope) -> u32 { } } +fn build_noname_summary_hint(proposal: &NoNameProposal) -> String { + format!("NoName提示:后续重点关注{}", proposal.focus.trim()) +} + +fn build_noname_option_bias_hint(proposal: &NoNameProposal) -> String { + format!( + "NoName选项偏置:下轮优先围绕{}提供行动切入点", + proposal.focus.trim() + ) +} + +fn build_noname_plot_text_hint(proposal: &NoNameProposal) -> String { + format!("【NoName】重点关注:{}", proposal.focus.trim()) +} + +fn proposal_allows_apply_scope(proposal: &NoNameProposal, scope: NoNameApplyScope) -> bool { + proposal.apply_scopes.is_empty() || proposal.apply_scopes.contains(&scope) +} + +pub fn plan_noname_apply_target( + proposal: &NoNameProposal, + scope: NoNameApplyScope, + plot_state: Option<&PlotState>, + plot_text: Option<&str>, +) -> NoNameApplyTargetPlan { + let target = scope.as_str(); + let priority = priority_for_apply_scope(scope); + if !proposal_allows_apply_scope(proposal, scope) { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_scope_forbidden", + note: format!("提案未声明 {} 作用域,跳过对应输出", target), + }, + }; + } + + if !target_segment_supports_apply_scope(proposal.target_segment, scope) { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_target_mismatch", + note: format!( + "目标段 {} 不支持 {},跳过对应输出", + proposal.target_segment.as_str(), + target + ), + }, + }; + } + + match scope { + NoNameApplyScope::PlotTextHint => { + let current_text = plot_text.unwrap_or_default(); + if current_text.trim().is_empty() { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_empty_plot_text", + note: "当前正文为空,跳过受控正文提示".to_string(), + }, + }; + } + if current_text.contains("【NoName】") || current_text.contains("NoName提示") { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_duplicate", + note: "正文已包含 NoName 标记,跳过重复提示".to_string(), + }, + }; + } + } + NoNameApplyScope::PlotAugmentationHint => { + let Some(state) = plot_state else { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_missing_plot_state", + note: "缺少 plot_state,跳过剧情增强提示".to_string(), + }, + }; + }; + if !state.is_waiting_for_input { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_not_waiting", + note: "当前不处于等待输入状态,跳过剧情增强提示".to_string(), + }, + }; + } + let hint = format!( + "NoName plot augmentation: focus={} | effect={}", + proposal.focus.trim(), + proposal.intended_effect.trim() + ); + if state + .pending_plot_augmentation_hints + .iter() + .any(|existing| existing == &hint) + { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_duplicate", + note: format!( + "剧情增强提示已包含“{}”,跳过重复写入", + proposal.focus.trim() + ), + }, + }; + } + } + NoNameApplyScope::ChapterSummaryHint => { + if let Some(state) = plot_state { + let summary = state.current_chapter.summary.trim(); + if !summary.is_empty() && summary.contains(proposal.focus.trim()) { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_duplicate", + note: format!( + "章节摘要已覆盖“{}”,跳过重复提示", + proposal.focus.trim() + ), + }, + }; + } + } + } + NoNameApplyScope::OptionBiasHint => { + let Some(state) = plot_state else { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_missing_plot_state", + note: "缺少 plot_state,跳过选项偏置提示".to_string(), + }, + }; + }; + if !state.is_waiting_for_input { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_not_waiting", + note: "当前不处于等待输入状态,跳过选项偏置提示".to_string(), + }, + }; + } + let option_hint = build_noname_option_bias_hint(proposal); + let diagnostics = state + .last_generation_diagnostics + .as_deref() + .unwrap_or_default(); + if diagnostics.contains(option_hint.as_str()) { + return NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Skip { + outcome: "skipped_duplicate", + note: format!( + "诊断中已包含选项偏置提示“{}”,跳过重复写入", + proposal.focus.trim() + ), + }, + }; + } + } + NoNameApplyScope::Diagnostics => {} + } + + NoNameApplyTargetPlan { + target, + priority, + order: 0, + decision: NoNameApplyTargetDecision::Apply, + } +} + +pub fn build_noname_apply_plan_set( + proposal: &NoNameProposal, + plot_state: Option<&PlotState>, + plot_text: Option<&str>, +) -> Vec { + let mut plans = vec![ + plan_noname_apply_target( + proposal, + NoNameApplyScope::PlotTextHint, + plot_state, + plot_text, + ), + plan_noname_apply_target( + proposal, + NoNameApplyScope::PlotAugmentationHint, + plot_state, + None, + ), + plan_noname_apply_target( + proposal, + NoNameApplyScope::ChapterSummaryHint, + plot_state, + None, + ), + plan_noname_apply_target(proposal, NoNameApplyScope::OptionBiasHint, plot_state, None), + ]; + plans.sort_by(|left, right| { + right + .priority + .cmp(&left.priority) + .then_with(|| left.target.cmp(right.target)) + }); + for (index, plan) in plans.iter_mut().enumerate() { + plan.order = (index + 1) as u32; + } + plans +} + +pub fn record_noname_apply_plan(trace: &mut NoNameTrace, plan: &NoNameApplyTargetPlan) { + let (decision, note) = match &plan.decision { + NoNameApplyTargetDecision::Apply => ( + "apply", + Some(format!( + "允许执行 {},优先级 {},顺位 #{}", + plan.target, plan.priority, plan.order + )), + ), + NoNameApplyTargetDecision::Skip { note, .. } => ("skip", Some(note.clone())), + }; + trace.record_apply_plan(plan.order, plan.target, decision, plan.priority, note); +} + +fn record_noname_apply_plan_and_skip_execution( + trace: &mut NoNameTrace, + plan: &NoNameApplyTargetPlan, +) { + record_noname_apply_plan(trace, plan); + if let NoNameApplyTargetDecision::Skip { outcome, note } = &plan.decision { + trace.record_apply_execution(plan.target, *outcome, Some(note.clone())); + } +} + +pub fn apply_noname_plot_text_hint( + plot_text: &mut String, + trace: &mut NoNameTrace, + proposal: &NoNameProposal, +) -> bool { + if proposal.status != NoNameProposalStatus::Applied { + return false; + } + + let plan = plan_noname_apply_target( + proposal, + NoNameApplyScope::PlotTextHint, + None, + Some(plot_text.as_str()), + ); + if !matches!(plan.decision, NoNameApplyTargetDecision::Apply) { + if let NoNameApplyTargetDecision::Skip { outcome, note } = &plan.decision { + trace.record_apply_execution(plan.target, *outcome, Some(note.clone())); + } + return false; + } + + let hint = build_noname_plot_text_hint(proposal); + match proposal.target_segment { + NoNameTargetSegment::CurrentTurnHead => { + let original = plot_text.trim().to_string(); + *plot_text = format!("{}\n\n{}", hint, original); + } + _ => { + plot_text.push_str("\n\n"); + plot_text.push_str(&hint); + } + } + trace.record_proposal_transition(format!("{}:applied:plot_text_hint", proposal.proposal_id)); + trace.record_apply_execution( + "plot_text_hint", + "applied", + Some(format!("已将提案提示插入正文,聚焦“{}”", proposal.focus)), + ); + true +} + +pub fn apply_noname_low_risk_outputs( + plot_state: &mut PlotState, + trace: &mut NoNameTrace, + proposal: &NoNameProposal, + plot_text_applied: bool, +) { + if proposal.status != NoNameProposalStatus::Applied { + return; + } + + let mut applied_targets: Vec<&str> = Vec::new(); + + let summary_plan = plan_noname_apply_target( + proposal, + NoNameApplyScope::ChapterSummaryHint, + Some(plot_state), + None, + ); + if matches!(summary_plan.decision, NoNameApplyTargetDecision::Apply) { + let hint = build_noname_summary_hint(proposal); + let summary = plot_state.current_chapter.summary.trim(); + if summary.is_empty() { + plot_state.current_chapter.summary = hint.clone(); + } else { + plot_state.current_chapter.summary = match proposal.target_segment { + NoNameTargetSegment::ChapterSummaryHead => format!("{};{}", hint, summary), + _ => format!("{};{}", summary, hint), + }; + } + trace.record_proposal_transition(format!( + "{}:applied:chapter_summary_hint", + proposal.proposal_id + )); + trace.record_apply_execution( + "chapter_summary_hint", + "applied", + Some(format!("已写入章节摘要提示,聚焦“{}”", proposal.focus)), + ); + applied_targets.push("chapter_summary_hint"); + } else { + record_noname_apply_plan_and_skip_execution(trace, &summary_plan); + } + + let option_bias_plan = plan_noname_apply_target( + proposal, + NoNameApplyScope::OptionBiasHint, + Some(plot_state), + None, + ); + if matches!(option_bias_plan.decision, NoNameApplyTargetDecision::Apply) { + let option_hint = build_noname_option_bias_hint(proposal); + let diagnostics = plot_state + .last_generation_diagnostics + .clone() + .unwrap_or_default(); + if diagnostics.is_empty() { + plot_state.last_generation_diagnostics = Some(option_hint.clone()); + } else if !diagnostics.contains(option_hint.as_str()) { + plot_state.last_generation_diagnostics = + Some(format!("{};{}", diagnostics, option_hint)); + } + trace.record_proposal_transition(format!( + "{}:applied:option_bias_hint", + proposal.proposal_id + )); + trace.record_apply_execution( + "option_bias_hint", + "applied", + Some(format!("已写入下轮选项偏置提示,聚焦“{}”", proposal.focus)), + ); + applied_targets.push("option_bias_hint"); + } else { + record_noname_apply_plan_and_skip_execution(trace, &option_bias_plan); + } + + if plot_text_applied { + applied_targets.push("plot_text_hint"); + } + let apply_outcome = if applied_targets.is_empty() { + "applied_no_scoped_output" + } else { + "applied_scoped_outputs" + }; + trace.set_apply_result( + true, + apply_outcome, + Some(format!( + "已应用作用域:{};聚焦“{}”", + if applied_targets.is_empty() { + "无".to_string() + } else { + applied_targets.join(", ") + }, + proposal.focus + )), + ); +} + fn next_apply_plan_order(trace: &NoNameTrace) -> u32 { trace .apply_plan_log @@ -1199,6 +1616,132 @@ mod tests { (trace, request_id) } + fn make_low_risk_proposal() -> NoNameProposal { + NoNameProposal { + proposal_id: "proposal-low-risk".to_string(), + kind: NoNameProposalKind::PlotCandidate, + producer_role: NoNameRole::Director, + title: "low risk hint".to_string(), + summary: "suggest low risk hints".to_string(), + focus: "sect crisis".to_string(), + target_segment: NoNameTargetSegment::CurrentTurnTail, + intended_effect: "guide next turn without mutating final plot state".to_string(), + rationale: "keep safe outputs scoped".to_string(), + suggested_action: Some("low risk apply".to_string()), + labels: vec!["test".to_string()], + apply_scopes: vec![ + NoNameApplyScope::PlotTextHint, + NoNameApplyScope::ChapterSummaryHint, + NoNameApplyScope::OptionBiasHint, + ], + status: NoNameProposalStatus::Applied, + applyable: true, + } + } + + #[test] + fn low_risk_apply_plan_set_orders_and_skips_forbidden_scopes() { + let proposal = make_low_risk_proposal(); + let plot_state = make_plot_state(""); + let plans = build_noname_apply_plan_set( + &proposal, + Some(&plot_state), + Some("current turn narrative"), + ); + + assert_eq!( + plans.iter().map(|plan| plan.target).collect::>(), + vec![ + "plot_text_hint", + "plot_augmentation_hint", + "chapter_summary_hint", + "option_bias_hint" + ] + ); + assert_eq!( + plans.iter().map(|plan| plan.order).collect::>(), + vec![1, 2, 3, 4] + ); + assert!(matches!( + plans[0].decision, + NoNameApplyTargetDecision::Apply + )); + assert!(matches!( + &plans[1].decision, + NoNameApplyTargetDecision::Skip { outcome, .. } + if *outcome == "skipped_scope_forbidden" + )); + } + + #[test] + fn apply_noname_plot_text_hint_updates_text_and_trace() { + let proposal = make_low_risk_proposal(); + let mut trace = NoNameTrace::empty( + "trace-low-risk", + "session-1", + "turn-1", + NoNameMode::Assisted, + ); + let mut plot_text = "The mountain gate falls quiet.".to_string(); + + let applied = apply_noname_plot_text_hint(&mut plot_text, &mut trace, &proposal); + + assert!(applied); + assert!(plot_text.contains("【NoName】重点关注:sect crisis")); + assert!(trace + .proposal_transition_log + .iter() + .any(|entry| entry == "proposal-low-risk:applied:plot_text_hint")); + assert!(trace + .apply_execution_log + .iter() + .any(|entry| entry.target == "plot_text_hint" && entry.outcome == "applied")); + } + + #[test] + fn apply_noname_low_risk_outputs_updates_summary_diagnostics_and_trace() { + let proposal = make_low_risk_proposal(); + let mut trace = NoNameTrace::empty( + "trace-low-risk", + "session-1", + "turn-1", + NoNameMode::Assisted, + ); + let mut plot_state = make_plot_state(""); + + apply_noname_low_risk_outputs(&mut plot_state, &mut trace, &proposal, true); + + assert!(plot_state + .current_chapter + .summary + .contains("NoName提示:后续重点关注sect crisis")); + assert!(plot_state + .last_generation_diagnostics + .as_deref() + .unwrap_or_default() + .contains("NoName选项偏置:下轮优先围绕sect crisis提供行动切入点")); + assert!(trace + .apply_execution_log + .iter() + .any(|entry| { entry.target == "chapter_summary_hint" && entry.outcome == "applied" })); + assert!(trace + .apply_execution_log + .iter() + .any(|entry| { entry.target == "option_bias_hint" && entry.outcome == "applied" })); + assert_eq!( + trace + .apply_result + .as_ref() + .map(|entry| entry.outcome.as_str()), + Some("applied_scoped_outputs") + ); + assert!(trace + .apply_result + .as_ref() + .and_then(|entry| entry.reason.as_deref()) + .is_some_and(|note| note.contains("plot_text_hint"))); + } + #[test] fn human_review_apply_intent_records_ready_for_plot_text() { let (mut trace, request_id) = make_trace_for_plot_text_review(); diff --git a/src-tauri/src/noname_command_support.rs b/src-tauri/src/noname_command_support.rs new file mode 100644 index 0000000..6283b74 --- /dev/null +++ b/src-tauri/src/noname_command_support.rs @@ -0,0 +1,558 @@ +use crate::entity_store::EntityStore; +use crate::game_state::GameState; +use crate::memory_layers::MemoryLayers; +use crate::noname_apply::apply_noname_low_risk_outputs; +use crate::noname_context_builder::build_context_packet; +use crate::noname_context_types::NoNameContextBuildInput; +use crate::noname_guardrails::{NoNameDirectorGuardrailInput, NoNameGuardrailResult}; +use crate::noname_memory_manager::NoNameMemoryManager; +use crate::noname_memory_types::NoNameWorkingMemoryItem; +use crate::noname_roles::NoNameDirectorObservation; +use crate::noname_runtime::{NoNameDirectorRunResult, NoNameRuntime, NoNameTurnInput}; +use crate::noname_trace::NoNameTrace; +use crate::noname_types::{NoNameApplyScope, NoNameMode, NoNameRole}; +use crate::numerical_system::ActionResult; +use crate::plot_engine::{PlayerAction, PlotState}; + +pub fn noname_mode_label(mode: NoNameMode) -> &'static str { + match mode { + NoNameMode::Disabled => "disabled", + NoNameMode::ObserveOnly => "observe_only", + NoNameMode::Assisted => "assisted", + } +} + +pub fn build_noname_action_summary(action: &PlayerAction, plot_state: &PlotState) -> String { + if let Some(selected_option_id) = action.selected_option_id { + if let Some(option) = plot_state + .current_scene + .available_options + .get(selected_option_id) + { + return format!("选择选项: {}", option.description); + } + } + + if !action.content.trim().is_empty() { + return format!("自由输入: {}", action.content.trim()); + } + + "玩家执行了默认推进动作".to_string() +} + +pub fn append_noname_observation_diagnostics( + existing: &mut Option, + mode: NoNameMode, + observation: &NoNameDirectorObservation, + guardrail_result: Option<&NoNameGuardrailResult>, + trace: &NoNameTrace, +) { + let mut segments = vec![ + format!("focus={}", observation.focus), + format!("rationale={}", observation.rationale), + format!("proposal={}", observation.proposal.title), + format!( + "target_segment={}", + observation.proposal.target_segment.as_str() + ), + format!("intended_effect={}", observation.proposal.intended_effect), + format!("proposal_status={}", observation.proposal.status.as_str()), + format!( + "apply_scopes={}", + if observation.proposal.apply_scopes.is_empty() { + "none".to_string() + } else { + observation + .proposal + .apply_scopes + .iter() + .map(|scope| scope.as_str()) + .collect::>() + .join(",") + } + ), + format!( + "applyable={}", + if observation.proposal.applyable { + "yes" + } else { + "no" + } + ), + ]; + if let Some(result) = guardrail_result { + segments.push(format!("guardrail={}", result.outcome.as_str())); + if let Some(reason) = &result.reason { + segments.push(format!("reason={}", reason)); + } + } + if let Some(apply_result) = &trace.apply_result { + segments.push(format!("apply={}", apply_result.outcome)); + if let Some(reason) = &apply_result.reason { + segments.push(format!("apply_reason={}", reason)); + } + } + let note = format!( + "NoName.{}:{}", + noname_mode_label(mode), + segments.join(";") + ); + match existing { + Some(diag) => { + diag.push(';'); + diag.push_str(¬e); + } + None => { + *existing = Some(note); + } + } +} + +pub fn build_noname_director_context_input( + action_summary: &str, + game_state: &GameState, + plot_state: &PlotState, +) -> NoNameContextBuildInput { + NoNameContextBuildInput { + role: NoNameRole::Director, + world_id: game_state.script.id.clone(), + run_id: "active-run".to_string(), + scene_id: plot_state.current_scene.id.clone(), + character_ids: vec![game_state.player.id.clone()], + map_node_id: Some(game_state.player.location.clone()), + player_intent: if action_summary.trim().is_empty() { + None + } else { + Some(action_summary.to_string()) + }, + recent_context_lines: plot_state + .current_chapter + .content + .iter() + .rev() + .take(6) + .cloned() + .collect::>() + .into_iter() + .rev() + .collect(), + token_budget: 320, + per_section_limit: 4, + } +} + +pub fn build_noname_director_guardrail_input( + action_summary: &str, + game_state: &GameState, + plot_state: &PlotState, +) -> NoNameDirectorGuardrailInput { + NoNameDirectorGuardrailInput { + plot_state: plot_state.clone(), + action_result: plot_state + .last_action_result + .clone() + .unwrap_or(ActionResult { + success: true, + description: action_summary.to_string(), + stat_changes: Vec::new(), + events: Vec::new(), + }), + player_name: game_state.player.name.clone(), + player_realm_level: game_state.player.stats.cultivation_realm.level, + player_combat_power: game_state.player.stats.combat_power, + } +} + +pub fn run_noname_director_observe_turn( + runtime: &mut NoNameRuntime, + entity_store: &EntityStore, + memory_layers: Option<&MemoryLayers>, + action_summary: &str, + game_state: &GameState, + plot_state: &PlotState, + timestamp: u64, +) -> Result { + let mut memory_manager = NoNameMemoryManager::new(); + if let Some(memory) = memory_layers { + memory_manager.ingest_legacy_layers(memory); + } + memory_manager.push_working_memory( + NoNameWorkingMemoryItem { + memory_id: format!("work-{}", timestamp), + turn_id: format!("turn-{}", timestamp), + source: "execute_player_action".to_string(), + category: "recent_turn".to_string(), + summary: action_summary.to_string(), + expires_at: None, + priority: 10, + }, + 8, + ); + + let context_input = build_noname_director_context_input(action_summary, game_state, plot_state); + let context_packet = build_context_packet(entity_store, &memory_manager, &context_input); + let guardrail_input = + build_noname_director_guardrail_input(action_summary, game_state, plot_state); + + runtime + .run_director_observe_turn( + NoNameTurnInput { + trace_id: format!("noname-trace-{}", timestamp), + session_id: "active-run".to_string(), + turn_id: format!("turn-{}", timestamp), + caller_role: NoNameRole::Director, + }, + action_summary, + &context_packet, + Some(&guardrail_input), + ) + .map_err(|e| e.to_string()) +} + +pub fn apply_noname_turn_outputs( + plot_state: &mut PlotState, + noname_result: &mut NoNameDirectorRunResult, + plot_text_applied: bool, + pending_plot_augmentation_trace: Option<(String, String)>, +) { + if let Some((outcome, note)) = pending_plot_augmentation_trace { + noname_result.trace.record_apply_execution( + NoNameApplyScope::PlotAugmentationHint.as_str(), + outcome.as_str(), + Some(note), + ); + noname_result.trace.record_proposal_transition(format!( + "{}:pending_plot_augmentation:{}", + noname_result.proposal.proposal_id, outcome + )); + } + apply_noname_low_risk_outputs( + plot_state, + &mut noname_result.trace, + &noname_result.proposal, + plot_text_applied, + ); + append_noname_observation_diagnostics( + &mut plot_state.last_generation_diagnostics, + noname_result.trace.mode, + &noname_result.observation, + noname_result.guardrail_result.as_ref(), + &noname_result.trace, + ); +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::game_engine::GameEngine; + use crate::models::{CultivationRealm, Element, Grade, SpiritualRoot}; + use crate::noname_guardrails::NoNameGuardrailResult; + use crate::noname_roles::NoNameDirectorObservation; + use crate::noname_trace::{NoNameApplyTraceResult, NoNameTrace}; + use crate::noname_types::{ + NoNameApplyScope, NoNameMode, NoNameProposal, NoNameProposalKind, NoNameProposalStatus, + NoNameRole, NoNameTargetSegment, + }; + use crate::numerical_system::Action; + use crate::plot_engine::{ + ActionType, ChapterState, PlayerAction, PlayerOption, PlotInteractionState, PlotSettings, + PlotState, Scene, + }; + use crate::script::{InitialState, Location, Script, ScriptType, WorldSetting}; + + fn make_option(id: usize, description: &str) -> PlayerOption { + PlayerOption { + id, + description: description.to_string(), + requirements: Vec::new(), + action: Action::Custom { + description: description.to_string(), + }, + } + } + + fn make_plot_state() -> PlotState { + let mut state = PlotState { + current_scene: Scene { + id: "scene-1".to_string(), + name: "山门".to_string(), + description: "山门风声渐紧".to_string(), + location: "sect".to_string(), + available_options: vec![make_option(0, "返回山门广场")], + }, + plot_history: vec![], + is_waiting_for_input: true, + interaction_state: PlotInteractionState::WaitingForChoice, + last_action_result: None, + settings: PlotSettings::default(), + current_chapter: ChapterState::new(1, "第一章".to_string()), + chapters: vec![], + segment_count: 0, + last_generation_diagnostics: None, + last_option_generation_source: None, + last_consistency_risk_score: None, + pending_plot_augmentation_hints: Vec::new(), + }; + state.current_chapter.content = vec![ + "第一段".to_string(), + "第二段".to_string(), + "第三段".to_string(), + ]; + state + } + + fn make_game_state() -> GameState { + let mut world_setting = WorldSetting::new(); + world_setting.cultivation_realms = vec![ + CultivationRealm::new("Qi Condensation".to_string(), 1, 0, 1.0), + CultivationRealm::new("Foundation Establishment".to_string(), 2, 0, 2.0), + ]; + world_setting.locations = vec![Location { + id: "sect".to_string(), + name: "Azure Cloud Sect".to_string(), + description: "A peaceful cultivation sect".to_string(), + spiritual_energy: 1.0, + }]; + let initial_state = InitialState { + player_name: "Test Player".to_string(), + player_spiritual_root: SpiritualRoot { + element: Element::Fire, + grade: Grade::Heavenly, + affinity: 0.9, + elements: Vec::new(), + }, + starting_location: "sect".to_string(), + starting_age: 16, + }; + let script = Script::new( + "world-1".to_string(), + "测试世界".to_string(), + ScriptType::Custom, + world_setting, + initial_state, + ); + GameEngine::new() + .initialize_game(script) + .expect("test game should initialize") + } + + fn make_observation() -> NoNameDirectorObservation { + NoNameDirectorObservation { + role: NoNameRole::Director, + action_summary: "选择返回山门".to_string(), + focus: "山门危机".to_string(), + rationale: "应优先观察山门危机".to_string(), + prompt_preview: "prompt".to_string(), + role_goal: None, + scene_focus: None, + forbidden_scopes: Vec::new(), + note_type_hits: Vec::new(), + source_stats: Vec::new(), + context_token_budget_used: None, + context_slice_stats: Vec::new(), + proposal: NoNameProposal { + proposal_id: "proposal-1".to_string(), + kind: NoNameProposalKind::PlotCandidate, + producer_role: NoNameRole::Director, + title: "Director提案:山门危机".to_string(), + summary: "建议优先观察山门危机".to_string(), + focus: "山门危机".to_string(), + target_segment: NoNameTargetSegment::CurrentTurnTail, + intended_effect: "维持低风险观察导向".to_string(), + rationale: "应优先观察山门危机".to_string(), + suggested_action: Some("保持 observe-only".to_string()), + labels: vec!["director".to_string()], + apply_scopes: vec![NoNameApplyScope::Diagnostics], + status: NoNameProposalStatus::Observed, + applyable: false, + }, + } + } + + fn make_director_run_result() -> NoNameDirectorRunResult { + let observation = make_observation(); + let mut proposal = observation.proposal.clone(); + proposal.status = NoNameProposalStatus::Applied; + proposal.applyable = true; + proposal.apply_scopes = vec![ + NoNameApplyScope::ChapterSummaryHint, + NoNameApplyScope::OptionBiasHint, + ]; + NoNameDirectorRunResult { + trace: NoNameTrace::empty("trace-outputs", "session-1", "turn-1", NoNameMode::Assisted), + observation: NoNameDirectorObservation { + proposal: proposal.clone(), + ..observation + }, + proposal, + guardrail_result: Some(NoNameGuardrailResult::accept()), + related_observations: Vec::new(), + } + } + + #[test] + fn action_summary_prefers_selected_option_text() { + let action = PlayerAction { + action_type: ActionType::SelectedOption, + content: String::new(), + selected_option_id: Some(0), + meta: None, + }; + + let summary = build_noname_action_summary(&action, &make_plot_state()); + + assert_eq!(summary, "选择选项: 返回山门广场"); + } + + #[test] + fn action_summary_uses_free_text_when_no_option_matches() { + let action = PlayerAction { + action_type: ActionType::FreeText, + content: " 观察灵气流动 ".to_string(), + selected_option_id: None, + meta: None, + }; + + let summary = build_noname_action_summary(&action, &make_plot_state()); + + assert_eq!(summary, "自由输入: 观察灵气流动"); + } + + #[test] + fn diagnostics_append_mode_guardrail_apply_and_proposal_details() { + let mut diagnostics = Some("原始诊断".to_string()); + let guardrail = NoNameGuardrailResult::accept(); + let mut trace = + NoNameTrace::empty("trace-1", "session-1", "turn-1", NoNameMode::ObserveOnly); + trace.apply_result = Some(NoNameApplyTraceResult { + attempted: false, + outcome: "skipped_observe_only".to_string(), + reason: Some("当前模式仅观察,不尝试 assisted apply".to_string()), + }); + + append_noname_observation_diagnostics( + &mut diagnostics, + NoNameMode::ObserveOnly, + &make_observation(), + Some(&guardrail), + &trace, + ); + + let text = diagnostics.expect("diagnostics should exist"); + assert!(text.contains("原始诊断")); + assert!(text.contains("NoName.observe_only")); + assert!(text.contains("山门危机")); + assert!(text.contains("Director提案")); + assert!(text.contains("proposal_status=observed")); + assert!(text.contains("applyable=no")); + assert!(text.contains("guardrail=accept")); + assert!(text.contains("apply=skipped_observe_only")); + } + + #[test] + fn director_context_input_keeps_recent_lines_and_player_identity() { + let action_summary = "选择选项: 返回山门广场"; + let game_state = make_game_state(); + let plot_state = make_plot_state(); + + let input = build_noname_director_context_input(action_summary, &game_state, &plot_state); + + assert_eq!(input.role, NoNameRole::Director); + assert_eq!(input.world_id, "world-1"); + assert_eq!(input.run_id, "active-run"); + assert_eq!(input.scene_id, "scene-1"); + assert_eq!(input.character_ids, vec![game_state.player.id]); + assert_eq!(input.map_node_id, Some(game_state.player.location)); + assert_eq!(input.player_intent.as_deref(), Some(action_summary)); + assert_eq!( + input.recent_context_lines, + vec![ + "第一段".to_string(), + "第二段".to_string(), + "第三段".to_string() + ] + ); + assert_eq!(input.token_budget, 320); + assert_eq!(input.per_section_limit, 4); + } + + #[test] + fn director_guardrail_input_uses_last_action_or_safe_fallback() { + let game_state = make_game_state(); + let plot_state = make_plot_state(); + + let fallback = + build_noname_director_guardrail_input("自由输入: 观察灵气", &game_state, &plot_state); + + assert_eq!(fallback.player_name, game_state.player.name); + assert_eq!( + fallback.player_realm_level, + game_state.player.stats.cultivation_realm.level + ); + assert_eq!( + fallback.player_combat_power, + game_state.player.stats.combat_power + ); + assert_eq!(fallback.action_result.description, "自由输入: 观察灵气"); + assert!(fallback.action_result.success); + + let mut plot_state_with_result = plot_state.clone(); + plot_state_with_result.last_action_result = Some(ActionResult { + success: false, + description: "山门阵法拒绝回应".to_string(), + stat_changes: Vec::new(), + events: Vec::new(), + }); + + let from_result = build_noname_director_guardrail_input( + "自由输入: 观察灵气", + &game_state, + &plot_state_with_result, + ); + + assert_eq!(from_result.action_result.description, "山门阵法拒绝回应"); + assert!(!from_result.action_result.success); + } + + #[test] + fn turn_outputs_apply_low_risk_outputs_and_pending_trace_marker() { + let mut plot_state = make_plot_state(); + let mut result = make_director_run_result(); + + apply_noname_turn_outputs( + &mut plot_state, + &mut result, + false, + Some(( + "pending_plot_augmentation_consumed".to_string(), + "NoName pending plot augmentation consumed=1".to_string(), + )), + ); + + assert!(plot_state + .current_chapter + .summary + .contains("NoName提示:后续重点关注山门危机")); + let diagnostics = plot_state + .last_generation_diagnostics + .as_deref() + .unwrap_or_default(); + assert!(diagnostics.contains("NoName选项偏置")); + assert!(diagnostics.contains("NoName.assisted")); + assert!(result.trace.apply_execution_log.iter().any(|entry| { + entry.target == "plot_augmentation_hint" + && entry.outcome == "pending_plot_augmentation_consumed" + })); + assert!(result.trace.proposal_transition_log.iter().any(|entry| { + entry == "proposal-1:pending_plot_augmentation:pending_plot_augmentation_consumed" + })); + assert_eq!( + result + .trace + .apply_result + .as_ref() + .map(|entry| entry.outcome.as_str()), + Some("applied_scoped_outputs") + ); + } +} diff --git a/src-tauri/src/noname_runtime.rs b/src-tauri/src/noname_runtime.rs index a36dd79..a7b36d6 100644 --- a/src-tauri/src/noname_runtime.rs +++ b/src-tauri/src/noname_runtime.rs @@ -194,6 +194,14 @@ impl NoNameRuntime { self.recent_traces.iter().cloned().collect() } + pub fn get_trace_by_id(&self, trace_id: &str) -> Option { + self.recent_traces + .iter() + .rev() + .find(|trace| trace.trace_id == trace_id) + .cloned() + } + pub fn replace_trace(&mut self, trace: NoNameTrace) -> bool { if let Some(existing) = self .recent_traces @@ -207,6 +215,26 @@ impl NoNameRuntime { false } + pub fn update_trace_by_id( + &mut self, + trace_id: &str, + update: F, + ) -> Result + where + F: FnOnce(&mut NoNameTrace) -> Result<(), String>, + { + let trace = self + .recent_traces + .iter_mut() + .rev() + .find(|trace| trace.trace_id == trace_id) + .ok_or_else(|| format!("NoName trace not found: {}", trace_id))?; + let mut updated = trace.clone(); + update(&mut updated)?; + *trace = updated.clone(); + Ok(updated) + } + pub fn clear_traces(&mut self) { self.recent_traces.clear(); self.protocol_runtime.clear(); @@ -752,6 +780,53 @@ mod tests { assert_eq!(runtime.get_recent_traces()[0].elapsed_ms, 77); } + #[test] + fn update_trace_by_id_mutates_existing_trace() { + let mut runtime = NoNameRuntime::new(NoNameConfig::observe_only()); + runtime + .run_turn(NoNameTurnInput::default()) + .expect("trace should be stored"); + + let updated = runtime + .update_trace_by_id("noname-trace-0", |trace| { + trace.elapsed_ms = 88; + Ok(()) + }) + .expect("trace update should succeed"); + + assert_eq!(updated.elapsed_ms, 88); + assert_eq!(runtime.get_recent_traces()[0].elapsed_ms, 88); + } + + #[test] + fn update_trace_by_id_reports_missing_trace() { + let mut runtime = NoNameRuntime::new(NoNameConfig::observe_only()); + + let error = runtime + .update_trace_by_id("missing-trace", |_| Ok(())) + .expect_err("missing trace should fail"); + + assert_eq!(error, "NoName trace not found: missing-trace"); + } + + #[test] + fn update_trace_by_id_keeps_failed_update_mutation_local_to_closure() { + let mut runtime = NoNameRuntime::new(NoNameConfig::observe_only()); + runtime + .run_turn(NoNameTurnInput::default()) + .expect("trace should be stored"); + + let error = runtime + .update_trace_by_id("noname-trace-0", |trace| { + trace.elapsed_ms = 99; + Err("review decision rejected".to_string()) + }) + .expect_err("failed update should bubble up"); + + assert_eq!(error, "review decision rejected"); + assert_eq!(runtime.get_recent_traces()[0].elapsed_ms, 0); + } + #[test] fn observe_only_runtime_can_run_director_observation() { use crate::noname_context_types::{NoNameContextPacket, NoNameContextSourceStat}; diff --git a/src-tauri/src/tauri_commands.rs b/src-tauri/src/tauri_commands.rs index ab78fa5..4768b0b 100644 --- a/src-tauri/src/tauri_commands.rs +++ b/src-tauri/src/tauri_commands.rs @@ -16,18 +16,17 @@ use crate::llm_service::{LLMConfig, LLMRequest, LLMService}; use crate::memory_layers::{ChapterSummary, MemoryEntry, WorldFact}; use crate::noname_apply::{ apply_human_review_decision_to_trace, apply_manual_plot_text_hint_to_plot_state, - apply_reviewed_output_input_to_plot_state, resolve_second_guardrail_for_trace, + apply_noname_plot_text_hint, apply_reviewed_output_input_to_plot_state, + build_noname_apply_plan_set, record_noname_apply_plan, resolve_second_guardrail_for_trace, NoNameReviewedApplyRequestInput, }; +use crate::noname_command_support::{ + apply_noname_turn_outputs, build_noname_action_summary, run_noname_director_observe_turn, +}; use crate::noname_config::NoNameConfig; -use crate::noname_context_builder::build_context_packet; -use crate::noname_context_types::NoNameContextBuildInput; -use crate::noname_guardrails::{NoNameDirectorGuardrailInput, NoNameGuardrailResult}; -use crate::noname_memory_manager::NoNameMemoryManager; -use crate::noname_roles::NoNameDirectorObservation; -use crate::noname_runtime::{NoNameDirectorRunResult, NoNameRuntime, NoNameTurnInput}; +use crate::noname_runtime::{NoNameDirectorRunResult, NoNameRuntime}; use crate::noname_trace::{NoNameHumanReviewDecision, NoNameSecondGuardrailDecision, NoNameTrace}; -use crate::noname_types::{NoNameApplyScope, NoNameMode, NoNameRole, NoNameTargetSegment}; +use crate::noname_types::{NoNameApplyScope, NoNameMode}; use crate::novel_generator::{Novel, NovelGenerator}; use crate::numerical_system::{Action, Context, StatChange}; use crate::plot_consistency::{ @@ -65,149 +64,6 @@ fn noname_runtime() -> &'static Mutex { NONAME_RUNTIME.get_or_init(|| Mutex::new(NoNameRuntime::new(NoNameConfig::observe_only()))) } -fn noname_mode_label(mode: NoNameMode) -> &'static str { - match mode { - NoNameMode::Disabled => "disabled", - NoNameMode::ObserveOnly => "observe_only", - NoNameMode::Assisted => "assisted", - } -} - -fn build_noname_action_summary(action: &PlayerAction, plot_state: &PlotState) -> String { - if let Some(selected_option_id) = action.selected_option_id { - if let Some(option) = plot_state - .current_scene - .available_options - .get(selected_option_id) - { - return format!("选择选项: {}", option.description); - } - } - - if !action.content.trim().is_empty() { - return format!("自由输入: {}", action.content.trim()); - } - - "玩家执行了默认推进动作".to_string() -} - -fn append_noname_observation_diagnostics( - existing: &mut Option, - mode: NoNameMode, - observation: &NoNameDirectorObservation, - guardrail_result: Option<&NoNameGuardrailResult>, - trace: &NoNameTrace, -) { - let mut segments = vec![ - format!("focus={}", observation.focus), - format!("rationale={}", observation.rationale), - format!("proposal={}", observation.proposal.title), - format!( - "target_segment={}", - observation.proposal.target_segment.as_str() - ), - format!("intended_effect={}", observation.proposal.intended_effect), - format!("proposal_status={}", observation.proposal.status.as_str()), - format!( - "apply_scopes={}", - if observation.proposal.apply_scopes.is_empty() { - "none".to_string() - } else { - observation - .proposal - .apply_scopes - .iter() - .map(|scope| scope.as_str()) - .collect::>() - .join(",") - } - ), - format!( - "applyable={}", - if observation.proposal.applyable { - "yes" - } else { - "no" - } - ), - ]; - if let Some(result) = guardrail_result { - segments.push(format!("guardrail={}", result.outcome.as_str())); - if let Some(reason) = &result.reason { - segments.push(format!("reason={}", reason)); - } - } - if let Some(apply_result) = &trace.apply_result { - segments.push(format!("apply={}", apply_result.outcome)); - if let Some(reason) = &apply_result.reason { - segments.push(format!("apply_reason={}", reason)); - } - } - let note = format!( - "NoName.{}:{}", - noname_mode_label(mode), - segments.join(";") - ); - match existing { - Some(diag) => { - diag.push(';'); - diag.push_str(¬e); - } - None => { - *existing = Some(note); - } - } -} - -fn build_noname_summary_hint(proposal: &crate::noname_types::NoNameProposal) -> String { - format!("NoName提示:后续重点关注{}", proposal.focus.trim()) -} - -fn build_noname_option_bias_hint(proposal: &crate::noname_types::NoNameProposal) -> String { - format!( - "NoName选项偏置:下轮优先围绕{}提供行动切入点", - proposal.focus.trim() - ) -} - -fn build_noname_plot_text_hint(proposal: &crate::noname_types::NoNameProposal) -> String { - format!("【NoName】重点关注:{}", proposal.focus.trim()) -} - -fn proposal_allows_apply_scope( - proposal: &crate::noname_types::NoNameProposal, - scope: NoNameApplyScope, -) -> bool { - proposal.apply_scopes.is_empty() || proposal.apply_scopes.contains(&scope) -} - -fn target_segment_supports_apply_scope( - target_segment: NoNameTargetSegment, - scope: NoNameApplyScope, -) -> bool { - match scope { - NoNameApplyScope::PlotTextHint | NoNameApplyScope::PlotAugmentationHint => matches!( - target_segment, - NoNameTargetSegment::CurrentTurnHead | NoNameTargetSegment::CurrentTurnTail - ), - _ => true, - } -} - -#[derive(Debug, Clone, PartialEq, Eq)] -enum NoNameApplyTargetDecision { - Apply, - Skip { outcome: &'static str, note: String }, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -struct NoNameApplyTargetPlan { - target: &'static str, - priority: u32, - order: u32, - decision: NoNameApplyTargetDecision, -} - #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct NoNameManualPlotTextApplyResult { @@ -220,10 +76,7 @@ fn load_noname_trace(trace_id: &str) -> Result { .lock() .map_err(|_| "NoName runtime lock poisoned".to_string())?; runtime - .get_recent_traces() - .into_iter() - .rev() - .find(|trace| trace.trace_id == trace_id) + .get_trace_by_id(trace_id) .ok_or_else(|| format!("NoName trace not found: {}", trace_id)) } @@ -269,505 +122,28 @@ where Ok(result) } -fn priority_for_apply_scope(scope: NoNameApplyScope) -> u32 { - match scope { - NoNameApplyScope::PlotTextHint => 300, - NoNameApplyScope::PlotAugmentationHint => 250, - NoNameApplyScope::ChapterSummaryHint => 200, - NoNameApplyScope::OptionBiasHint => 100, - NoNameApplyScope::Diagnostics => 50, - } -} - -fn plan_noname_apply_target( - proposal: &crate::noname_types::NoNameProposal, - scope: NoNameApplyScope, - plot_state: Option<&PlotState>, - plot_text: Option<&str>, -) -> NoNameApplyTargetPlan { - let target = scope.as_str(); - let priority = priority_for_apply_scope(scope); - if !proposal_allows_apply_scope(proposal, scope) { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_scope_forbidden", - note: format!("提案未声明 {} 作用域,跳过对应输出", target), - }, - }; - } - - if !target_segment_supports_apply_scope(proposal.target_segment, scope) { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_target_mismatch", - note: format!( - "目标段 {} 不支持 {},跳过对应输出", - proposal.target_segment.as_str(), - target - ), - }, - }; - } - - match scope { - NoNameApplyScope::PlotTextHint => { - let current_text = plot_text.unwrap_or_default(); - if current_text.trim().is_empty() { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_empty_plot_text", - note: "当前正文为空,跳过受控正文提示".to_string(), - }, - }; - } - if current_text.contains("【NoName】") || current_text.contains("NoName提示") { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_duplicate", - note: "正文已包含 NoName 标记,跳过重复提示".to_string(), - }, - }; - } - } - NoNameApplyScope::PlotAugmentationHint => { - let Some(state) = plot_state else { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_missing_plot_state", - note: "缺少 plot_state,跳过剧情增强提示".to_string(), - }, - }; - }; - if !state.is_waiting_for_input { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_not_waiting", - note: "当前不处于等待输入状态,跳过剧情增强提示".to_string(), - }, - }; - } - let hint = format!( - "NoName plot augmentation: focus={} | effect={}", - proposal.focus.trim(), - proposal.intended_effect.trim() - ); - if state - .pending_plot_augmentation_hints - .iter() - .any(|existing| existing == &hint) - { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_duplicate", - note: format!( - "剧情增强提示已包含“{}”,跳过重复写入", - proposal.focus.trim() - ), - }, - }; - } - } - NoNameApplyScope::ChapterSummaryHint => { - if let Some(state) = plot_state { - let summary = state.current_chapter.summary.trim(); - if !summary.is_empty() && summary.contains(proposal.focus.trim()) { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_duplicate", - note: format!( - "章节摘要已覆盖“{}”,跳过重复提示", - proposal.focus.trim() - ), - }, - }; - } - } - } - NoNameApplyScope::OptionBiasHint => { - let Some(state) = plot_state else { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_missing_plot_state", - note: "缺少 plot_state,跳过选项偏置提示".to_string(), - }, - }; - }; - if !state.is_waiting_for_input { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_not_waiting", - note: "当前不处于等待输入状态,跳过选项偏置提示".to_string(), - }, - }; - } - let option_hint = build_noname_option_bias_hint(proposal); - let diagnostics = state - .last_generation_diagnostics - .as_deref() - .unwrap_or_default(); - if diagnostics.contains(option_hint.as_str()) { - return NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Skip { - outcome: "skipped_duplicate", - note: format!( - "诊断中已包含选项偏置提示“{}”,跳过重复写入", - proposal.focus.trim() - ), - }, - }; - } - } - NoNameApplyScope::Diagnostics => {} - } - - NoNameApplyTargetPlan { - target, - priority, - order: 0, - decision: NoNameApplyTargetDecision::Apply, - } -} - -fn build_noname_apply_plan_set( - proposal: &crate::noname_types::NoNameProposal, - plot_state: Option<&PlotState>, - plot_text: Option<&str>, -) -> Vec { - let mut plans = vec![ - plan_noname_apply_target( - proposal, - NoNameApplyScope::PlotTextHint, - plot_state, - plot_text, - ), - plan_noname_apply_target( - proposal, - NoNameApplyScope::PlotAugmentationHint, - plot_state, - None, - ), - plan_noname_apply_target( - proposal, - NoNameApplyScope::ChapterSummaryHint, - plot_state, - None, - ), - plan_noname_apply_target(proposal, NoNameApplyScope::OptionBiasHint, plot_state, None), - ]; - plans.sort_by(|left, right| { - right - .priority - .cmp(&left.priority) - .then_with(|| left.target.cmp(right.target)) - }); - for (index, plan) in plans.iter_mut().enumerate() { - plan.order = (index + 1) as u32; - } - plans -} - -fn record_noname_apply_plan(trace: &mut NoNameTrace, plan: &NoNameApplyTargetPlan) { - let (decision, note) = match &plan.decision { - NoNameApplyTargetDecision::Apply => ( - "apply", - Some(format!( - "允许执行 {},优先级 {},顺位 #{}", - plan.target, plan.priority, plan.order - )), - ), - NoNameApplyTargetDecision::Skip { note, .. } => ("skip", Some(note.clone())), - }; - trace.record_apply_plan(plan.order, plan.target, decision, plan.priority, note); -} - -fn record_noname_apply_plan_and_skip_execution( - trace: &mut NoNameTrace, - plan: &NoNameApplyTargetPlan, -) { - record_noname_apply_plan(trace, plan); - if let NoNameApplyTargetDecision::Skip { outcome, note } = &plan.decision { - trace.record_apply_execution(plan.target, *outcome, Some(note.clone())); - } -} - -fn apply_noname_plot_text_hint( - plot_text: &mut String, - noname_result: &mut NoNameDirectorRunResult, -) -> bool { - if noname_result.proposal.status != crate::noname_types::NoNameProposalStatus::Applied { - return false; - } - - let plan = plan_noname_apply_target( - &noname_result.proposal, - NoNameApplyScope::PlotTextHint, - None, - Some(plot_text.as_str()), - ); - if !matches!(plan.decision, NoNameApplyTargetDecision::Apply) { - if let NoNameApplyTargetDecision::Skip { outcome, note } = &plan.decision { - noname_result - .trace - .record_apply_execution(plan.target, *outcome, Some(note.clone())); - } - return false; - } - - let hint = build_noname_plot_text_hint(&noname_result.proposal); - match noname_result.proposal.target_segment { - NoNameTargetSegment::CurrentTurnHead => { - let original = plot_text.trim().to_string(); - *plot_text = format!("{}\n\n{}", hint, original); - } - _ => { - plot_text.push_str("\n\n"); - plot_text.push_str(&hint); - } - } - noname_result.trace.record_proposal_transition(format!( - "{}:applied:plot_text_hint", - noname_result.proposal.proposal_id - )); - noname_result.trace.record_apply_execution( - "plot_text_hint", - "applied", - Some(format!( - "已将提案提示插入正文,聚焦“{}”", - noname_result.proposal.focus - )), - ); - true -} - -fn apply_noname_low_risk_outputs( - plot_state: &mut PlotState, - noname_result: &mut NoNameDirectorRunResult, - plot_text_applied: bool, -) { - if noname_result.proposal.status != crate::noname_types::NoNameProposalStatus::Applied { - return; - } - - let mut applied_targets: Vec<&str> = Vec::new(); - - let summary_plan = plan_noname_apply_target( - &noname_result.proposal, - NoNameApplyScope::ChapterSummaryHint, - Some(plot_state), - None, - ); - if matches!(summary_plan.decision, NoNameApplyTargetDecision::Apply) { - let hint = build_noname_summary_hint(&noname_result.proposal); - let summary = plot_state.current_chapter.summary.trim(); - if summary.is_empty() { - plot_state.current_chapter.summary = hint.clone(); - } else { - plot_state.current_chapter.summary = match noname_result.proposal.target_segment { - NoNameTargetSegment::ChapterSummaryHead => format!("{};{}", hint, summary), - _ => format!("{};{}", summary, hint), - }; - } - noname_result.trace.record_proposal_transition(format!( - "{}:applied:chapter_summary_hint", - noname_result.proposal.proposal_id - )); - noname_result.trace.record_apply_execution( - "chapter_summary_hint", - "applied", - Some(format!( - "已写入章节摘要提示,聚焦“{}”", - noname_result.proposal.focus - )), - ); - applied_targets.push("chapter_summary_hint"); - } else { - record_noname_apply_plan_and_skip_execution(&mut noname_result.trace, &summary_plan); - } - - let option_bias_plan = plan_noname_apply_target( - &noname_result.proposal, - NoNameApplyScope::OptionBiasHint, - Some(plot_state), - None, - ); - if matches!(option_bias_plan.decision, NoNameApplyTargetDecision::Apply) { - let option_hint = build_noname_option_bias_hint(&noname_result.proposal); - let diagnostics = plot_state - .last_generation_diagnostics - .clone() - .unwrap_or_default(); - if diagnostics.is_empty() { - plot_state.last_generation_diagnostics = Some(option_hint.clone()); - } else if !diagnostics.contains(option_hint.as_str()) { - plot_state.last_generation_diagnostics = - Some(format!("{};{}", diagnostics, option_hint)); - } - noname_result.trace.record_proposal_transition(format!( - "{}:applied:option_bias_hint", - noname_result.proposal.proposal_id - )); - noname_result.trace.record_apply_execution( - "option_bias_hint", - "applied", - Some(format!( - "已写入下轮选项偏置提示,聚焦“{}”", - noname_result.proposal.focus - )), - ); - applied_targets.push("option_bias_hint"); - } else { - record_noname_apply_plan_and_skip_execution(&mut noname_result.trace, &option_bias_plan); - } - - if plot_text_applied { - applied_targets.push("plot_text_hint"); - } - let apply_outcome = if applied_targets.is_empty() { - "applied_no_scoped_output" - } else { - "applied_scoped_outputs" - }; - noname_result.trace.set_apply_result( - true, - apply_outcome, - Some(format!( - "已应用作用域:{};聚焦“{}”", - if applied_targets.is_empty() { - "无".to_string() - } else { - applied_targets.join(", ") - }, - noname_result.proposal.focus - )), - ); - - if let Ok(mut runtime) = noname_runtime().lock() { - let _ = runtime.replace_trace(noname_result.trace.clone()); - } -} - fn run_noname_observe_only_for_turn( action_summary: &str, game_state: &GameState, plot_state: &PlotState, timestamp: u64, ) -> Result { - let mut memory_manager = NoNameMemoryManager::new(); - if let Ok(memory) = memory_layers().lock() { - memory_manager.ingest_legacy_layers(&memory); - } - memory_manager.push_working_memory( - crate::noname_memory_types::NoNameWorkingMemoryItem { - memory_id: format!("work-{}", timestamp), - turn_id: format!("turn-{}", timestamp), - source: "execute_player_action".to_string(), - category: "recent_turn".to_string(), - summary: action_summary.to_string(), - expires_at: None, - priority: 10, - }, - 8, - ); - let store = entity_store() .lock() .map_err(|_| "NoName observe-only: entity store lock poisoned".to_string())?; - let context_packet = build_context_packet( - &store, - &memory_manager, - &NoNameContextBuildInput { - role: NoNameRole::Director, - world_id: game_state.script.id.clone(), - run_id: "active-run".to_string(), - scene_id: plot_state.current_scene.id.clone(), - character_ids: vec![game_state.player.id.clone()], - map_node_id: Some(game_state.player.location.clone()), - player_intent: if action_summary.trim().is_empty() { - None - } else { - Some(action_summary.to_string()) - }, - recent_context_lines: plot_state - .current_chapter - .content - .iter() - .rev() - .take(6) - .cloned() - .collect::>() - .into_iter() - .rev() - .collect(), - token_budget: 320, - per_section_limit: 4, - }, - ); - drop(store); - + let memory = memory_layers().lock().ok(); let mut runtime = noname_runtime() .lock() .map_err(|_| "NoName observe-only: runtime lock poisoned".to_string())?; - let result = runtime - .run_director_observe_turn( - NoNameTurnInput { - trace_id: format!("noname-trace-{}", timestamp), - session_id: "active-run".to_string(), - turn_id: format!("turn-{}", timestamp), - caller_role: NoNameRole::Director, - }, - action_summary, - &context_packet, - Some(&NoNameDirectorGuardrailInput { - plot_state: plot_state.clone(), - action_result: plot_state.last_action_result.clone().unwrap_or( - crate::numerical_system::ActionResult { - success: true, - description: action_summary.to_string(), - stat_changes: Vec::new(), - events: Vec::new(), - }, - ), - player_name: game_state.player.name.clone(), - player_realm_level: game_state.player.stats.cultivation_realm.level, - player_combat_power: game_state.player.stats.combat_power, - }), - ) - .map_err(|e| e.to_string())?; - - Ok(result) + run_noname_director_observe_turn( + &mut runtime, + &store, + memory.as_deref(), + action_summary, + game_state, + plot_state, + timestamp, + ) } fn maybe_run_noname_for_turn( @@ -3833,7 +3209,11 @@ pub async fn execute_player_action( for plan in &plans { record_noname_apply_plan(&mut result.trace, plan); } - noname_plot_text_applied = apply_noname_plot_text_hint(&mut plot_text_for_player, result); + noname_plot_text_applied = apply_noname_plot_text_hint( + &mut plot_text_for_player, + &mut result.trace, + &result.proposal, + ); } plot_state.append_segment(plot_text_for_history.clone()); @@ -4173,29 +3553,15 @@ pub async fn execute_player_action( } if let Some(mut noname_result) = noname_result { - if let Some((outcome, note)) = pending_plot_augmentation_trace.take() { - noname_result.trace.record_apply_execution( - NoNameApplyScope::PlotAugmentationHint.as_str(), - outcome.as_str(), - Some(note), - ); - noname_result.trace.record_proposal_transition(format!( - "{}:pending_plot_augmentation:{}", - noname_result.proposal.proposal_id, outcome - )); - } - apply_noname_low_risk_outputs( + apply_noname_turn_outputs( &mut plot_state, &mut noname_result, noname_plot_text_applied, + pending_plot_augmentation_trace.take(), ); - append_noname_observation_diagnostics( - &mut plot_state.last_generation_diagnostics, - noname_result.trace.mode, - &noname_result.observation, - noname_result.guardrail_result.as_ref(), - &noname_result.trace, - ); + if let Ok(mut runtime) = noname_runtime().lock() { + let _ = runtime.replace_trace(noname_result.trace.clone()); + } } game_state.player.refresh_profile_views(); @@ -4238,24 +3604,14 @@ pub async fn mark_noname_controlled_output_review( let mut runtime = noname_runtime() .lock() .map_err(|_| "NoName runtime lock poisoned".to_string())?; - let mut trace = runtime - .get_recent_traces() - .into_iter() - .rev() - .find(|trace| trace.trace_id == trace_id) - .ok_or_else(|| format!("NoName trace not found: {}", trace_id))?; let reviewed_at = SystemTime::now() .duration_since(UNIX_EPOCH) .map(|duration| duration.as_secs()) .unwrap_or_default(); - apply_human_review_decision_to_trace(&mut trace, &request_id, decision, reviewed_at)?; - - if !runtime.replace_trace(trace.clone()) { - return Err(format!("NoName trace not found: {}", trace_id)); - } - - Ok(trace) + runtime.update_trace_by_id(&trace_id, |trace| { + apply_human_review_decision_to_trace(trace, &request_id, decision, reviewed_at) + }) } #[tauri::command] @@ -4267,20 +3623,9 @@ pub async fn resolve_noname_second_guardrail( let mut runtime = noname_runtime() .lock() .map_err(|_| "NoName runtime lock poisoned".to_string())?; - let mut trace = runtime - .get_recent_traces() - .into_iter() - .rev() - .find(|trace| trace.trace_id == trace_id) - .ok_or_else(|| format!("NoName trace not found: {}", trace_id))?; - - resolve_second_guardrail_for_trace(&mut trace, &request_id, decision)?; - - if !runtime.replace_trace(trace.clone()) { - return Err(format!("NoName trace not found: {}", trace_id)); - } - - Ok(trace) + runtime.update_trace_by_id(&trace_id, |trace| { + resolve_second_guardrail_for_trace(trace, &request_id, decision) + }) } #[tauri::command] @@ -5037,7 +4382,11 @@ mod tests { use super::*; use crate::event_log::{EventImportance, GameEvent}; use crate::models::{CultivationRealm, Element, Grade, SpiritualRoot}; + use crate::noname_apply::apply_noname_low_risk_outputs; + use crate::noname_command_support::append_noname_observation_diagnostics; use crate::noname_output_interface::NoNameControlledOutputDecision; + use crate::noname_roles::NoNameDirectorObservation; + use crate::noname_types::NoNameRole; use crate::script::{InitialState, Location, ScriptType, WorldSetting}; use std::sync::{Mutex as TestMutex, OnceLock as TestOnceLock}; use tempfile::tempdir; @@ -6877,116 +6226,6 @@ mod tests { assert_eq!(normalize_action_bucket(&selected_only), "selected:2"); } - #[test] - fn test_build_noname_action_summary_prefers_selected_option_text() { - let action = PlayerAction { - action_type: crate::plot_engine::ActionType::SelectedOption, - content: String::new(), - selected_option_id: Some(0), - meta: None, - }; - let plot_state = PlotState { - current_scene: crate::plot_engine::Scene { - id: "scene-1".to_string(), - name: "山门".to_string(), - description: "山门风声渐紧".to_string(), - location: "sect".to_string(), - available_options: vec![make_option(0, "返回山门广场")], - }, - plot_history: vec![], - is_waiting_for_input: true, - interaction_state: PlotInteractionState::WaitingForChoice, - last_action_result: None, - settings: PlotSettings::default(), - current_chapter: crate::plot_engine::ChapterState::new(1, "第一章".to_string()), - chapters: vec![], - segment_count: 0, - last_generation_diagnostics: None, - last_option_generation_source: None, - last_consistency_risk_score: None, - pending_plot_augmentation_hints: Vec::new(), - }; - - let summary = build_noname_action_summary(&action, &plot_state); - assert!(summary.contains("返回山门广场")); - } - - #[test] - fn test_append_noname_observation_diagnostics_appends_note() { - let mut diagnostics = Some("原始诊断".to_string()); - let observation = NoNameDirectorObservation { - role: NoNameRole::Director, - action_summary: "选择返回山门".to_string(), - focus: "山门危机".to_string(), - rationale: "应优先观察山门危机".to_string(), - prompt_preview: "prompt".to_string(), - role_goal: None, - scene_focus: None, - forbidden_scopes: Vec::new(), - note_type_hits: Vec::new(), - source_stats: Vec::new(), - context_token_budget_used: None, - context_slice_stats: Vec::new(), - proposal: crate::noname_types::NoNameProposal { - proposal_id: "proposal-1".to_string(), - kind: crate::noname_types::NoNameProposalKind::PlotCandidate, - producer_role: NoNameRole::Director, - title: "Director提案:山门危机".to_string(), - summary: "建议优先观察山门危机".to_string(), - focus: "山门危机".to_string(), - target_segment: crate::noname_types::NoNameTargetSegment::CurrentTurnTail, - intended_effect: "维持低风险观察导向".to_string(), - rationale: "应优先观察山门危机".to_string(), - suggested_action: Some("保持 observe-only".to_string()), - labels: vec!["director".to_string()], - apply_scopes: vec![crate::noname_types::NoNameApplyScope::Diagnostics], - status: crate::noname_types::NoNameProposalStatus::Observed, - applyable: false, - }, - }; - let guardrail = NoNameGuardrailResult::accept(); - - append_noname_observation_diagnostics( - &mut diagnostics, - NoNameMode::ObserveOnly, - &observation, - Some(&guardrail), - &NoNameTrace { - trace_id: "trace-1".to_string(), - session_id: "session-1".to_string(), - turn_id: "turn-1".to_string(), - mode: NoNameMode::ObserveOnly, - graph_path: vec![], - capability_calls: vec![], - proposals: vec![], - proposal_transition_log: vec!["proposal-1:observed".to_string()], - apply_plan_log: vec![], - apply_execution_log: vec![], - controlled_output_reviews: vec![], - related_observations: vec![], - protocol_events: vec![], - guardrail_result: None, - apply_result: Some(crate::noname_trace::NoNameApplyTraceResult { - attempted: false, - outcome: "skipped_observe_only".to_string(), - reason: Some("当前模式仅观察,不尝试 assisted apply".to_string()), - }), - fallback_used: false, - elapsed_ms: 0, - }, - ); - - let text = diagnostics.expect("diagnostics should exist"); - assert!(text.contains("原始诊断")); - assert!(text.contains("NoName.observe_only")); - assert!(text.contains("山门危机")); - assert!(text.contains("Director提案")); - assert!(text.contains("proposal_status=observed")); - assert!(text.contains("applyable=no")); - assert!(text.contains("guardrail=accept")); - assert!(text.contains("apply=skipped_observe_only")); - } - #[test] fn test_t7_mode_matrix_disabled_skips_noname_turn() { let _guard = noname_mode_matrix_lock() @@ -7018,7 +6257,12 @@ mod tests { let mut result = maybe_run_noname_for_turn("自由输入: 观察山门灵气", &game_state, &plot_state, 11) .expect("observe-only should produce a NoName observation"); - apply_noname_low_risk_outputs(&mut plot_state_after, &mut result, false); + apply_noname_low_risk_outputs( + &mut plot_state_after, + &mut result.trace, + &result.proposal, + false, + ); append_noname_observation_diagnostics( &mut plot_state_after.last_generation_diagnostics, result.trace.mode, @@ -7066,7 +6310,12 @@ mod tests { let mut result = maybe_run_noname_for_turn("自由输入: 观察山门灵气", &game_state, &plot_state, 12) .expect("assisted should produce a NoName observation"); - apply_noname_low_risk_outputs(&mut plot_state_after, &mut result, false); + apply_noname_low_risk_outputs( + &mut plot_state_after, + &mut result.trace, + &result.proposal, + false, + ); append_noname_observation_diagnostics( &mut plot_state_after.last_generation_diagnostics, result.trace.mode, @@ -7400,7 +6649,7 @@ mod tests { related_observations: vec![], }; - apply_noname_low_risk_outputs(&mut plot_state, &mut result, false); + apply_noname_low_risk_outputs(&mut plot_state, &mut result.trace, &result.proposal, false); assert!(plot_state.current_chapter.summary.contains("NoName提示")); assert!(plot_state.current_chapter.summary.contains("山门危机")); @@ -7509,7 +6758,8 @@ mod tests { }; let mut plot_text = "山门风声渐紧".to_string(); - let applied = apply_noname_plot_text_hint(&mut plot_text, &mut result); + let applied = + apply_noname_plot_text_hint(&mut plot_text, &mut result.trace, &result.proposal); assert!(applied); assert!(plot_text.contains("【NoName】重点关注:山门危机")); @@ -7598,7 +6848,8 @@ mod tests { }; let mut plot_text = "山门风声渐紧".to_string(); - let applied = apply_noname_plot_text_hint(&mut plot_text, &mut result); + let applied = + apply_noname_plot_text_hint(&mut plot_text, &mut result.trace, &result.proposal); assert!(applied); assert!(plot_text.starts_with("【NoName】重点关注:山门危机")); @@ -7698,7 +6949,7 @@ mod tests { related_observations: vec![], }; - apply_noname_low_risk_outputs(&mut plot_state, &mut result, false); + apply_noname_low_risk_outputs(&mut plot_state, &mut result.trace, &result.proposal, false); assert!(!plot_state.current_chapter.summary.contains("NoName提示")); assert!(plot_state @@ -7818,7 +7069,7 @@ mod tests { related_observations: vec![], }; - apply_noname_low_risk_outputs(&mut plot_state, &mut result, false); + apply_noname_low_risk_outputs(&mut plot_state, &mut result.trace, &result.proposal, false); assert!(plot_state .current_chapter @@ -7899,7 +7150,8 @@ mod tests { }; let mut plot_text = "山门风声渐紧".to_string(); - let applied = apply_noname_plot_text_hint(&mut plot_text, &mut result); + let applied = + apply_noname_plot_text_hint(&mut plot_text, &mut result.trace, &result.proposal); assert!(!applied); assert_eq!(plot_text, "山门风声渐紧"); @@ -8008,7 +7260,7 @@ mod tests { related_observations: vec![], }; - apply_noname_low_risk_outputs(&mut plot_state, &mut result, false); + apply_noname_low_risk_outputs(&mut plot_state, &mut result.trace, &result.proposal, false); assert_eq!( plot_state.current_chapter.summary, @@ -8117,7 +7369,7 @@ mod tests { related_observations: vec![], }; - apply_noname_low_risk_outputs(&mut plot_state, &mut result, false); + apply_noname_low_risk_outputs(&mut plot_state, &mut result.trace, &result.proposal, false); assert_eq!( plot_state