From 5989b817b4acf996c8408dfa7c0662bebfe58a66 Mon Sep 17 00:00:00 2001 From: ooodc <549496103@qq.com> Date: Sun, 10 May 2026 09:37:41 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=9B=B4=E6=96=B0=E6=8F=90=E7=A4=BA?= =?UTF-8?q?=E8=AF=8D=E6=9D=A5=E6=BA=90=E7=B1=BB=E5=9E=8B=EF=BC=8C=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E7=94=A8=E6=88=B7=E8=87=AA=E5=AE=9A=E4=B9=89=E5=92=8C?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E7=94=9F=E6=88=90=E5=86=85=E5=AE=B9=EF=BC=9B?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=20AGENT.md=20=E6=A8=A1=E6=9D=BF=E6=96=87?= =?UTF-8?q?=E4=BB=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gateway/agent_md_template.md | 25 +++ src/gateway/prompt.rs | 280 +++++++++++++++++++++---------- 2 files changed, 221 insertions(+), 84 deletions(-) create mode 100644 src/gateway/agent_md_template.md diff --git a/src/gateway/agent_md_template.md b/src/gateway/agent_md_template.md new file mode 100644 index 0000000..5774448 --- /dev/null +++ b/src/gateway/agent_md_template.md @@ -0,0 +1,25 @@ + 可以保留说明文字而不生效 +- 删除本注释块并添加您的自定义配置即可生效 +--> diff --git a/src/gateway/prompt.rs b/src/gateway/prompt.rs index 6f184d8..c41cf8d 100644 --- a/src/gateway/prompt.rs +++ b/src/gateway/prompt.rs @@ -5,11 +5,20 @@ use crate::agent::AgentError; use crate::platform::atomic_rename; pub(crate) const DEFAULT_AGENT_PROMPT: &str = include_str!("default_agent_prompt.md"); +pub(crate) const AGENT_MD_TEMPLATE: &str = include_str!("agent_md_template.md"); +/// 提示词来源类型 #[derive(Clone)] -struct PromptSource { - path: PathBuf, - default_content: Option<&'static str>, +enum PromptSource { + /// 内置内容 - 系统默认,始终注入,不可修改 + Builtin(&'static str), + /// 用户自定义 - 自动创建空白文件,可选注入 + UserCustom { + path: PathBuf, + template: &'static str, + }, + /// 自动生成 - 由系统维护,可选注入 + AutoGenerated(PathBuf), } pub(crate) fn load_agent_prompt() -> Result, AgentError> { @@ -22,52 +31,73 @@ pub(crate) fn upsert_managed_agent_memory_summary(markdown_body: &str) -> Result fn prompt_sources() -> Result, AgentError> { Ok(vec![ - PromptSource { + // 1. 系统默认 - 始终注入 + PromptSource::Builtin(DEFAULT_AGENT_PROMPT), + // 2. 用户自定义 - 自动创建空白模板,可选注入 + PromptSource::UserCustom { path: agent_prompt_path()?, - default_content: Some(DEFAULT_AGENT_PROMPT), - }, - PromptSource { - path: memory_summary_path()?, - default_content: None, + template: AGENT_MD_TEMPLATE, }, + // 3. 记忆摘要 - 可选 + PromptSource::AutoGenerated(memory_summary_path()?), ]) } fn load_prompt_from_sources(sources: &[PromptSource]) -> Result, AgentError> { - let mut fragments = Vec::with_capacity(sources.len()); + let mut fragments = Vec::new(); for source in sources { - if let Some(fragment) = read_prompt_fragment(source)? { - fragments.push(fragment); + match source { + PromptSource::Builtin(content) => { + fragments.push(content.to_string()); + } + PromptSource::UserCustom { path, template } => { + // 确保父目录存在 + ensure_parent_dir(path)?; + // 文件不存在时创建空白模板 + if !path.exists() { + fs::write(path, template) + .map_err(|err| AgentError::Other(format!("create AGENT.md template error: {}", err)))?; + } + // 读取内容,仅当非空(去除注释后)时注入 + let content = fs::read_to_string(path) + .map_err(|err| AgentError::Other(format!("read AGENT.md error: {}", err)))?; + let trimmed = strip_comments_and_whitespace(&content); + if !trimmed.is_empty() { + fragments.push(trimmed.to_string()); + } + } + PromptSource::AutoGenerated(path) => { + if path.exists() { + let content = fs::read_to_string(path) + .map_err(|err| AgentError::Other(format!("read MEMORY_SUMMARY.md error: {}", err)))?; + if !content.trim().is_empty() { + fragments.push(content.trim().to_string()); + } + } + } } } if fragments.is_empty() { - return Ok(None); + Ok(None) + } else { + Ok(Some(fragments.join("\n\n"))) } - - Ok(Some(fragments.join("\n\n"))) } -fn read_prompt_fragment(source: &PromptSource) -> Result, AgentError> { - ensure_parent_dir(&source.path)?; +/// 去除 HTML 注释和空白行,检查是否还有有效内容 +fn strip_comments_and_whitespace(content: &str) -> String { + // 去除 HTML 注释 + let re = regex::Regex::new(r"").unwrap(); + let without_comments = re.replace_all(content, ""); - if !source.path.exists() { - if let Some(default_content) = source.default_content { - write_prompt_file(&source.path, default_content)?; - } else { - return Ok(None); - } - } - - let content = fs::read_to_string(&source.path) - .map_err(|err| AgentError::Other(format!("read prompt file error: {}", err)))?; - let trimmed = content.trim(); - if trimmed.is_empty() { - return Ok(None); - } - - Ok(Some(trimmed.to_string())) + // 去除空白行后检查是否还有有效内容 + without_comments + .lines() + .filter(|line| !line.trim().is_empty()) + .collect::>() + .join("\n") } fn persist_memory_summary(path: &Path, markdown_body: &str) -> Result<(), AgentError> { @@ -126,23 +156,121 @@ mod tests { let agent_path = temp.path().join("AGENT.md"); let memory_path = temp.path().join("MEMORY_SUMMARY.md"); - write_prompt_file(&agent_path, "# Agent\n静态规则").unwrap(); - write_prompt_file(&memory_path, "## 用户记忆摘要\n- 偏好简洁").unwrap(); + // 创建用户自定义内容 + fs::write(&agent_path, "# Agent\n静态规则\n").unwrap(); + // 创建记忆摘要 + fs::write(&memory_path, "## 用户记忆摘要\n- 偏好简洁\n").unwrap(); let prompt = load_prompt_from_sources(&[ - PromptSource { + PromptSource::Builtin(DEFAULT_AGENT_PROMPT), + PromptSource::UserCustom { path: agent_path.clone(), - default_content: Some(DEFAULT_AGENT_PROMPT), - }, - PromptSource { - path: memory_path.clone(), - default_content: None, + template: AGENT_MD_TEMPLATE, }, + PromptSource::AutoGenerated(memory_path.clone()), ]) .unwrap() .unwrap(); - assert_eq!(prompt, "# Agent\n静态规则\n\n## 用户记忆摘要\n- 偏好简洁"); + // 验证三部分都包含:系统默认 + 用户自定义 + 记忆摘要 + assert!(prompt.contains(DEFAULT_AGENT_PROMPT)); + assert!(prompt.contains("# Agent\n静态规则")); + assert!(prompt.contains("## 用户记忆摘要")); + // 验证顺序 + let parts: Vec<_> = prompt.split("\n\n").collect(); + assert!(parts[0].contains("# PicoBot 代理配置")); // 系统默认开头 + } + + #[test] + fn test_load_prompt_from_sources_skips_empty_user_custom() { + let temp = tempdir().unwrap(); + let agent_path = temp.path().join("AGENT.md"); + let memory_path = temp.path().join("MEMORY_SUMMARY.md"); + + // AGENT.md 为空(只有注释和空白) + fs::write(&agent_path, "\n\n \n").unwrap(); + // 创建记忆摘要 + fs::write(&memory_path, "## 用户记忆摘要\n").unwrap(); + + let prompt = load_prompt_from_sources(&[ + PromptSource::Builtin(DEFAULT_AGENT_PROMPT), + PromptSource::UserCustom { + path: agent_path.clone(), + template: AGENT_MD_TEMPLATE, + }, + PromptSource::AutoGenerated(memory_path.clone()), + ]) + .unwrap() + .unwrap(); + + // 只包含系统默认和记忆摘要,不包含空的 AGENT.md + assert!(prompt.contains(DEFAULT_AGENT_PROMPT)); + assert!(prompt.contains("## 用户记忆摘要")); + assert!(!prompt.contains("")); + } + + #[test] + fn test_load_prompt_from_sources_creates_template_for_missing_agent_md() { + let temp = tempdir().unwrap(); + let agent_path = temp.path().join("AGENT.md"); + let memory_path = temp.path().join("MEMORY_SUMMARY.md"); + + // AGENT.md 不存在 + assert!(!agent_path.exists()); + + let prompt = load_prompt_from_sources(&[ + PromptSource::Builtin(DEFAULT_AGENT_PROMPT), + PromptSource::UserCustom { + path: agent_path.clone(), + template: AGENT_MD_TEMPLATE, + }, + PromptSource::AutoGenerated(memory_path.clone()), + ]) + .unwrap() + .unwrap(); + + // AGENT.md 应该被创建 + assert!(agent_path.exists()); + // AGENT.md 应该包含模板内容(在注释中) + let agent_content = fs::read_to_string(&agent_path).unwrap(); + assert!(agent_content.contains("# 自定义 Agent 配置")); + // 因为模板全是注释,去除后为空,所以不会注入到最终提示词 + assert!(!prompt.contains("自定义 Agent 配置")); + // 但系统默认提示词始终存在 + assert!(prompt.contains(DEFAULT_AGENT_PROMPT)); + + // 测试添加实际内容后会注入 + fs::write(&agent_path, "# 我的自定义配置\n\n我是 Rust 开发者\n").unwrap(); + let prompt_with_custom = load_prompt_from_sources(&[ + PromptSource::Builtin(DEFAULT_AGENT_PROMPT), + PromptSource::UserCustom { + path: agent_path.clone(), + template: AGENT_MD_TEMPLATE, + }, + PromptSource::AutoGenerated(memory_path.clone()), + ]) + .unwrap() + .unwrap(); + // 现在应该包含自定义配置 + assert!(prompt_with_custom.contains("# 我的自定义配置")); + assert!(prompt_with_custom.contains("我是 Rust 开发者")); + assert!(prompt_with_custom.contains(DEFAULT_AGENT_PROMPT)); + + // 测试清空 AGENT.md 后不会注入 + fs::write(&agent_path, "\n\n \n").unwrap(); + let prompt_empty = load_prompt_from_sources(&[ + PromptSource::Builtin(DEFAULT_AGENT_PROMPT), + PromptSource::UserCustom { + path: agent_path.clone(), + template: AGENT_MD_TEMPLATE, + }, + PromptSource::AutoGenerated(memory_path.clone()), + ]) + .unwrap() + .unwrap(); + // 清空后(只有注释)不会包含自定义配置 + assert!(!prompt_empty.contains("我的自定义配置")); + assert!(prompt_empty.contains(DEFAULT_AGENT_PROMPT)); } #[test] @@ -151,63 +279,47 @@ mod tests { let agent_path = temp.path().join("AGENT.md"); let memory_path = temp.path().join("MEMORY_SUMMARY.md"); - write_prompt_file(&agent_path, "# Agent\n静态规则").unwrap(); + // 创建用户自定义内容 + fs::write(&agent_path, "# Agent\n静态规则\n").unwrap(); + // MEMORY_SUMMARY.md 不存在 let prompt = load_prompt_from_sources(&[ - PromptSource { + PromptSource::Builtin(DEFAULT_AGENT_PROMPT), + PromptSource::UserCustom { path: agent_path.clone(), - default_content: Some(DEFAULT_AGENT_PROMPT), - }, - PromptSource { - path: memory_path.clone(), - default_content: None, + template: AGENT_MD_TEMPLATE, }, + PromptSource::AutoGenerated(memory_path.clone()), ]) .unwrap() .unwrap(); - assert_eq!(prompt, "# Agent\n静态规则"); - assert!(!memory_path.exists()); + // 包含系统默认和用户自定义,不包含记忆摘要 + assert!(prompt.contains(DEFAULT_AGENT_PROMPT)); + assert!(prompt.contains("# Agent\n静态规则")); + assert!(!prompt.contains("MEMORY_SUMMARY")); } #[test] - fn test_load_prompt_from_sources_creates_default_agent_prompt() { - let temp = tempdir().unwrap(); - let agent_path = temp.path().join("AGENT.md"); - - let prompt = load_prompt_from_sources(&[PromptSource { - path: agent_path.clone(), - default_content: Some("# Default Agent\n规则"), - }]) - .unwrap() - .unwrap(); - - assert_eq!(prompt, "# Default Agent\n规则"); - assert_eq!(fs::read_to_string(&agent_path).unwrap(), "# Default Agent\n规则\n"); + fn test_strip_comments_and_whitespace_removes_html_comments() { + let content = "\n实际内容\n"; + let result = strip_comments_and_whitespace(content); + assert!(!result.contains("\n\n\n \n"; + let result = strip_comments_and_whitespace(content); + assert!(result.is_empty()); } #[test]