新增: idea source 消息级溯源 + workspace_root 去固定根 + run_command 偏好修复

idea source 结构化(消息级溯源闭环 — 灵感来源 conv_msg:{id}):
- 新建 idea_source.rs(IDEA_SOURCE_AUTO_FILL_ENABLED + maybe_fill_idea_source 保守五条件:
  开关/create_idea/source 空(不覆盖 AI 填)/message_id 非 None/result.id)
- audit/mod low_risk + chat ai_approve 双执行路径接补全(create_idea Medium 审批)

workspace_root 去固定根(F-260619-03 收尾):
- is_authorized 删 workspace_root 固定放行(代码硬编码),改走 persistent/session 白名单
- default_with_root 保留(默认初始 persistent,首次免授权行为不变)
- 用户从白名单删工程根后也需授权(动态白名单完整语义)

run_command 偏好修复(db 分析:591ff4a0 run_command 31/56=55% 严重偏好):
- P1 描述收紧(明确仅命令执行 + 负向引导:读取 read_file/编辑 patch_file/列目录 list_directory/搜索 search_files)
- P2 ToolDomain::Exec(run_command 独立 domain,仅 Debug 含 Exec,Code/File/Search 不含)
- P3 注册顺序(run_command 放 File 层最后)+ DEBUG_RULES 加命令执行关键词

自验: df-ai 228(intent Exec)+ devflow 169(idea source)+ workspace EXIT 0
This commit is contained in:
2026-06-20 04:08:10 +08:00
parent 7911bc292c
commit e4c2e310a5
6 changed files with 496 additions and 90 deletions

View File

