新增: 批次工作落地(推进链/评估闭环/事件总线/并发/加固) + 技术债清理 + 文档整理

后端:
- 工作流推进链(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:
2026-06-21 20:51:26 +08:00
parent 330bb7f505
commit bd6a41fe6e
111 changed files with 11932 additions and 1034 deletions

View 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,
})
}
}