|
1 | 1 | //! 档案记忆偏好域(REQ-043 / v0.7.2 REQ-152 系列键 / v0.9.0 REQ-188 四维形态 / v0.13.6 REQ-222 领域记忆)。 |
2 | 2 | //! |
3 | 3 | //! @ai-context: 从 video_profile.rs 拆出(AGENTS.md §3 单文件 ≤300 行):记忆偏好 = |
4 | | -//! 「用户确认过一次 → 同窗口标题/同系列下次直接生效」。三条通道互相独立: |
5 | | -//! kind/form 走 entries,domain 走 domain_entries(防 domain-only 条目把 |
6 | | -//! legacy kind 结果染成 Unknown);三者的 tie-break 一律「最长关键词优先」 |
7 | | -//! (len > best 严格大于 ⇒ 等长时先入条目胜出)。 |
| 4 | +//! 「用户确认过一次 → 同窗口标题/同系列下次直接生效」。四条通道互相独立: |
| 5 | +//! kind/form 走 entries,domain 走 domain_entries,tier 走 tier_entries |
| 6 | +//! (批 7 T19)——防某通道的条目把别的通道结果染脏;四者的 tie-break 一律 |
| 7 | +//! 「最长关键词优先」(len > best 严格大于 ⇒ 等长时先入条目胜出)。 |
8 | 8 | //! @ai-context: 副作用 = 文件 IO:load 读 JSON(缺失/损坏回落空库,不阻断启动); |
9 | 9 | //! save 先写 .tmp 再 rename(原子写,防写一半损坏记忆库)。调用方保证 |
10 | 10 | //! 路径与并发(本域无锁、无 tauri state,可 tempfile 单测)。 |
@@ -46,13 +46,32 @@ pub struct DomainMemoryEntry { |
46 | 46 | pub domain: crate::video_profile_spec::DomainTag, |
47 | 47 | } |
48 | 48 |
|
| 49 | +/// 批 7 T19(规格 §1 L5 行 34「记忆加 tier 字段」):画面档记忆条目。 |
| 50 | +/// |
| 51 | +/// @ai-context: 与 MemoryEntry(kind/form 通道)**分离**——防 tier-only 条目把 |
| 52 | +/// legacy kind/form 结果染脏(与 DomainMemoryEntry 的分离理由同族); |
| 53 | +/// 与 `update_live_profile`(**采集态热切换**,改正在跑的会话)严格区分: |
| 54 | +/// 本条只写记忆偏好(下次同标题/同系列会话的起点)。 |
| 55 | +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] |
| 56 | +pub struct TierMemoryEntry { |
| 57 | + pub keyword: String, |
| 58 | + /// series 键标记(与 MemoryEntry.is_series 同口径) |
| 59 | + #[serde(default)] |
| 60 | + pub is_series: bool, |
| 61 | + /// 用户确认的画面档(rich/medium/low/none) |
| 62 | + pub tier: crate::video_profile_spec::VisualTier, |
| 63 | +} |
| 64 | + |
49 | 65 | /// 记忆偏好库(JSON 持久化;同 vocab 模式:路径可注入,测试用 tempfile)。 |
50 | 66 | #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] |
51 | 67 | pub struct ProfileMemory { |
52 | 68 | pub entries: Vec<MemoryEntry>, |
53 | 69 | /// v0.13.6(REQ-222):领域记忆独立通道(旧 JSON 缺省空——零迁移) |
54 | 70 | #[serde(default)] |
55 | 71 | pub domain_entries: Vec<DomainMemoryEntry>, |
| 72 | + /// 批 7 T19:画面档记忆独立通道(旧 JSON 缺省空——零迁移,同 domain_entries 先例) |
| 73 | + #[serde(default)] |
| 74 | + pub tier_entries: Vec<TierMemoryEntry>, |
56 | 75 | } |
57 | 76 |
|
58 | 77 | impl ProfileMemory { |
@@ -191,6 +210,46 @@ impl ProfileMemory { |
191 | 210 | best.map(|(_, tag)| tag) |
192 | 211 | } |
193 | 212 |
|
| 213 | + /// 批 7 T19(规格 §1 L5 行 34):记录用户确认的画面档——同标题/系列下次直接生效。 |
| 214 | + /// |
| 215 | + /// @ai-context: 独立通道不污染 kind/form 记忆;空键拒绝(不记悬挂条目)。 |
| 216 | + pub fn remember_tier(&mut self, keyword: &str, tier: crate::video_profile_spec::VisualTier) { |
| 217 | + let (key, is_series) = Self::memory_key(keyword); |
| 218 | + if key.is_empty() { |
| 219 | + return; |
| 220 | + } |
| 221 | + if let Some(e) = self.tier_entries.iter_mut().find(|e| e.keyword == key) { |
| 222 | + e.is_series = is_series; |
| 223 | + e.tier = tier; |
| 224 | + } else { |
| 225 | + self.tier_entries.push(TierMemoryEntry { keyword: key, is_series, tier }); |
| 226 | + } |
| 227 | + } |
| 228 | + |
| 229 | + /// 按标题查询画面档记忆(批 7 T19):series 键优先,完整标题兜底;最长关键词优先。 |
| 230 | + pub fn lookup_tier(&self, title: &str) -> Option<crate::video_profile_spec::VisualTier> { |
| 231 | + if let Some(info) = crate::series_detect::extract_series(title) { |
| 232 | + if let Some(t) = self.lookup_tier_best(&info.series) { |
| 233 | + return Some(t); |
| 234 | + } |
| 235 | + } |
| 236 | + self.lookup_tier_best(title) |
| 237 | + } |
| 238 | + |
| 239 | + /// 最长关键词优先匹配(纯函数;档位记忆专用通道)。 |
| 240 | + fn lookup_tier_best(&self, key: &str) -> Option<crate::video_profile_spec::VisualTier> { |
| 241 | + let mut best: Option<(usize, crate::video_profile_spec::VisualTier)> = None; |
| 242 | + for e in &self.tier_entries { |
| 243 | + if key.contains(&e.keyword) && !e.keyword.is_empty() { |
| 244 | + let len = e.keyword.chars().count(); |
| 245 | + if best.as_ref().is_none_or(|(bl, _)| len > *bl) { |
| 246 | + best = Some((len, e.tier)); |
| 247 | + } |
| 248 | + } |
| 249 | + } |
| 250 | + best.map(|(_, tier)| tier) |
| 251 | + } |
| 252 | + |
194 | 253 | /// 按标题查询四维形态记忆(REQ-188):form 优先,旧条目经 kind 映射兜底。 |
195 | 254 | /// |
196 | 255 | /// @ai-context: 消费端 v2(检测卡/会话落库)用本方法;旧 lookup 保留为 |
@@ -226,3 +285,7 @@ impl ProfileMemory { |
226 | 285 | best.map(|(_, form)| form) |
227 | 286 | } |
228 | 287 | } |
| 288 | + |
| 289 | +#[cfg(test)] |
| 290 | +#[path = "video_profile_memory_tests.rs"] |
| 291 | +mod tests; |
0 commit comments