use anyhow::{Context, Result};
use std::collections::HashMap;
use crate::types::{DocumentMatch, TopicSummary};
pub(crate) fn list_topics() -> Result<serde_json::Value> {
eprintln!("📋 请求主题列表");
let summaries: Vec<TopicSummary> = crate::TOPICS
.iter()
.map(|t| TopicSummary {
topic: t.topic,
title: t.title,
when_to_use: t.when_to_use,
keywords: t.keywords,
})
.collect();
eprintln!("✓ 返回 {} 个主题", summaries.len());
serde_json::to_value(summaries).context("Failed to serialize topics")
}
fn parse_front_matter(content: &str) -> Option<(HashMap<String, String>, Vec<String>, String)> {
let lines: Vec<&str> = content.lines().collect();
if lines.is_empty() || lines[0].trim() != "---" {
return None;
}
let mut front_matter = HashMap::new();
let mut keywords = Vec::new();
let mut in_keywords = false;
let mut body_start = 0;
for (idx, line) in lines.iter().enumerate().skip(1) {
let trimmed = line.trim();
if trimmed == "---" {
body_start = idx + 1;
break;
}
if trimmed == "keywords:" {
in_keywords = true;
continue;
}
if in_keywords {
if let Some(stripped) = trimmed.strip_prefix("- ") {
keywords.push(stripped.trim().to_string());
} else if trimmed.contains(':') {
in_keywords = false;
}
}
if !in_keywords && trimmed.contains(':') {
let parts: Vec<&str> = trimmed.splitn(2, ':').collect();
if parts.len() == 2 {
front_matter.insert(parts[0].trim().to_string(), parts[1].trim().to_string());
}
}
}
let body = lines[body_start..].join("\n");
Some((front_matter, keywords, body))
}
pub(crate) fn search_across_docs(query_words: &[String]) -> Result<String> {
eprintln!("🔍 全局搜索关键词: {:?}", query_words);
if query_words.is_empty() {
return Ok("# ⚠️ 搜索错误\n\n搜索关键词不能为空。".to_string());
}
let query_lower: Vec<String> = query_words.iter().map(|w| w.to_lowercase()).collect();
let mut matches: Vec<DocumentMatch> = Vec::new();
for topic_info in crate::TOPICS.iter() {
let (_front_matter, _keywords_from_content, body) =
match parse_front_matter(topic_info.content) {
Some(parsed) => parsed,
None => continue,
};
let mut score = 0.0f32;
let mut matched_keywords = Vec::new();
let topic = topic_info.topic;
let title = topic_info.title;
let when_to_use = topic_info.when_to_use;
let topic_lower = topic.to_lowercase();
let title_lower = title.to_lowercase();
let when_to_use_lower = when_to_use.to_lowercase();
for query_word in &query_lower {
if topic_lower.contains(query_word) {
score += 10.0;
matched_keywords.push(query_word.clone());
}
if title_lower.contains(query_word) {
score += 5.0;
if !matched_keywords.contains(query_word) {
matched_keywords.push(query_word.clone());
}
}
for keyword in topic_info.keywords.iter() {
if keyword.to_lowercase().contains(query_word) {
score += 3.0;
if !matched_keywords.contains(query_word) {
matched_keywords.push(query_word.clone());
}
break;
}
}
if when_to_use_lower.contains(query_word) {
score += 2.0;
if !matched_keywords.contains(query_word) {
matched_keywords.push(query_word.clone());
}
}
if body.to_lowercase().contains(query_word) {
score += 1.0;
if !matched_keywords.contains(query_word) {
matched_keywords.push(query_word.clone());
}
}
}
if score > 0.0 {
matches.push(DocumentMatch {
topic: topic.to_string(),
title: title.to_string(),
when_to_use: when_to_use.to_string(),
keywords: topic_info.keywords.iter().map(|k| k.to_string()).collect(),
file_path: topic_info.file.to_string(),
matched_keywords,
relevance_score: score,
});
}
}
if matches.is_empty() {
return Ok(format!(
"# 未找到匹配结果\n\n搜索关键词: **{}**\n\n没有找到包含这些关键词的文档。\n\n提示:尝试使用不同的关键词或访问完整的主题列表。",
query_words.join(", ")
));
}
matches.sort_by(|a, b| b.relevance_score.partial_cmp(&a.relevance_score).unwrap());
let mut output = format!(
"# 搜索结果\n\n搜索关键词: **{}**\n\n找到 **{}** 个相关文档:\n\n",
query_words.join(", "),
matches.len()
);
for (idx, doc_match) in matches.iter().enumerate() {
let relevance_percentage =
(doc_match.relevance_score / matches[0].relevance_score * 100.0).round() as u32;
output.push_str(&format!(
"## {}. {} (相关度: {}%)\n\n",
idx + 1,
doc_match.title,
relevance_percentage
));
output.push_str(&format!("- **主题**: `{}`\n", doc_match.topic));
output.push_str(&format!("- **文件**: `{}`\n", doc_match.file_path));
if !doc_match.when_to_use.is_empty() {
output.push_str(&format!("- **使用场景**: {}\n", doc_match.when_to_use));
}
if !doc_match.keywords.is_empty() {
output.push_str(&format!(
"- **关键词**: {}\n",
doc_match
.keywords
.iter()
.map(|k| format!("`{}`", k))
.collect::<Vec<_>>()
.join(", ")
));
}
output.push_str(&format!(
"- **匹配关键词**: {}\n",
doc_match
.matched_keywords
.iter()
.map(|k| format!("`{}`", k))
.collect::<Vec<_>>()
.join(", ")
));
output.push('\n');
}
output.push_str("---\n\n💡 使用 `get_tips` 工具并指定准确的 `topic` 值来获取完整文档内容。\n");
eprintln!("✓ 找到 {} 个匹配文档", matches.len());
Ok(output)
}
pub(crate) fn get_tips(topic: &str) -> Result<String> {
eprintln!("📖 请求主题: {}", topic);
let topic_info = crate::TOPICS.iter().find(|t| t.topic == topic);
if let Some(info) = topic_info {
eprintln!("✓ 返回文档: {} ({} 字符)", topic, info.content.len());
Ok(info.content.to_string())
} else {
eprintln!("⚠️ 未找到精确匹配,尝试全局搜索");
let query_words: Vec<String> = topic
.split(|c: char| c.is_whitespace() || c == '_' || c == '-')
.filter(|s| !s.is_empty())
.map(|s| s.to_string())
.collect();
if query_words.is_empty() {
anyhow::bail!("主题未找到: {}", topic);
}
search_across_docs(&query_words)
}
}