重构: 后端 df-ai/commands 拆分+df-nodes/workflow 改造+P0 bug 修复
- df-ai: context 历史中毒三档自愈 sanitize_messages(AC3)+anthropic_compat tool_use_id None 跳过(AC1/AC2)+删 router/stream 死码
- df-core: events 加 select_type+decisions 多选审批契约(F-260615-01)
- df-execute: shell run_command 工具复用(F-260615-05)
- df-nodes: human_node 多选校验+2 端到端测(F-01)+取消跳 set_failed(B-03b-R1/R2/R8)
- df-workflow: executor/dag/state cancel 闭环(B-06/07/03a/b)+provider approve options(R-PD-5)
- df-storage: find_path_conflict 抽公共(R-PD-11)+COLS 常量断言
- df-ideas: 删 IdeaPromoter/PromotionPolicy 死码(R-PD-14)
- src-tauri/commands/ai: secret keyring 迁移(FR-S1/R-PD-4)+GeneratingGuard RAII+disarm(B-09/26)+newConversation 软复位(B-10)+stream 心跳/stop select/空回复判错(B-02/04/05/15)+run_command(F-05)+mask audit(AR-3)
- src-tauri/commands/{project,task,workflow,mod,lib,state}: task detail IPC(F-02)+approve decisions+task list 联动(B-29)
- Cargo.lock+Cargo.toml 依赖同步
This commit is contained in:
@@ -16,6 +16,39 @@ use crate::models::{
|
||||
TaskRecord, WorkflowRecord,
|
||||
};
|
||||
|
||||
/// 规范化路径用于比较:canonicalize 解析绝对规范路径(失败降级),
|
||||
/// 统一正斜杠 + 小写。与 `df_project::scan::normalize_path` **同算法镜像**(df-storage
|
||||
/// 不依赖 df-project,故独立实现;改动须同步)。防 `C:\a\b` vs `C:/a/b/` 绕过。
|
||||
fn normalize_stored_path(p: &str) -> String {
|
||||
match std::path::Path::new(p).canonicalize() {
|
||||
Ok(abs) => abs.to_string_lossy().replace('\\', "/").to_lowercase(),
|
||||
Err(_) => p
|
||||
.trim_end_matches(['\\', '/'])
|
||||
.replace('\\', "/")
|
||||
.to_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 知识库 SELECT 列清单(防 COLS 漂移)
|
||||
// ============================================================
|
||||
|
||||
/// `knowledges` 表对应 `KnowledgeRecord` 14 个字段的列名(顺序与结构体一致)。
|
||||
///
|
||||
/// 多处 `search`/`search_vector` 内联 COLS 串的 DRY 收口(CR-260615-03):集中一处定义,
|
||||
/// 配合下方 `KNOWLEDGE_COL_COUNT` 断言,任一处加列漏改会被测试 `test_knowledge_cols_matches_record`
|
||||
/// 立即捕获(`knowledge_from_row` 按 name 取列,SELECT 漏列会运行时 rusqlite 报错,故提前断言)。
|
||||
const KNOWLEDGE_COLS: &str = "id,kind,title,content,tags,status,confidence,reuse_count,verified,source_project,source_ref,reasoning,created_at,updated_at";
|
||||
/// `KnowledgeRecord` 字段数(与上面列清单的逗号分隔项数一致,被测试断言)。
|
||||
/// 仅测试期消费(列漂移断言);保留为非 `cfg(test)` 以便测试外的阅读者一眼看到字段数。
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
const KNOWLEDGE_COL_COUNT: usize = 14;
|
||||
/// `search_vector` 用的列清单:KNOWLEDGE_COLS + embedding(余弦计算用,不入 KnowledgeRecord)。
|
||||
const KNOWLEDGE_COLS_WITH_EMBEDDING: &str = concat!(
|
||||
"id,kind,title,content,tags,status,confidence,reuse_count,verified,",
|
||||
"source_project,source_ref,reasoning,created_at,updated_at,embedding"
|
||||
);
|
||||
|
||||
// ============================================================
|
||||
// 辅助宏 — 消除 6 个 Repo 的重复样板
|
||||
// ============================================================
|
||||
@@ -355,11 +388,8 @@ pub fn is_allowed_column(table: &str, field: &str) -> bool {
|
||||
// ============================================================
|
||||
|
||||
fn now_millis_str() -> String {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis()
|
||||
.to_string()
|
||||
// 转发 df_core::now_millis(DRY:时间获取统一入口在 df-core,避免本 crate 与 commands 层各写一份 SystemTime 调用)
|
||||
df_core::now_millis().to_string()
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
@@ -617,6 +647,27 @@ impl ProjectRepo {
|
||||
.map_err(|e| Error::Storage(e.to_string()))?
|
||||
}
|
||||
|
||||
/// 查找已绑定该规范化路径的项目(排除 exclude_id 自身)。无冲突返回 None。
|
||||
///
|
||||
/// DRY(R-PD-11):统一 project.rs::find_binding_conflict 与 tool_registry.rs::bind_dir_to_project
|
||||
/// 的「列项目→排除自身→按规范化路径比较」逻辑。本 crate 不依赖 df-project,故 norm_path 须由
|
||||
/// 调用方先经 `df_project::scan::normalize_path`(内含 canonicalize,防 `C:\a\b` vs `C:/a/b/` 绕过)。
|
||||
pub async fn find_path_conflict(
|
||||
&self,
|
||||
norm_path: &str,
|
||||
exclude_id: Option<&str>,
|
||||
) -> Result<Option<ProjectRecord>> {
|
||||
let projects = self.list_active().await?;
|
||||
Ok(projects.into_iter().find(|p| {
|
||||
let excluded = exclude_id.is_some_and(|eid| p.id == eid);
|
||||
!excluded && p.path.as_ref().is_some_and(|pp| {
|
||||
// 路径规范化由调用方完成目标侧;这里为已存项目路径补规范化
|
||||
// (历史路径写法可能不规范,统一规范化比较避免误判/漏判)
|
||||
normalize_stored_path(pp) == norm_path
|
||||
})
|
||||
}))
|
||||
}
|
||||
|
||||
/// 列出回收站(deleted_at IS NOT NULL),按更新时间(≈删除时间)降序
|
||||
pub async fn list_deleted(&self) -> Result<Vec<ProjectRecord>> {
|
||||
let conn = self.conn.clone();
|
||||
@@ -1068,10 +1119,9 @@ impl KnowledgeRepo {
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
let mut results = Vec::new();
|
||||
const COLS: &str = "id,kind,title,content,tags,status,confidence,reuse_count,verified,source_project,source_ref,reasoning,created_at,updated_at";
|
||||
if let Some(k) = &kind {
|
||||
let mut stmt = guard
|
||||
.prepare(&format!("SELECT {COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 OR content LIKE ?2) AND kind = ?3 ORDER BY reuse_count DESC LIMIT ?4"))
|
||||
.prepare(&format!("SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 OR content LIKE ?2) AND kind = ?3 ORDER BY reuse_count DESC LIMIT ?4"))
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let rows = stmt
|
||||
.query_map(params![pattern, pattern, k, limit_i], |row| knowledge_from_row(row))
|
||||
@@ -1081,7 +1131,7 @@ impl KnowledgeRepo {
|
||||
}
|
||||
} else {
|
||||
let mut stmt = guard
|
||||
.prepare(&format!("SELECT {COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 OR content LIKE ?2) ORDER BY reuse_count DESC LIMIT ?3"))
|
||||
.prepare(&format!("SELECT {KNOWLEDGE_COLS} FROM knowledges WHERE status = 'published' AND (title LIKE ?1 OR content LIKE ?2) ORDER BY reuse_count DESC LIMIT ?3"))
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let rows = stmt
|
||||
.query_map(params![pattern, pattern, limit_i], |row| knowledge_from_row(row))
|
||||
@@ -1190,12 +1240,10 @@ impl KnowledgeRepo {
|
||||
let query_vec = query_vec.to_vec();
|
||||
tokio::task::spawn_blocking(move || {
|
||||
let guard = conn.blocking_lock();
|
||||
// 显式列: 14 个 KnowledgeRecord 字段 + embedding(余弦计算用)
|
||||
const COLS: &str = "id,kind,title,content,tags,status,confidence,reuse_count,verified,\
|
||||
source_project,source_ref,reasoning,created_at,updated_at,embedding";
|
||||
// 显式列: 14 个 KnowledgeRecord 字段 + embedding(余弦计算用,不入 KnowledgeRecord)
|
||||
let mut stmt = guard
|
||||
.prepare(&format!(
|
||||
"SELECT {COLS} FROM knowledges WHERE status = 'published' AND embedding IS NOT NULL"
|
||||
"SELECT {KNOWLEDGE_COLS_WITH_EMBEDDING} FROM knowledges WHERE status = 'published' AND embedding IS NOT NULL"
|
||||
))
|
||||
.map_err(|e| Error::Storage(e.to_string()))?;
|
||||
let rows = stmt
|
||||
@@ -1410,6 +1458,33 @@ mod tests {
|
||||
use crate::db::Database;
|
||||
use crate::models::KnowledgeRecord;
|
||||
|
||||
// ---------- COLS 漂移防护(CR-260615-03) ----------
|
||||
|
||||
/// KNOWLEDGE_COLS 列数须等于 KNOWLEDGE_COL_COUNT(任一处漂移:加列漏改 / 串错位 → 立即失败)。
|
||||
/// `knowledge_from_row` 按 name 取列,SELECT 漏列会在运行时被 rusqlite 报错;此断言提前到测试期捕获。
|
||||
#[test]
|
||||
fn test_knowledge_cols_matches_record() {
|
||||
let count = KNOWLEDGE_COLS.split(',').count();
|
||||
assert_eq!(
|
||||
count, KNOWLEDGE_COL_COUNT,
|
||||
"KNOWLEDGE_COLS({count}列) ≠ KNOWLEDGE_COL_COUNT({KNOWLEDGE_COL_COUNT}); \
|
||||
修改一处须同步另一处"
|
||||
);
|
||||
// search_vector 多一列 embedding
|
||||
let count_with_emb = KNOWLEDGE_COLS_WITH_EMBEDDING.split(',').count();
|
||||
assert_eq!(
|
||||
count_with_emb,
|
||||
KNOWLEDGE_COL_COUNT + 1,
|
||||
"KNOWLEDGE_COLS_WITH_EMBEDDING({count_with_emb}列) ≠ KNOWLEDGE_COL_COUNT+1({}); \
|
||||
embedding 列应单独追加",
|
||||
KNOWLEDGE_COL_COUNT + 1
|
||||
);
|
||||
// 每个列名须能被 split 出来(防末尾多逗号 / 空段)
|
||||
for col in KNOWLEDGE_COLS.split(',') {
|
||||
assert!(!col.is_empty(), "KNOWLEDGE_COLS 含空列名段");
|
||||
}
|
||||
}
|
||||
|
||||
// ---------- 向量纯函数 ----------
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user