//! 四个 MentionResolver 实现(核心设计2) //! //! - [`ProjectResolver`]:持 `Arc`,经 `ProjectRepo::get_by_id` 取项目记录, //! path 经 [`sanitize_for`] 脱敏(Local 全路径 / Remote basename)。 //! - [`TaskResolver`]:经 `TaskRepo::get_by_id` 拿 task,再 join `project_id` 取 project_name。 //! - [`IdeaResolver`]:经 `IdeaRepo::get_by_id`(IdeaRecord 无 project_id,注入无 path)。 //! - [`SkillResolver`]:调 [`super::super::skills::read_skill_content_stripped`] 取剥 frontmatter 正文。 //! //! Resolver 持 `Arc` 而非 `Arc` —— 避免循环依赖(AppState 持 //! `Arc`,Resolver 若持 AppState 则成环)。Repo 在 resolve 内即时 //! `::new(&db)` 构造(Arc clone 廉价),与 tool_registry.rs 闭包内建 Repo 同模式。 use std::sync::Arc; use async_trait::async_trait; use df_storage::crud::{IdeaRepo, ProjectRepo, TaskRepo}; use df_storage::db::Database; use df_types::augmentation::{Augmentation, MentionRef, ResolveError, SanitizedPath}; use df_types::types::{IdeaStatus, ProjectStatus, TaskStatus}; use crate::commands::ai::augmentation::registry::ResolverRegistry; use crate::commands::ai::augmentation::sanitize::{sanitize_for, ProviderLocality}; use crate::commands::ai::augmentation::MentionResolver; use crate::commands::ai::skills::read_skill_content_stripped; /// 启动期构建 ResolverRegistry,注册四个 resolver(Project/Task/Idea/Skill)。 /// /// resolver 持 `Arc` 访问 repo(非 AppState,避免循环依赖)。 /// Skill resolver 不持 db(调 skills::read_skill_content_stripped 读缓存 + 单文件)。 /// 在 [`crate::state::AppState::init`] 与 `build_ai_tool_registry` 同侧调用,产 /// `Arc` 注入 `AppState.resolvers`。 pub fn build_resolver_registry(db: Arc) -> ResolverRegistry { let mut reg = ResolverRegistry::new(); reg.register(Arc::new(ProjectResolver::new(db.clone()))); reg.register(Arc::new(TaskResolver::new(db.clone()))); reg.register(Arc::new(IdeaResolver::new(db.clone()))); reg.register(Arc::new(SkillResolver::new())); reg } /// @项目 Resolver:取项目记录 + path 脱敏。 /// /// path 脱敏(Local 全路径 / Remote basename)由 `loc` 参数决定,经 [`sanitize_for`] /// 包成 [`SanitizedPath`] newtype 防裸 String 误用。项目未绑定目录(path=None)时 /// Augmentation.path=None(与现状一致,不堵未绑定项目)。 pub struct ProjectResolver { db: Arc, } impl ProjectResolver { pub fn new(db: Arc) -> Self { Self { db } } } #[async_trait] impl MentionResolver for ProjectResolver { fn kind(&self) -> &'static str { "project" } async fn resolve( &self, rf: &MentionRef, loc: ProviderLocality, ) -> Result { let id = match rf { MentionRef::Project { id, .. } => id.clone(), _ => { return Err(ResolveError::KindMismatch { expected: "project".to_string(), actual: rf.kind_str().to_string(), }); } }; let repo = ProjectRepo::new(&self.db); let record = repo .get_by_id(&id) .await .map_err(|e| ResolveError::Skip { kind: "project".to_string(), ref_id: id.clone(), reason: format!("查询失败: {}", e), })? .ok_or_else(|| ResolveError::NotFound { kind: "project".to_string(), ref_id: id.clone(), })?; // status: String → ProjectStatus(未知字符串按 Planning 兜底,不阻断注入) let status = ProjectStatus::from_db_str(&record.status).unwrap_or(ProjectStatus::Planning); // path 脱敏:None(未绑定)保持 None;Some 则按 locality 脱敏包 newtype let path = record .path .as_deref() .filter(|s| !s.is_empty()) .map(|raw| SanitizedPath::new(sanitize_for(raw, loc))); Ok(Augmentation::Project { id: record.id, name: record.name, status, description: record.description, path, }) } } /// @任务 Resolver:取任务记录 + join 项目名(project_name)。 /// /// TaskRepo::get_by_id 拿 task,再经 ProjectRepo::get_by_id(task.project_id) 取项目名; /// project_id 无对应项目(游离任务/项目已删)时 project_name=None(非错误,不阻断注入)。 pub struct TaskResolver { db: Arc, } impl TaskResolver { pub fn new(db: Arc) -> Self { Self { db } } } #[async_trait] impl MentionResolver for TaskResolver { fn kind(&self) -> &'static str { "task" } async fn resolve( &self, rf: &MentionRef, _loc: ProviderLocality, ) -> Result { let (id, _name) = match rf { MentionRef::Task { id, name } => (id.clone(), name.clone()), _ => { return Err(ResolveError::KindMismatch { expected: "task".to_string(), actual: rf.kind_str().to_string(), }); } }; let task_repo = TaskRepo::new(&self.db); let task = task_repo .get_by_id(&id) .await .map_err(|e| ResolveError::Skip { kind: "task".to_string(), ref_id: id.clone(), reason: format!("查询失败: {}", e), })? .ok_or_else(|| ResolveError::NotFound { kind: "task".to_string(), ref_id: id.clone(), })?; let status = TaskStatus::from_db_str(&task.status).unwrap_or(TaskStatus::Todo); // join project_name:project_id 取 ProjectRecord.name;失败/无对应 None(非错误) let project_name = { let project_repo = ProjectRepo::new(&self.db); match project_repo.get_by_id(&task.project_id).await { Ok(Some(p)) => Some(p.name), _ => None, } }; Ok(Augmentation::Task { id: task.id, title: task.title, status, description: task.description, project_name, }) } } /// @灵感 Resolver:取灵感记录(IdeaRecord 无 project_id,注入无 path)。 pub struct IdeaResolver { db: Arc, } impl IdeaResolver { pub fn new(db: Arc) -> Self { Self { db } } } #[async_trait] impl MentionResolver for IdeaResolver { fn kind(&self) -> &'static str { "idea" } async fn resolve( &self, rf: &MentionRef, _loc: ProviderLocality, ) -> Result { let (id, _name) = match rf { MentionRef::Idea { id, name } => (id.clone(), name.clone()), _ => { return Err(ResolveError::KindMismatch { expected: "idea".to_string(), actual: rf.kind_str().to_string(), }); } }; let repo = IdeaRepo::new(&self.db); let record = repo .get_by_id(&id) .await .map_err(|e| ResolveError::Skip { kind: "idea".to_string(), ref_id: id.clone(), reason: format!("查询失败: {}", e), })? .ok_or_else(|| ResolveError::NotFound { kind: "idea".to_string(), ref_id: id.clone(), })?; let status = IdeaStatus::from_db_str(&record.status).unwrap_or(IdeaStatus::Draft); Ok(Augmentation::Idea { id: record.id, title: record.title, status, description: record.description, }) } } /// /技能 Resolver:调 [`read_skill_content_stripped`] 取剥 frontmatter 正文。 /// /// skill 以 name 为唯一键(无全局 id),MentionRef::Skill 仅携带 name。 /// 缓存未命中 / 文件读失败 → NotFound(skill 已卸载或名称错)。 /// source 取缓存中 SkillInfo.source(skill/command/plugin),默认 "skill" 兜底。 pub struct SkillResolver; impl SkillResolver { pub fn new() -> Self { Self } } impl Default for SkillResolver { fn default() -> Self { Self::new() } } #[async_trait] impl MentionResolver for SkillResolver { fn kind(&self) -> &'static str { "skill" } async fn resolve( &self, rf: &MentionRef, _loc: ProviderLocality, ) -> Result { let name = match rf { MentionRef::Skill { name } => name.clone(), _ => { return Err(ResolveError::KindMismatch { expected: "skill".to_string(), actual: rf.kind_str().to_string(), }); } }; // 取剥 frontmatter 正文(注入用);None = 缓存未命中或文件读失败 let body = read_skill_content_stripped(&name).ok_or_else(|| ResolveError::NotFound { kind: "skill".to_string(), ref_id: name.clone(), })?; // source 从缓存取 SkillInfo.source;失败(缓存不一致)默认 "skill" let source = crate::commands::ai::skills::skills_cached() .into_iter() .find(|s| s.name == name) .map(|s| s.source) .unwrap_or_else(|| "skill".to_string()); Ok(Augmentation::Skill { name, source, body, }) } }