重构: AI流式断线保文StreamResult三分支+重试对齐决策a1,推进链落df-nodes

This commit is contained in:
2026-06-16 19:11:19 +08:00
parent 7d5cd4c89a
commit ba7f35552b
30 changed files with 1325 additions and 283 deletions

View File

@@ -205,51 +205,57 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
// ── 只读 (Low) ──
registry.register(
"list_projects", "列出所有项目,返回项目列表ID、名称、状态、描述。最多返回 50 条",
df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |_args: serde_json::Value| {
"list_projects", "列出所有项目,支持 offset/limit 分页。返回 items项目列表、total总量、has_more是否有更多页。默认 limit=50",
df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
let db = db.clone();
Box::pin(async move {
let repo = df_storage::crud::ProjectRepo::new(&db);
let mut items = repo.list_active().await?; // list_active 排除回收站(deleted_at),防 LLM 看到已软删项目
let items = repo.list_active().await?; // list_active 排除回收站(deleted_at),防 LLM 看到已软删项目
let total = items.len();
let truncated = total > MAX_LIST_RESULTS;
items.truncate(MAX_LIST_RESULTS);
Ok(serde_json::json!({ "items": items, "truncated": truncated }))
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect();
let has_more = (offset + page_items.len()) < total;
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
})
})},
);
registry.register(
"list_tasks", "列出任务,可按 project_id 筛选。最多返回 50 条",
df_ai::ai_tools::object_schema(vec![("project_id", "string", false)]), RiskLevel::Low,
"list_tasks", "列出任务,可按 project_id 筛选,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50",
df_ai::ai_tools::object_schema(vec![("project_id", "string", false), ("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
let db = db.clone();
Box::pin(async move {
let repo = df_storage::crud::TaskRepo::new(&db);
let mut tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) {
let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) {
repo.query("project_id", pid).await?
} else {
repo.list_all().await?
};
let total = tasks.len();
let truncated = total > MAX_LIST_RESULTS;
tasks.truncate(MAX_LIST_RESULTS);
Ok(serde_json::json!({ "items": tasks, "truncated": truncated }))
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
let page_items: Vec<_> = tasks.into_iter().skip(offset).take(limit).collect();
let has_more = (offset + page_items.len()) < total;
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
})
})},
);
registry.register(
"list_ideas", "列出所有灵感。最多返回 50 条",
df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |_args: serde_json::Value| {
"list_ideas", "列出所有灵感,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50",
df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
let db = db.clone();
Box::pin(async move {
let repo = df_storage::crud::IdeaRepo::new(&db);
let mut items = repo.list_all().await?;
let items = repo.list_all().await?;
let total = items.len();
let truncated = total > MAX_LIST_RESULTS;
items.truncate(MAX_LIST_RESULTS);
Ok(serde_json::json!({ "items": items, "truncated": truncated }))
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect();
let has_more = (offset + page_items.len()) < total;
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
})
})},
);
@@ -456,16 +462,19 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
})},
);
registry.register(
"list_trash", "列出回收站已删除项目(最多返回 50 条)",
df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |_args: serde_json::Value| {
"list_trash", "列出回收站已删除项目,支持 offset/limit 分页。返回 items、total、has_more。默认 limit=50",
df_ai::ai_tools::object_schema(vec![("offset", "integer", false), ("limit", "integer", false)]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
let db = db.clone();
Box::pin(async move {
let repo = df_storage::crud::ProjectRepo::new(&db);
let mut items = repo.list_deleted().await?;
let truncated = items.len() > MAX_LIST_RESULTS;
items.truncate(MAX_LIST_RESULTS);
Ok(serde_json::json!({ "items": items, "truncated": truncated }))
let items = repo.list_deleted().await?;
let total = items.len();
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
let limit = args["limit"].as_u64().unwrap_or(MAX_LIST_RESULTS as u64).min(MAX_LIST_RESULTS as u64) as usize;
let page_items: Vec<_> = items.into_iter().skip(offset).take(limit).collect();
let has_more = (offset + page_items.len()) < total;
Ok(serde_json::json!({ "items": page_items, "total": total, "has_more": has_more }))
})
})},
);
@@ -568,23 +577,27 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
}
anyhow::bail!("读取文件失败: {}", e);
}
// search 模式: 按行枚举收集含 search 子串的行,限 50 条
// search 模式: 按行枚举收集含 search 子串的行,支持 offset/limit 分页
if let Some(search) = args["search"].as_str() {
let search_max = 50usize;
let mut matches_vec: Vec<serde_json::Value> = Vec::with_capacity(search_max);
const SEARCH_MAX: usize = 50;
let search_offset = args["offset"].as_u64().unwrap_or(0) as usize;
let search_limit = args["limit"].as_u64().unwrap_or(SEARCH_MAX as u64).min(SEARCH_MAX as u64) as usize;
// 先收集全部匹配行用于 total 计数,再做 skip/take 分页
let mut all_matches: Vec<serde_json::Value> = Vec::new();
for (idx, line) in content.lines().enumerate() {
if line.contains(search) {
matches_vec.push(serde_json::json!({ "line": idx + 1, "content": line }));
if matches_vec.len() >= search_max { break; }
all_matches.push(serde_json::json!({ "line": idx + 1, "content": line }));
}
}
let total = content.lines().filter(|l| l.contains(search)).count();
let total = all_matches.len();
let page_matches: Vec<_> = all_matches.into_iter().skip(search_offset).take(search_limit).collect();
let has_more = (search_offset + page_matches.len()) < total;
return Ok(serde_json::json!({
"path": path, "size": metadata.len(),
"search": search,
"matches": matches_vec,
"matches": page_matches,
"total": total,
"has_more": total > search_max,
"has_more": has_more,
}));
}
// 默认分页模式: limit 硬上限 2000 行(防 LLM 传超大 limit 读全文件,1MB 限下仍可能数万行)
@@ -1043,8 +1056,8 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
// ── 文件搜索 (Low risk) ──
registry.register(
"search_files", "在指定目录下搜索匹配模式(字符串包含匹配)的文件名,返回路径和大小列表。支持递归搜索,结果限 50 条",
df_ai::ai_tools::object_schema(vec![("path", "string", true), ("pattern", "string", true), ("recursive", "boolean", false)]),
"search_files", "在指定目录下搜索匹配模式(字符串包含匹配)的文件名,支持 offset/limit 分页。返回 results、total、has_more。默认 limit=50",
df_ai::ai_tools::object_schema(vec![("path", "string", true), ("pattern", "string", true), ("recursive", "boolean", false), ("offset", "integer", false), ("limit", "integer", false)]),
RiskLevel::Low,
Box::new(|args: serde_json::Value| Box::pin(async move {
let resolved = resolve_workspace_path(
@@ -1055,14 +1068,49 @@ pub fn build_ai_tool_registry(db: &Arc<Database>) -> AiToolRegistry {
let recursive = args["recursive"].as_bool().unwrap_or(false);
let pattern_lower = pattern.to_lowercase();
const MAX_RESULTS: usize = 50;
let mut results = Vec::new();
let offset = args["offset"].as_u64().unwrap_or(0) as usize;
let limit = args["limit"].as_u64().unwrap_or(MAX_RESULTS as u64).min(MAX_RESULTS as u64) as usize;
// 先收集全部结果用于 total 计数,再做 skip/take 分页
let mut all_results = Vec::new();
let mut total = 0u64;
search_files_recursive(path, &pattern_lower, recursive, 0, 5, MAX_RESULTS, &mut results, &mut total).await?;
let has_more = total as usize > MAX_RESULTS;
Ok(serde_json::json!({ "path": path, "pattern": pattern, "results": results, "total": total, "has_more": has_more }))
// 用较大上限收集全量(分页由内存 skip/take 控制)
search_files_recursive(path, &pattern_lower, recursive, 0, 5, offset + limit, &mut all_results, &mut total).await?;
let page_results: Vec<_> = all_results.into_iter().skip(offset).take(limit).collect();
let has_more = (offset + page_results.len()) < total as usize;
Ok(serde_json::json!({ "path": path, "pattern": pattern, "results": page_results, "total": total, "has_more": has_more }))
})),
);
// ── 探总量工具 (Low risk, F-260616-08) ──
registry.register(
"get_project_count", "获取项目总数(未删除项目),用于分页策略判断。返回 { total: usize }",
df_ai::ai_tools::object_schema(vec![]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |_args: serde_json::Value| {
let db = db.clone();
Box::pin(async move {
let repo = df_storage::crud::ProjectRepo::new(&db);
let items = repo.list_active().await?;
Ok(serde_json::json!({ "total": items.len() }))
})
})},
);
registry.register(
"get_task_count", "获取任务总数(未删除任务),用于分页策略判断。返回 { total: usize }",
df_ai::ai_tools::object_schema(vec![("project_id", "string", false)]), RiskLevel::Low,
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
let db = db.clone();
Box::pin(async move {
let repo = df_storage::crud::TaskRepo::new(&db);
let tasks = if let Some(pid) = args.get("project_id").and_then(|v| v.as_str()) {
repo.query("project_id", pid).await?
} else {
repo.list_all().await?
};
Ok(serde_json::json!({ "total": tasks.len() }))
})
})},
);
registry
}