@@ -119,6 +119,13 @@ const DEBUG_RULES: &[Rule] = &[
Rule { kw: "", weight: 0.5 },
Rule { kw: "panic", weight: 0.9 },
Rule { kw: "traceback", weight: 0.9 },
// 命令执行场景关键词run_command 入口引导):用户明确"运行/执行/构建/测试/跑"
// 才该走 Exec domain(run_command)。中等权重(0.7)避免误抢 Code 命中(编译/重构 1.0)。
Rule { kw: "运行", weight: 0.7 },
Rule { kw: "执行", weight: 0.7 },
Rule { kw: "构建", weight: 0.7 },
Rule { kw: "测试", weight: 0.7 },
Rule { kw: "", weight: 0.6 },
];
const FILE_RULES: &[Rule] = &[
@@ -225,8 +232,10 @@ const GENERIC_GROUP: &[IntentGroup] = &[
pub enum ToolDomain {
/// 数据/业务:项目/任务/灵感/工作流/回收站
Data,
/// 文件:读写/patch/列目录/搜索/命令
/// 文件:读写/patch/列目录/搜索(不含命令执行)
File,
/// 命令执行run_commandshell 命令,独立 domain 防止被泛 File 意图带出)
Exec,
/// 网络HTTP
Http,
}
@@ -261,12 +270,15 @@ impl ToolDomain {
"patch_file",
"list_directory",
"search_files",
"run_command",
"file_info",
"append_file",
"delete_file",
"rename_file",
],
// Exec domain 独立:run_command 从 File 移出。
// 意图侧仅 Debug 默认带 Exec用户明确"运行/测试/构建"
// Code/File/Search 不带 → 减少 LLM 对 run_command 的偏好暴露。
ToolDomain::Exec => &["run_command"],
ToolDomain::Http => &["http_request"],
}
}
@@ -337,11 +349,13 @@ fn best_in_group(message: &str, group: &[IntentGroup]) -> Option<(Intent, f32)>
///
/// 设计Code → [file, http]File → [file]Project/Task/Idea → [data]
/// Http → [http]Search → [file](含 search_filesConversation → [];
/// Chat → []Debug → [file, http, data](调试常需读文件+查 API+查任务/工作流状态,CR-25 审查🟡-1 加 data 防"调试任务"丢 Data 工具);Unknown → [](全量)。
/// Chat → []Debug → [file, exec, http, data](调试常需跑命令+读文件+查 API+查任务/工作流状态,CR-25 审查🟡-1 加 data 防"调试任务"丢 Data 工具);
/// **仅 Debug 含 Exec**(用户明确"运行/测试/构建/调试"才暴露 run_command,
/// Code/File/Search 不含 Exec → 收紧 run_command 暴露面Unknown → [](全量)。
pub fn tool_subset_for(intent: &Intent) -> Vec<&'static str> {
let domains: &[ToolDomain] = match intent {
Intent::Code => &[ToolDomain::File, ToolDomain::Http],
Intent::Debug => &[ToolDomain::File, ToolDomain::Http, ToolDomain::Data],
Intent::Debug => &[ToolDomain::File, ToolDomain::Exec, ToolDomain::Http, ToolDomain::Data],
Intent::File => &[ToolDomain::File],
Intent::Project => &[ToolDomain::Data],
Intent::Task => &[ToolDomain::Data],
@@ -669,7 +683,9 @@ mod tests {
fn tool_domain_names_match_registry() {
// 抽样核对与 src-tauri tool_registry 实际注册名一致
assert!(ToolDomain::File.tools().contains(&"patch_file"));
assert!(ToolDomain::File.tools().contains(&"run_command"));
// run_command 已从 File 移出 Exec domain(意图收紧:仅 Debug 默认带 run_command)
assert!(!ToolDomain::File.tools().contains(&"run_command"));
assert_eq!(ToolDomain::Exec.tools(), &["run_command"]);
assert!(ToolDomain::Data.tools().contains(&"advance_task"));
assert!(ToolDomain::Data.tools().contains(&"run_workflow"));
assert!(ToolDomain::Data.tools().contains(&"list_trash"));
@@ -838,9 +854,9 @@ mod tests {
}
#[test]
fn filter_debug_subset_spans_three_domains_complete() {
// 对抗(多 domain):Debug subset 跨 File + Http + Data domain,
// 必须同条消息里能同时命中个 domain 的工具(防 domain 漏挂)
fn filter_debug_subset_spans_four_domains_complete() {
// 对抗(多 domain):Debug subset 跨 File + Exec + Http + Data domain,
// 必须同条消息里能同时命中个 domain 的工具(防 domain 漏挂)
let all = vec![
// Data domain
tool_def("list_tasks"),
@@ -849,6 +865,8 @@ mod tests {
// File domain
tool_def("read_file"),
tool_def("patch_file"),
// Exec domain
tool_def("run_command"),
// Http domain
tool_def("http_request"),
];
@@ -856,8 +874,27 @@ mod tests {
let names: Vec<String> = out.iter().map(|d| d.function.name.clone()).collect();
assert!(names.contains(&"list_tasks".to_string()), "Debug 必含 Data(list_tasks), 防 CR-25 🟡-1 丢 Data");
assert!(names.contains(&"read_file".to_string()), "Debug 必含 File");
assert!(names.contains(&"run_command".to_string()), "Debug 必含 Exec(run_command)");
assert!(names.contains(&"http_request".to_string()), "Debug 必含 Http");
assert_eq!(names.len(), 6, " domain 工具全保留, 实际: {:?}", names);
assert_eq!(names.len(), 7, " domain 工具全保留, 实际: {:?}", names);
}
#[test]
fn run_command_only_exposed_for_debug_intent() {
// 核心修复断言:run_command 仅 Debug 暴露。
// Code/File/Search 意图 subset 不含 Exec domain → 不含 run_command。
for intent in [Intent::Code, Intent::File, Intent::Search] {
let s = tool_subset_for(&intent);
assert!(
!s.contains(&"run_command"),
"Intent {:?} subset 不应含 run_command(仅 Debug 暴露), 实际: {:?}",
intent,
s
);
}
// Debug 必含 run_command(Exec domain)
let dbg = tool_subset_for(&Intent::Debug);
assert!(dbg.contains(&"run_command"), "Debug 必含 run_command");
}
#[test]
@@ -893,11 +930,12 @@ mod tests {
];
for intent in all_intents {
let subset = tool_subset_for(&intent);
// 全 registry 工具名( domain 并集)
// 全 registry 工具名( domain 并集:Data + File + Exec + Http)
let registry: std::collections::HashSet<&str> = ToolDomain::Data
.tools()
.iter()
.chain(ToolDomain::File.tools().iter())
.chain(ToolDomain::Exec.tools().iter())
.chain(ToolDomain::Http.tools().iter())
.copied()
.collect();