新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理
后端: - 工作流推进链(D-03):advance_task/状态机/闸门走 df-nodes Node trait,conditions 条件引擎扩展 - 想法评估闭环:启发式评分+对抗评估,df-ideas/scoring + df-storage/idea_eval_repo + idea 前端打通 - 全局事件数据总线:df-ai/context+context_helpers+augmentation 跨模块解耦 - AI planner/plan_hint/intent:aichat B 路线并行多轮基础 - patch_file 加固(TD-03/04):读改写整体锁防 lost update,expected_hash 合约闭环 - 压缩超时兜底(F-15 卡死根治) - F-09 多会话并发:LlmConcurrency per-conv + streamingGuard 前端守护 + verify 脚本 - 知识注入 DRY/skills/audit 扩展 清理: - aichat 技术债(误报 allow/死导入/过时注释 30 项) - URGENT.md 删除(11 项加急全解决/迁 todo) - 文档整理(todo/待决策/待审查/ARCHITECTURE/INDEX + 总线/技术债审查新文档)
This commit is contained in:
283
src-tauri/src/commands/ai/augmentation/resolvers.rs
Normal file
283
src-tauri/src/commands/ai/augmentation/resolvers.rs
Normal file
@@ -0,0 +1,283 @@
|
||||
//! 四个 MentionResolver 实现(核心设计2)
|
||||
//!
|
||||
//! - [`ProjectResolver`]:持 `Arc<Database>`,经 `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<Database>` 而非 `Arc<AppState>` —— 避免循环依赖(AppState 持
|
||||
//! `Arc<ResolverRegistry>`,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<Database>` 访问 repo(非 AppState,避免循环依赖)。
|
||||
/// Skill resolver 不持 db(调 skills::read_skill_content_stripped 读缓存 + 单文件)。
|
||||
/// 在 [`crate::state::AppState::init`] 与 `build_ai_tool_registry` 同侧调用,产
|
||||
/// `Arc<ResolverRegistry>` 注入 `AppState.resolvers`。
|
||||
pub fn build_resolver_registry(db: Arc<Database>) -> 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<Database>,
|
||||
}
|
||||
|
||||
impl ProjectResolver {
|
||||
pub fn new(db: Arc<Database>) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MentionResolver for ProjectResolver {
|
||||
fn kind(&self) -> &'static str {
|
||||
"project"
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
&self,
|
||||
rf: &MentionRef,
|
||||
loc: ProviderLocality,
|
||||
) -> Result<Augmentation, ResolveError> {
|
||||
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<Database>,
|
||||
}
|
||||
|
||||
impl TaskResolver {
|
||||
pub fn new(db: Arc<Database>) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MentionResolver for TaskResolver {
|
||||
fn kind(&self) -> &'static str {
|
||||
"task"
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
&self,
|
||||
rf: &MentionRef,
|
||||
_loc: ProviderLocality,
|
||||
) -> Result<Augmentation, ResolveError> {
|
||||
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<Database>,
|
||||
}
|
||||
|
||||
impl IdeaResolver {
|
||||
pub fn new(db: Arc<Database>) -> Self {
|
||||
Self { db }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl MentionResolver for IdeaResolver {
|
||||
fn kind(&self) -> &'static str {
|
||||
"idea"
|
||||
}
|
||||
|
||||
async fn resolve(
|
||||
&self,
|
||||
rf: &MentionRef,
|
||||
_loc: ProviderLocality,
|
||||
) -> Result<Augmentation, ResolveError> {
|
||||
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<Augmentation, ResolveError> {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user