修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离

- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条)
- queue 加 conversationId 字段,按会话精准 drain
- regenerate/editMessage 只清本会话排队消息
- newConversation 保留旧会话排队消息
- AiError 只清出错会话的队列项
This commit is contained in:
lxy
2026-07-20 00:19:50 +08:00
parent 42efb31bbf
commit e9e3578d26
59 changed files with 2875 additions and 1330 deletions
+54 -24
View File
@@ -593,7 +593,7 @@ fn register_project_tools(registry: &mut AiToolRegistry, db: &Arc<Database>) {
})},
);
registry.register(
"bind_directory", "为项目绑定代码目录(自动探测技术栈,防重复绑定)",
"bind_directory", "为项目绑定代码目录(自动探测技术栈,防重复绑定)。path 应为**项目根路径**(单仓库=仓库根;多工程 monorepo=工作区根,非单个子工程路径;子工程信息走 list_project_modules/update_project_module 维护)。多次调用会覆盖 path 字段,monorepo 场景勿为每个子工程重复绑定。可选 stack 手动指定技术栈(如 'Go / Gin / Vue3'),不传则自动探测。",
df_ai::ai_tools::object_schema(vec![("id", "string", true), ("path", "string", true), ("stack", "string", false)]),
RiskLevel::Medium,
{ let db = db.clone(); Box::new(move |args: serde_json::Value| {
@@ -1308,22 +1308,27 @@ fn register_task_graph_tools(registry: &mut AiToolRegistry, db: &Arc<Database>)
// Git 只读 AI 工具(工程系统,2026-06-29)
// ============================================================
/// 在指定目录执行 git 命令(10s 超时,返回 stdout)。失败返回空字符串(非崩溃)。
/// 在指定目录执行 git 命令(10s 超时,返回 stdout)。失败/超时返回空字符串(非崩溃)。
///
/// BUG-2026-07-18: 原实现 spawn_blocking 内裸 std::process::Command::output() 无 timeout
/// (注释谎称"10s 超时")。git 在 OneDrive/网盘/挂载盘/lfs/大仓库场景会卡数十秒到无限,
/// spawn_blocking 线程永不返回 → 累计耗尽 tokio blocking 池 → 间接卡死单线程 runtime
/// (与 env_snapshot::probe_version 同型病根)。AI 的 git_status/log/diff 工具在会话内高频触发。
///
/// 改用 tokio::process + tokio::time::timeout(10s) + kill_on_drop:超时 drop 时 child 进程
/// 被 kill,不泄漏线程/进程,对齐 shell.rs execute 同源封装。
async fn exec_git(working_dir: &str, args: &[&str]) -> String {
let dir = working_dir.to_string();
let args_vec: Vec<String> = args.iter().map(|s| s.to_string()).collect();
let result = tokio::task::spawn_blocking(move || {
let mut cmd = std::process::Command::new("git");
cmd.args(&args_vec).current_dir(&dir);
cmd.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null());
match cmd.output() {
Ok(out) => String::from_utf8_lossy(&out.stdout).to_string(),
Err(_) => String::new(),
}
})
.await;
result.unwrap_or_default()
let mut cmd = tokio::process::Command::new("git");
cmd.args(args)
.current_dir(working_dir)
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.kill_on_drop(true);
match tokio::time::timeout(std::time::Duration::from_secs(10), cmd.output()).await {
Ok(Ok(out)) => String::from_utf8_lossy(&out.stdout).to_string(),
Ok(Err(_)) => String::new(),
Err(_elapsed) => String::new(), // 超时:child 被 kill_on_drop 终止
}
}
/// git status --porcelain 解析为结构化文件列表。
@@ -1767,7 +1772,16 @@ fn register_file_tools(
use tokio::fs::File;
use tokio::io::AsyncReadExt;
let mut file = File::open(path).await
.map_err(|e| anyhow::anyhow!("无法访问文件 {}: {}", path, e))?;
.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
anyhow::anyhow!(
"无法访问文件 {}: 路径不存在。建议用 list_directory 先查看目录下的实际文件列表",
path
)
} else {
anyhow::anyhow!("无法访问文件 {}: {}", path, e)
}
})?;
let metadata = file.metadata().await
.map_err(|e| anyhow::anyhow!("读取元数据失败 {}: {}", path, e))?;
if metadata.len() > 1_048_576 {
@@ -2510,11 +2524,25 @@ fn register_file_tools(
let output_mode = args.get("output_mode").and_then(|v| v.as_str()).unwrap_or("content");
let max_results = args.get("max_results").and_then(|v| v.as_u64()).unwrap_or(50).clamp(1, 200) as usize;
// 编译正则:case_insensitive 开 i flag;失败上抛明确错误(非法正则不是业务错,LLM 据此修参)
// 编译正则:case_insensitive 开 i flag;失败尝试自动修复常见问题(未分组 | 用 (?:...) 包裹)
let mut re_builder = regex::RegexBuilder::new(pattern);
re_builder.case_insensitive(case_insensitive);
let re = re_builder.build()
.map_err(|e| anyhow::anyhow!("正则编译失败「{}」: {}", pattern, e))?;
let (re, _auto_fixed) = match re_builder.build() {
Ok(r) => (r, false),
Err(e) => {
// LLM 常传 `a|b|c` 未分组模式(如 `<template|<div`),尝试 (?:...) 包裹后重试
let wrapped = format!("(?:{})", pattern);
let mut fix_builder = regex::RegexBuilder::new(&wrapped);
fix_builder.case_insensitive(case_insensitive);
match fix_builder.build() {
Ok(r) => (r, true),
Err(_) => anyhow::bail!(
"正则编译失败「{}」: {}。提示:多选模式请用 (?:a|b|c) 分组",
pattern, e
),
}
}
};
// glob 过滤器:编译为 regex 单段匹配(* → [^/]*, ? → [^/], 字面其他字符 escape)。
// 仅匹配文件名单段(不含 /),对齐 Claude Code grep glob 语义。
@@ -3341,18 +3369,20 @@ mod tests {
// register_git_tools 3 个)
// Git 写工具(2026-06-29): data 层 31→34(新增 git_commit/git_branch/git_merge,
// register_git_tools 3→6)
// BUG-2026-07-07: 修正 baseline 计数(原写死 41 与实际 48 不符,HEAD 上即失败)。
// 48 = 34 data + 13 file + 1 http。data 34 含 6 知识图谱 + 2 基础设施 + 1 module + 6 git。
assert_eq!(
registry.len(),
41,
"工具总数应为 41(27 data + 13 file + 1 http),实际 {}", registry.len()
48,
"工具总数应为 48(34 data + 13 file + 1 http),实际 {}", registry.len()
);
// 工具名集合基线:防 rename / 漏注册 / 误删除。
// data 层 27 个(持 db):CRUD/状态机/工作流/知识图谱任务关联 + 项目事件流 + 基础设施配置
// data 层 34 个(持 db):CRUD/状态机/工作流/知识图谱任务关联 + 项目事件流 + 基础设施配置 + git 工具
// file 层 13 个(不持 db):命令/读/列/写/改/元/追加/删/移/搜/grep/环境探测/符号解析
// http 层 1 个(不持 db):http_request
let mut expected: Vec<&str> = vec![
// ── data 层 (27) ──
// ── data 层 (34) ──
"list_projects", "list_tasks", "list_ideas",
"update_project", "create_project", "bind_directory",
"create_task", "update_task", "advance_task",