1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use std::collections::HashSet;
9
10use super::config::*;
11use crate::truncate::{find_boundary, truncate_with_suffix};
12
13pub(crate) fn truncate_str(s: &str, max_len: usize) -> String {
19 truncate_with_suffix(s, max_len)
20}
21
22pub(crate) fn truncate(s: &str, max_len: usize) -> String {
24 if s.len() <= max_len {
25 s.to_string()
26 } else {
27 let end = find_boundary(s, max_len);
28 s[..end].to_string()
29 }
30}
31
32pub fn parse_memory_links(content: &str) -> HashSet<String> {
35 let re = regex::Regex::new(r"\[\[([^\]]+)\]\]").unwrap();
37 re.captures_iter(content)
38 .map(|c| c[1].trim().to_string())
39 .collect()
40}
41
42pub fn has_memory_links(content: &str) -> bool {
44 content.contains("[[") && content.contains("]]")
45}
46
47#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Hash)]
53#[serde(rename_all = "snake_case")]
54pub enum MemoryCategory {
55 Preference,
57 Decision,
59 Finding,
61 Solution,
63 Technical,
65 Structure,
67 KeyDecision,
69 FailedApproach,
71 UserIntentPattern,
73 TaskPattern,
75}
76
77impl MemoryCategory {
78 pub fn display_name(&self) -> &'static str {
80 match self {
81 MemoryCategory::Preference => "偏好",
82 MemoryCategory::Decision => "决策",
83 MemoryCategory::Finding => "发现",
84 MemoryCategory::Solution => "解决方案",
85 MemoryCategory::Technical => "技术",
86 MemoryCategory::Structure => "结构",
87 MemoryCategory::KeyDecision => "关键决策",
88 MemoryCategory::FailedApproach => "失败方案",
89 MemoryCategory::UserIntentPattern => "意图模式",
90 MemoryCategory::TaskPattern => "任务模式",
91 }
92 }
93
94 pub fn icon(&self) -> &'static str {
96 match self {
97 MemoryCategory::Preference => "👤",
98 MemoryCategory::Decision => "🎯",
99 MemoryCategory::Finding => "💡",
100 MemoryCategory::Solution => "🔧",
101 MemoryCategory::Technical => "📚",
102 MemoryCategory::Structure => "🏗️",
103 MemoryCategory::KeyDecision => "⚡",
104 MemoryCategory::FailedApproach => "❌",
105 MemoryCategory::UserIntentPattern => "🧠",
106 MemoryCategory::TaskPattern => "📋",
107 }
108 }
109
110 pub fn default_importance(&self) -> f64 {
112 match self {
113 MemoryCategory::Decision => DEFAULT_IMPORTANCE_DECISION,
114 MemoryCategory::Solution => DEFAULT_IMPORTANCE_SOLUTION,
115 MemoryCategory::Preference => DEFAULT_IMPORTANCE_PREF,
116 MemoryCategory::Finding => DEFAULT_IMPORTANCE_FINDING,
117 MemoryCategory::Technical => DEFAULT_IMPORTANCE_TECH,
118 MemoryCategory::Structure => DEFAULT_IMPORTANCE_STRUCTURE,
119 MemoryCategory::KeyDecision => 85.0,
120 MemoryCategory::FailedApproach => 70.0,
121 MemoryCategory::UserIntentPattern => 80.0,
122 MemoryCategory::TaskPattern => 75.0,
123 }
124 }
125}
126
127#[derive(Debug, Clone, Serialize, Deserialize)]
133pub struct MemoryEntry {
134 pub id: String,
136 #[serde(default, skip_serializing_if = "Option::is_none")]
138 pub name: Option<String>,
139 pub created_at: DateTime<Utc>,
141 pub last_referenced: DateTime<Utc>,
143 pub category: MemoryCategory,
145 pub content: String,
147 pub source_session: Option<String>,
149 #[serde(default, skip_serializing_if = "Option::is_none")]
151 pub project_path: Option<String>,
152 pub reference_count: u32,
154 pub importance: f64,
156 pub tags: Vec<String>,
158 pub is_manual: bool,
160 #[serde(default, skip_serializing_if = "HashSet::is_empty")]
162 pub related_memories: HashSet<String>,
163}
164
165impl MemoryEntry {
166 pub fn new(
168 category: MemoryCategory,
169 content: String,
170 source_session: Option<String>,
171 project_path: Option<String>,
172 ) -> Self {
173 let id = uuid::Uuid::new_v4().to_string();
174 let related_memories = parse_memory_links(&content);
176 Self {
177 id,
178 name: None,
179 created_at: Utc::now(),
180 last_referenced: Utc::now(),
181 category,
182 content,
183 source_session,
184 project_path,
185 reference_count: 0,
186 importance: category.default_importance(),
187 tags: Vec::new(),
188 is_manual: false,
189 related_memories,
190 }
191 }
192
193 pub fn with_name(
195 category: MemoryCategory,
196 name: String,
197 content: String,
198 source_session: Option<String>,
199 project_path: Option<String>,
200 ) -> Self {
201 let mut entry = Self::new(category, content, source_session, project_path);
202 entry.name = Some(name);
203 entry
204 }
205
206 pub fn manual(category: MemoryCategory, content: String, project_path: Option<String>) -> Self {
208 let mut entry = Self::new(category, content, None, project_path);
209 entry.is_manual = true;
210 entry.importance = 95.0;
211 entry
212 }
213
214 pub fn manual_with_name(
216 category: MemoryCategory,
217 name: String,
218 content: String,
219 project_path: Option<String>,
220 ) -> Self {
221 let mut entry = Self::manual(category, content, project_path);
222 entry.name = Some(name);
223 entry
224 }
225
226 pub fn manual_global(category: MemoryCategory, content: String) -> Self {
228 Self::manual(category, content, None)
229 }
230
231 pub fn get_links(&self) -> HashSet<String> {
233 parse_memory_links(&self.content)
234 }
235
236 pub fn has_links(&self) -> bool {
238 has_memory_links(&self.content)
239 }
240
241 pub fn refresh_links(&mut self) {
243 self.related_memories = parse_memory_links(&self.content);
244 }
245
246 pub fn mark_referenced(&mut self) {
248 self.mark_referenced_with_increment(2.0);
249 }
250
251 pub fn mark_referenced_with_increment(&mut self, increment: f64) {
253 self.reference_count += 1;
254 self.last_referenced = Utc::now();
255 self.importance = (self.importance + increment).min(MAX_IMPORTANCE_CEILING);
256 }
257
258 pub fn format_line(&self) -> String {
260 let time = self.created_at.format("%Y-%m-%d %H:%M");
261 let importance_marker = if self.importance >= IMPORTANCE_STAR_THRESHOLD {
262 "⭐"
263 } else {
264 ""
265 };
266 let manual_marker = if self.is_manual { "📝" } else { "" };
267 let link_marker = if self.has_links() { "🔗" } else { "" };
268 let name_display = self.name.as_deref().map(|n| format!("[{}]", n)).unwrap_or_default();
269 format!(
270 "{} {} {}{}{}{} {}",
271 self.category.icon(),
272 time,
273 importance_marker,
274 manual_marker,
275 link_marker,
276 name_display,
277 truncate_str(&self.content, MAX_DISPLAY_LENGTH)
278 )
279 }
280
281 pub fn format_for_prompt(&self) -> String {
284 if self.content.len() > MAX_MEMORY_CONTENT_LENGTH {
285 format!(
286 "{}...",
287 truncate(&self.content, MAX_MEMORY_CONTENT_LENGTH - 3)
288 )
289 } else {
290 self.content.clone()
291 }
292 }
293
294 pub fn format_for_prompt_with_category(&self) -> String {
297 let category_name = self.category.display_name();
298 if self.content.len() > MAX_MEMORY_CONTENT_LENGTH {
299 format!(
300 "{}: {}...",
301 category_name,
302 truncate(&self.content, MAX_MEMORY_CONTENT_LENGTH - 3)
303 )
304 } else {
305 format!("{}: {}", category_name, self.content)
306 }
307 }
308}
309
310#[cfg(test)]
311mod tests {
312 use super::*;
313
314 #[test]
315 fn test_parse_memory_links_single() {
316 let content = "使用 [[redis-config]] 进行缓存配置";
317 let links = parse_memory_links(content);
318 assert_eq!(links.len(), 1);
319 assert!(links.contains("redis-config"));
320 }
321
322 #[test]
323 fn test_parse_memory_links_multiple() {
324 let content = "参考 [[api-design]] 和 [[database-schema]] 进行开发";
325 let links = parse_memory_links(content);
326 assert_eq!(links.len(), 2);
327 assert!(links.contains("api-design"));
328 assert!(links.contains("database-schema"));
329 }
330
331 #[test]
332 fn test_parse_memory_links_no_links() {
333 let content = "这是一条普通记忆,没有链接";
334 let links = parse_memory_links(content);
335 assert!(links.is_empty());
336 }
337
338 #[test]
339 fn test_parse_memory_links_with_spaces() {
340 let content = "参考 [[ spaced-name ]] 进行开发";
341 let links = parse_memory_links(content);
342 assert!(links.contains("spaced-name")); }
344
345 #[test]
346 fn test_has_memory_links() {
347 assert!(has_memory_links("参考 [[config]] 设置"));
348 assert!(!has_memory_links("没有链接的记忆"));
349 }
350
351 #[test]
352 fn test_memory_entry_extract_links_on_creation() {
353 let entry = MemoryEntry::new(
354 MemoryCategory::Decision,
355 "使用 [[redis]] 作为缓存,参考 [[config-pattern]]".to_string(),
356 None,
357 None,
358 );
359 assert_eq!(entry.related_memories.len(), 2);
360 assert!(entry.related_memories.contains("redis"));
361 assert!(entry.related_memories.contains("config-pattern"));
362 }
363
364 #[test]
365 fn test_memory_entry_with_name() {
366 let entry = MemoryEntry::with_name(
367 MemoryCategory::Technical,
368 "api-endpoints".to_string(),
369 "API 端点定义".to_string(),
370 None,
371 None,
372 );
373 assert_eq!(entry.name, Some("api-endpoints".to_string()));
374 }
375
376 #[test]
377 fn test_memory_entry_refresh_links() {
378 let mut entry = MemoryEntry::new(
379 MemoryCategory::Decision,
380 "初始内容".to_string(),
381 None,
382 None,
383 );
384 assert!(entry.related_memories.is_empty());
385
386 entry.content = "更新内容,参考 [[new-link]]".to_string();
388 entry.refresh_links();
389 assert!(entry.related_memories.contains("new-link"));
390 }
391
392 #[test]
393 fn test_format_line_with_links() {
394 let entry = MemoryEntry::new(
395 MemoryCategory::Technical,
396 "参考 [[config]] 设置".to_string(),
397 None,
398 None,
399 );
400 let line = entry.format_line();
401 assert!(line.contains("🔗")); }
403
404 #[test]
405 fn test_format_line_with_name() {
406 let entry = MemoryEntry::with_name(
407 MemoryCategory::Decision,
408 "cache-decision".to_string(),
409 "决定使用 Redis".to_string(),
410 None,
411 None,
412 );
413 let line = entry.format_line();
414 assert!(line.contains("[cache-decision]"));
415 }
416}