//! 占位样本数据:一段内置会话 + 出图矩阵要用的 `ULL_AGENT_*` 取证开关。 //! //! ## 为什么是内置样本而不是 presenter //! //! 本 mission 只做**壳**:真数据由 M103(`presenter::agent::AgentRail` + 四个签名)供, //! 这里一行 presenter 调用都没有(任务里点名「不许提前调用」)。样本的职责只有两个: //! ①让三档状态在图上都有内容;②让「空会话」有对照([`FIXTURE_ENV`])。 //! //! 样本正文是**演示数据**,与 `pages/instances/data.rs` 的 demo 同一性质:它不是界面文案, //! 不进 i18n 词表(界面上会随真模型一起消失)。界面自己的文案(标题/按钮/空态/提示)全部走 `Key`。 //! //! ## 取证开关(只影响这一次进程,语义与 `ULL_APPEARANCE_*` / `ULL_START_ROUTE` 一致) //! //! | 变量 | 取值 | 作用 | //! |---|---|---| //! | [`RAIL_ENV`] | `collapsed` / `expanded` / `detached` | 启动时直接落到某一档(三态矩阵) | //! | [`FIXTURE_ENV`] | `empty` / `streaming` / `unread` / `focused` | 空会话(零消息)/ 流式中(中断键可见)/ 展开但带未读(单独看徽标)/ 键盘焦点环(`Tab` 走过一次的样子) | //! | [`WIDTH_ENV`] | `280` / `320` / `360` | 展开档宽度(磁铁档位的对照图) | use super::metrics::PRESETS; use super::state::{AgentUi, RailState, Speaker, Turn}; /// 启动档位。 pub const RAIL_ENV: &str = "ULL_AGENT_RAIL"; /// 会话样本(`empty` / `streaming`)。 pub const FIXTURE_ENV: &str = "ULL_AGENT_FIXTURE"; /// 展开档宽度(只接受三档预设值)。 pub const WIDTH_ENV: &str = "ULL_AGENT_WIDTH"; /// 内置会话样本(4 条,两类说话人 + 一条工具提示)。 pub fn transcript() -> Vec { vec![ Turn::new(Speaker::You, "Which instance should I start for a 1.20.4 test?"), Turn::new( Speaker::Assistant, "Survival is on 1.20.4 with Fabric 0.16.5 and its files are present. Redstone Lab is the same \ version but its folder is missing a jar.", ), Turn::new(Speaker::You, "Use Survival then. Anything I should know first?"), Turn::new( Speaker::Assistant, "Java 21 is already adopted for that instance, and the last launch exited normally. \ Say the word and I will queue it.", ), ] } /// 按环境变量构造一份状态(没设变量 = `None`,调用方用出厂态)。 /// /// 解析失败一律**忽略**(取证开关不该让进程起不来),并在 stderr 留一行,方便对照日志读图。 pub fn from_env() -> Option { let rail = std::env::var(RAIL_ENV).ok(); let fixture = std::env::var(FIXTURE_ENV).ok(); let width = std::env::var(WIDTH_ENV).ok(); if rail.is_none() && fixture.is_none() && width.is_none() { return None; } let fixture = fixture.as_deref().map(str::trim); let mut ui = match fixture { Some("empty") => AgentUi::empty(), _ => AgentUi::new(), }; if let Some(raw) = width.as_deref().map(str::trim) { match raw.parse::() { Ok(value) if PRESETS.contains(&value) => ui.set_expanded(value), _ => eprintln!("[agent] {WIDTH_ENV}={raw:?} 不是预设档位 {PRESETS:?} 之一,忽略"), } } if fixture == Some("streaming") { ui.pretend_streaming(); } // 档位走**真迁移**(`expand` / `detach` / `collapse`)而不是直接写字段:这样未读的语义 // (展开即读)在取证图里也成立,不会出现「展开着还挂着 2 条未读」这种自相矛盾的画面。 // 唯一要单独看徽标的情形用 `ULL_AGENT_FIXTURE=unread` 显式造(见模块文档的表)。 match rail.as_deref().map(str::trim) { None => ui.expand(), Some(name) => match RailState::from_name(name) { Some(RailState::Collapsed) => ui.collapse(), Some(RailState::Detached) => ui.detach(), Some(RailState::Expanded) => ui.expand(), None => eprintln!( "[agent] {RAIL_ENV}={name:?} 不是三档之一({}),忽略", RailState::ALL.map(RailState::name).join("/") ), }, } if fixture == Some("unread") { ui.unread = 3; } if fixture == Some("focused") { // 键盘巡航一次的样子(焦点环 = 10% 状态层)。真窗口里按一次 `Tab` 就会到这一态, // 但截图通道收不到键盘输入,所以只能这样取证。 ui.step_focus(false); } eprintln!( "[agent] rail={} width={} turns={} busy={} unread={}", ui.rail.name(), ui.expanded(), ui.transcript.len(), ui.busy, ui.unread ); Some(ui) }