修复: DeepSeek 400 全量扫描 + 队列 per-conv 隔离
- openai_compat: 扫描所有 assistant 消息剥离 orphan tool_calls(原仅查末条) - queue 加 conversationId 字段,按会话精准 drain - regenerate/editMessage 只清本会话排队消息 - newConversation 保留旧会话排队消息 - AiError 只清出错会话的队列项
This commit is contained in:
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user