PicoBot/src/gateway/prompt.rs
oudecheng cda14360af chore: 建立工程化基线(rustfmt + clippy + CI + eslint + prettier)
配置:
- rustfmt.toml: 固化 max_width=100 / 4 空格缩进,cargo fmt 全量格式化
- Cargo.toml: 配置 [lints.rust] 与 [lints.clippy] 渐进式规则
- .github/workflows/ci.yml: Rust(fmt+clippy+test) + 前端(eslint+tsc+test) 双平台 CI
- Makefile: 新增 check/fmt/fix 目标,clippy 对齐 --all-targets --all-features
- web: eslint flat config + prettier 配置 + package.json 脚本与依赖
- src/main.rs: loop→while 修复 clippy::never_loop

对抗性审查发现并修复:
- eslint 缺 caughtErrorsIgnorePattern 导致 catch(_) 误报为 error
- 前端 lint 未接入 CI,现已补上 Lint 步骤
- Makefile 与 CI 的 clippy flags 不一致,已对齐
2026-08-03 23:24:02 +08:00

348 lines
12 KiB
Rust

use std::fs;
use std::path::{Path, PathBuf};
use crate::agent::AgentError;
use crate::config::LLMProviderConfig;
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)]
enum PromptSource {
/// 内置内容 - 系统默认,始终注入,不可修改
Builtin(&'static str),
/// 用户自定义 - 自动创建空白文件,可选注入
UserCustom {
path: PathBuf,
template: &'static str,
},
/// 自动生成 - 由系统维护,可选注入
AutoGenerated(PathBuf),
}
pub(crate) fn load_agent_prompt() -> Result<Option<String>, AgentError> {
load_prompt_from_sources(&prompt_sources()?)
}
pub(crate) fn upsert_managed_agent_memory_summary(markdown_body: &str) -> Result<(), AgentError> {
persist_memory_summary(&memory_summary_path()?, markdown_body)
}
fn prompt_sources() -> Result<Vec<PromptSource>, AgentError> {
Ok(vec![
// 1. 系统默认 - 始终注入
PromptSource::Builtin(DEFAULT_AGENT_PROMPT),
// 2. 用户自定义 - 自动创建空白模板,可选注入
PromptSource::UserCustom {
path: agent_prompt_path()?,
template: AGENT_MD_TEMPLATE,
},
// 3. 记忆摘要 - 可选
PromptSource::AutoGenerated(memory_summary_path()?),
])
}
fn load_prompt_from_sources(sources: &[PromptSource]) -> Result<Option<String>, AgentError> {
let mut fragments = Vec::new();
for source in sources {
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))
})?;
let without_comments = strip_comments_and_whitespace(&content);
if !without_comments.is_empty() {
fragments.push(without_comments);
}
}
}
}
}
if fragments.is_empty() {
Ok(None)
} else {
Ok(Some(fragments.join("\n\n")))
}
}
/// 去除 HTML 注释和空白行,检查是否还有有效内容
fn strip_comments_and_whitespace(content: &str) -> String {
// 去除 HTML 注释 <!-- -->
let re = regex::Regex::new(r"<!--[\s\S]*?-->").unwrap();
let without_comments = re.replace_all(content, "");
// 去除空白行后检查是否还有有效内容
without_comments
.lines()
.filter(|line| !line.trim().is_empty())
.collect::<Vec<_>>()
.join("\n")
}
/// 生成系统环境信息提示词
/// 使用 agent 模块的共享实现
pub(crate) fn generate_system_environment_prompt(config: &LLMProviderConfig) -> String {
crate::agent::generate_system_env_prompt(config)
}
fn persist_memory_summary(path: &Path, markdown_body: &str) -> Result<(), AgentError> {
let trimmed = markdown_body.trim();
if trimmed.is_empty() {
return Ok(());
}
write_prompt_file(path, trimmed)
}
fn write_prompt_file(path: &Path, content: &str) -> Result<(), AgentError> {
ensure_parent_dir(path)?;
let normalized = content.trim_end().to_string() + "\n";
let temp_path = path.with_extension("md.tmp");
fs::write(&temp_path, normalized)
.map_err(|err| AgentError::Other(format!("write prompt temp file error: {}", err)))?;
// 使用平台抽象的原子重命名
atomic_rename(&temp_path, path)
.map_err(|err| AgentError::Other(format!("replace prompt file error: {}", err)))?;
Ok(())
}
fn ensure_parent_dir(path: &Path) -> Result<(), AgentError> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|err| AgentError::Other(format!("create agent prompt dir error: {}", err)))?;
}
Ok(())
}
fn agent_prompt_path() -> Result<PathBuf, AgentError> {
Ok(agent_dir_path()?.join("AGENT.md"))
}
fn memory_summary_path() -> Result<PathBuf, AgentError> {
Ok(agent_dir_path()?.join("MEMORY_SUMMARY.md"))
}
fn agent_dir_path() -> Result<PathBuf, AgentError> {
let home = dirs::home_dir()
.ok_or_else(|| AgentError::Other("home directory not found".to_string()))?;
Ok(home.join(".picobot").join("agent"))
}
#[cfg(test)]
mod tests {
use super::*;
use tempfile::tempdir;
#[test]
fn test_load_prompt_from_sources_aggregates_multiple_fragments_in_order() {
let temp = tempdir().unwrap();
let agent_path = temp.path().join("AGENT.md");
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
// 创建用户自定义内容
fs::write(&agent_path, "# Agent\n静态规则\n").unwrap();
// 创建记忆摘要
fs::write(&memory_path, "## 用户记忆摘要\n- 偏好简洁\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();
// 验证三部分都包含:系统默认 + 用户自定义 + 记忆摘要
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]
fn test_load_prompt_from_sources_ignores_missing_optional_source() {
let temp = tempdir().unwrap();
let agent_path = temp.path().join("AGENT.md");
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
// 创建用户自定义内容
fs::write(&agent_path, "# Agent\n静态规则\n").unwrap();
// MEMORY_SUMMARY.md 不存在
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();
// 包含系统默认和用户自定义,不包含记忆摘要
assert!(prompt.contains(DEFAULT_AGENT_PROMPT));
assert!(prompt.contains("# Agent\n静态规则"));
assert!(!prompt.contains("MEMORY_SUMMARY"));
}
#[test]
fn test_strip_comments_and_whitespace_removes_html_comments() {
let content = "<!-- 注释 -->\n实际内容\n<!-- 另一个注释 -->";
let result = strip_comments_and_whitespace(content);
assert!(!result.contains("<!--"));
assert!(result.contains("实际内容"));
}
#[test]
fn test_strip_comments_and_whitespace_removes_empty_lines() {
let content = "\n\n实际内容\n\n \n\n另一行\n\n";
let result = strip_comments_and_whitespace(content);
assert_eq!(result, "实际内容\n另一行");
}
#[test]
fn test_strip_comments_and_whitespace_returns_empty_for_only_comments() {
let content = "<!-- 注释1 -->\n<!-- 注释2 -->\n\n \n";
let result = strip_comments_and_whitespace(content);
assert!(result.is_empty());
}
#[test]
fn test_upsert_managed_agent_memory_summary_writes_trimmed_markdown() {
let temp = tempdir().unwrap();
let memory_path = temp.path().join("MEMORY_SUMMARY.md");
persist_memory_summary(&memory_path, "\n## 用户记忆摘要\n- 偏好简洁\n\n").unwrap();
assert_eq!(
fs::read_to_string(&memory_path).unwrap(),
"## 用户记忆摘要\n- 偏好简洁\n"
);
}
}