优化: 项目管理阻塞去重 + normalize_path/白名单抽公共
- collect_sample/detect_stack 三处包 spawn_blocking 防 async 阻塞 + bind_directory 补漏一处 - normalize_path 抽公共到 df-project/scan(删 project.rs 本地 + tool_registry inline 闭包) - tool_registry update_project 复用 crud is_allowed_column(删硬编码白名单,与 CRUD 同源) Sprint 20 审查 ③④⑤。todo 记 A/B 验证发现的 2 项非阻断(T-09 idea 补偿删级联 / T-10 normalize_path 同步)
This commit is contained in:
@@ -133,7 +133,10 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
||||
let id = args["id"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 id"))?;
|
||||
let field = args["field"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 field"))?;
|
||||
let value = args["value"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 value"))?;
|
||||
match field { "name" | "status" | "description" | "path" | "stack" => {}, _ => anyhow::bail!("不允许更新字段 '{}'", field) }
|
||||
// 复用 df-storage CRUD 白名单(按表隔离),与 update_field 校验同源
|
||||
if !df_storage::crud::is_allowed_column("projects", field) {
|
||||
anyhow::bail!("不允许更新字段 '{}'", field);
|
||||
}
|
||||
let repo = df_storage::crud::ProjectRepo::new(&db);
|
||||
repo.update_field(id, field, value).await?;
|
||||
Ok(serde_json::json!({ "id": id, "field": field, "updated": true }))
|
||||
@@ -176,26 +179,23 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
|
||||
anyhow::bail!("目录不存在: {path}");
|
||||
}
|
||||
let repo = df_storage::crud::ProjectRepo::new(&db);
|
||||
// 防重复:canonicalize 规范化比较,防路径写法差异绕过
|
||||
let normalize = |s: &str| -> String {
|
||||
std::path::Path::new(s)
|
||||
.canonicalize()
|
||||
.map(|a| a.to_string_lossy().replace('\\', "/").to_lowercase())
|
||||
.unwrap_or_else(|_| s.trim_end_matches(['\\', '/']).replace('\\', "/").to_lowercase())
|
||||
};
|
||||
let target = normalize(path);
|
||||
// 防重复:canonicalize 规范化比较,防路径写法差异绕过(复用 df-project 公共 normalize_path)
|
||||
let target = df_project::scan::normalize_path(path);
|
||||
let projects = repo.list_active().await?;
|
||||
for proj in &projects {
|
||||
if proj.id != id {
|
||||
if let Some(pp) = &proj.path {
|
||||
if normalize(pp) == target {
|
||||
if df_project::scan::normalize_path(pp) == target {
|
||||
anyhow::bail!("目录已被项目「{}」绑定", proj.name);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// 探测技术栈
|
||||
let stack = df_project::scan::detect_stack(dir)?;
|
||||
// 探测技术栈(spawn_blocking: detect_stack 内含多次同步 fs IO,避免阻塞 tokio runtime)
|
||||
let dir_buf = std::path::PathBuf::from(path);
|
||||
let stack = tokio::task::spawn_blocking(move || df_project::scan::detect_stack(&dir_buf))
|
||||
.await
|
||||
.map_err(|e| anyhow::anyhow!("技术栈探测任务失败: {e}"))??;
|
||||
let stack_json = serde_json::to_string(&stack)?;
|
||||
repo.update_field(id, "path", path).await?;
|
||||
repo.update_field(id, "stack", &stack_json).await?;
|
||||
|
||||
@@ -8,7 +8,7 @@ use tauri::State;
|
||||
use df_ai::build_provider;
|
||||
use df_ai::provider::{ChatMessage, CompletionRequest};
|
||||
use df_core::types::new_id;
|
||||
use df_project::scan::{collect_sample, detect_stack};
|
||||
use df_project::scan::{collect_sample, detect_stack, normalize_path};
|
||||
use df_storage::models::ProjectRecord;
|
||||
|
||||
use crate::state::AppState;
|
||||
@@ -53,11 +53,15 @@ pub async fn create_project(
|
||||
if let Some(conflict) = find_binding_conflict(&state, p, None).await? {
|
||||
return Err(format!("目录已被项目「{}」绑定", conflict.name));
|
||||
}
|
||||
// stack 优先用入参,否则自动探测
|
||||
// stack 优先用入参,否则自动探测(spawn_blocking 防 IO 阻塞 tokio runtime)
|
||||
let stack_json = match input.stack.as_deref().map(str::trim).filter(|s| !s.is_empty()) {
|
||||
Some(s) => s.to_string(),
|
||||
None => {
|
||||
let detected = detect_stack(Path::new(p)).map_err(|e| e.to_string())?;
|
||||
let root = std::path::PathBuf::from(p);
|
||||
let detected = tokio::task::spawn_blocking(move || detect_stack(&root))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map_err(|e| e.to_string())?;
|
||||
serde_json::to_string(&detected).map_err(|e| e.to_string())?
|
||||
}
|
||||
};
|
||||
@@ -148,19 +152,6 @@ pub async fn purge_project(state: State<'_, AppState>, id: String) -> Result<boo
|
||||
// 项目目录绑定 — 探测 / 防重复 / 重定位 / 有效性检查
|
||||
// ============================================================
|
||||
|
||||
/// 规范化路径用于比较:canonicalize 解析绝对规范路径(失败降级),
|
||||
/// 统一正斜杠 + 小写。防 `C:\a\b` vs `C:/a/b/` 绕过重复检查。
|
||||
/// 注:仅用于比较,存库保留用户输入的原始可读路径。
|
||||
fn normalize_path(p: &str) -> String {
|
||||
match Path::new(p).canonicalize() {
|
||||
Ok(abs) => abs.to_string_lossy().replace('\\', "/").to_lowercase(),
|
||||
Err(_) => p
|
||||
.trim_end_matches(['\\', '/'])
|
||||
.replace('\\', "/")
|
||||
.to_lowercase(),
|
||||
}
|
||||
}
|
||||
|
||||
/// 查找已绑定该目录的项目(排除 exclude_id 自身)。无冲突返回 None。
|
||||
async fn find_binding_conflict(
|
||||
state: &AppState,
|
||||
@@ -208,8 +199,12 @@ pub async fn relocate_project_path(
|
||||
if let Some(conflict) = find_binding_conflict(&state, &new_path, Some(&id)).await? {
|
||||
return Err(format!("目录已被项目「{}」绑定", conflict.name));
|
||||
}
|
||||
// 重探测技术栈
|
||||
let stack = detect_stack(Path::new(&new_path)).map_err(|e| e.to_string())?;
|
||||
// 重探测技术栈(spawn_blocking 防 IO 阻塞 tokio runtime)
|
||||
let root = std::path::PathBuf::from(&new_path);
|
||||
let stack = tokio::task::spawn_blocking(move || detect_stack(&root))
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map_err(|e| e.to_string())?;
|
||||
let stack_json = serde_json::to_string(&stack).map_err(|e| e.to_string())?;
|
||||
state
|
||||
.projects
|
||||
@@ -266,9 +261,16 @@ pub async fn scan_project_with_ai(
|
||||
return Err(format!("目录不存在: {path}"));
|
||||
}
|
||||
|
||||
// 1. 规则探测(兜底)+ 采样(纯 IO 轻量,直接调)
|
||||
let rule_stack = detect_stack(root).map_err(|e| e.to_string())?;
|
||||
let sample = collect_sample(root).map_err(|e| e.to_string())?;
|
||||
// 1. 规则探测(兜底)+ 采样(纯 IO 轻量,spawn_blocking 防 IO 阻塞 tokio runtime)
|
||||
let scan_root = std::path::PathBuf::from(&path);
|
||||
let (rule_stack, sample) = tokio::task::spawn_blocking(move || {
|
||||
let stack = detect_stack(&scan_root)?;
|
||||
let sample = collect_sample(&scan_root)?;
|
||||
Ok::<_, anyhow::Error>((stack, sample))
|
||||
})
|
||||
.await
|
||||
.map_err(|e| e.to_string())?
|
||||
.map_err(|e| e.to_string())?;
|
||||
|
||||
// 2. 取默认 provider(优先 is_default,否则首个)
|
||||
let providers = state.ai_providers.list_all().await.map_err(|e| e.to_string())?;
|
||||
|
||||
Reference in New Issue
Block a user