From 7f6aa1e782533757d965eff04a980980cb7b0980 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=BB=9D=E5=B0=98?= <237809796@qq.com> Date: Wed, 24 Jun 2026 10:22:09 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E:=20AST=E4=BB=A3=E7=A0=81?= =?UTF-8?q?=E6=99=BA=E8=83=BDPhase2(read=5Fsymbol=20drill=E4=B8=8B?= =?UTF-8?q?=E9=92=BB+=E8=B0=83=E7=94=A8=E7=82=B9=E5=B8=A6=E8=A1=8C+prompt?= =?UTF-8?q?=E4=BC=98=E5=85=88=E5=BC=95=E5=AF=BC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit code_intel: read_symbol 加 drill(目标节点子树递归找嵌套子定义,命中走full)/calls 每项带 line号(与start_row同口径,Vue切段偏移自动对齐)/2 新测试(drill命中+drill_not_found兜底) prompt: 中英能力列表加 read_symbol 优先引导(治 read_file 全文回灌 prompt 爆)+ 修支持集标错bug(.rs/.ts等原标不支持实为支持集) tool_registry: read_symbol schema 加 drill 参数 + read_file desc 反向引导 密度: code_intel.rs 全文 25733B vs 骨架 1348B → 降 19.1x;16 单测全绿 --- src-tauri/src/commands/ai/code_intel.rs | 117 ++++++++++++++++----- src-tauri/src/commands/ai/prompt.rs | 2 + src-tauri/src/commands/ai/tool_registry.rs | 7 +- 3 files changed, 98 insertions(+), 28 deletions(-) diff --git a/src-tauri/src/commands/ai/code_intel.rs b/src-tauri/src/commands/ai/code_intel.rs index 949fec4..7d9552d 100644 --- a/src-tauri/src/commands/ai/code_intel.rs +++ b/src-tauri/src/commands/ai/code_intel.rs @@ -106,6 +106,9 @@ fn extract_vue_script(source: &str) -> Option<(String, usize)> { /// - `symbol`:目标符号名(精确匹配定义节点的 name field) /// - `full`:true=完整定义体,false=骨架 /// - `kind_hint`:可选 kind 过滤(如 "function"/"struct"),None 匹配任意定义 kind +/// - `drill`:可选子定义名(如 "parse");Some(name)=在目标符号节点子树内递归找名为 name 的 +/// 嵌套定义(局部函数/impl 方法),命中即返该子定义的 full 态;未命中走兜底(reason=drill_not_found)。 +/// None=不下钻,走原 full/kind_hint 三态逻辑。 pub fn read_symbol( source: &str, file_hash: &str, @@ -113,6 +116,7 @@ pub fn read_symbol( symbol: &str, full: bool, kind_hint: Option<&str>, + drill: Option<&str>, ) -> serde_json::Value { let ext = path.rsplit('.').next().unwrap_or("").to_lowercase(); let is_vue = ext == "vue"; @@ -154,6 +158,21 @@ pub fn read_symbol( let mut found: Option = None; find_definition(&root, symbol, kind_hint, parse_source.as_bytes(), &mut found); + // drill:在目标符号节点子树内递归找名为 drill 的嵌套定义(局部函数/impl 方法)。 + // 命中即返该子定义 full 态;未命中走兜底(reason=drill_not_found,不 panic)。 + if let Some(drill_name) = drill { + if let Some(sym_node) = found { + let mut drill_found: Option = None; + find_definition(&sym_node, drill_name, None, parse_source.as_bytes(), &mut drill_found); + return match drill_found { + Some(dn) => extract_symbol(dn, parse_source.as_bytes(), source, path, file_hash, true, line_offset), + None => fallback(drill_name, path, file_hash, "drill_not_found", + &format!("符号「{}」内未找到下钻子定义「{}」(可能非嵌套定义/作用域差异)", symbol, drill_name)), + }; + } + // found 本身为 None 时 drill 无意义,落入下方 not_found 兜底 + } + match found { Some(node) => extract_symbol(node, parse_source.as_bytes(), source, path, file_hash, full, line_offset), None => fallback(symbol, path, file_hash, "not_found", @@ -237,7 +256,7 @@ fn extract_symbol( let node_src = node_text(node, source); let signature = extract_signature(&node_src); - let calls = collect_calls(node, source); + let calls = collect_calls(node, source, line_offset); if full { serde_json::json!({ @@ -265,7 +284,7 @@ fn extract_symbol( "signature": signature, "calls": calls, "mode": "skeleton", - "hint": "骨架态:传 full=true 取完整定义体(语法边界精准);下钻局部分支 drill 待 Phase2", + "hint": "骨架态:传 full=true 取完整定义体(语法边界精准);传 drill=\"子定义名\" 下钻符号内嵌套定义(局部函数/impl 方法)", }) } } @@ -289,25 +308,34 @@ fn extract_signature(node_src: &str) -> String { /// /// Phase1:扁平收集去重(Phase2 按分支结构组织调用图)。 /// Rust/TS/JS 的 call_expression 都有 "function" field(callee),统一提取。 -fn collect_calls(node: Node, source: &[u8]) -> Vec { +/// Phase2:每项带行号(call_expression 所在行,1-based + Vue 偏移,与 start_row 同口径)。 +fn collect_calls(node: Node, source: &[u8], line_offset: usize) -> Vec { let mut calls = Vec::new(); let mut seen = HashSet::new(); - collect_calls_inner(node, source, &mut calls, &mut seen); + collect_calls_inner(node, source, line_offset, &mut calls, &mut seen); calls } -fn collect_calls_inner(node: Node, source: &[u8], out: &mut Vec, seen: &mut HashSet) { +fn collect_calls_inner( + node: Node, + source: &[u8], + line_offset: usize, + out: &mut Vec, + seen: &mut HashSet, +) { let mut cursor = node.walk(); for child in node.children(&mut cursor) { if child.kind() == "call_expression" { if let Some(func) = child.child_by_field_name("function") { let name = node_text(func, source).to_string(); if !name.is_empty() && seen.insert(name.clone()) { - out.push(name); + // 行号:调用语句所在行(call_expression 节点本身的 start_position),1-based + Vue 偏移 + let line = child.start_position().row + 1 + line_offset; + out.push(serde_json::json!({ "name": name, "line": line })); } } } - collect_calls_inner(child, source, out, seen); + collect_calls_inner(child, source, line_offset, out, seen); } } @@ -375,7 +403,7 @@ struct Config { #[test] fn test_read_symbol_rust_skeleton() { - let r = read_symbol(RUST_FIXTURE, "hash1", "test.rs", "read_symbol", false, None); + let r = read_symbol(RUST_FIXTURE, "hash1", "test.rs", "read_symbol", false, None, None); assert_eq!(r["fallback"], serde_json::Value::Null); assert_eq!(r["symbol"], "read_symbol"); assert_eq!(r["kind"], "function_item"); @@ -384,15 +412,16 @@ struct Config { assert!(r["content"].is_null()); assert!(r["signature"].as_str().unwrap().contains("read_symbol")); // calls 含同文件静态调用(parse/validate)+ load(跨文件也命中名,Phase1 扁平) + // Phase2:每项是 {name, line} 对象 let calls = r["calls"].as_array().unwrap(); - assert!(calls.iter().any(|c| c == "parse")); - assert!(calls.iter().any(|c| c == "validate")); + assert!(calls.iter().any(|c| c["name"] == "parse")); + assert!(calls.iter().any(|c| c["name"] == "validate")); assert_eq!(r["body_lines"].as_u64().unwrap(), 6); } #[test] fn test_read_symbol_rust_full() { - let r = read_symbol(RUST_FIXTURE, "hash1", "test.rs", "read_symbol", true, None); + let r = read_symbol(RUST_FIXTURE, "hash1", "test.rs", "read_symbol", true, None, None); assert_eq!(r["mode"], "full"); let content = r["content"].as_str().unwrap(); // full 态返完整定义体(含函数体 {}) @@ -403,7 +432,7 @@ struct Config { #[test] fn test_read_symbol_struct() { - let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "Config", false, None); + let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "Config", false, None, None); assert_eq!(r["symbol"], "Config"); assert_eq!(r["kind"], "struct_item"); } @@ -411,14 +440,14 @@ struct Config { #[test] fn test_read_symbol_kind_hint_filter() { // kind_hint="struct" 时,"read_symbol"(function)不应被误匹配为 struct - let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "Config", false, Some("struct")); + let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "Config", false, Some("struct"), None); assert_eq!(r["symbol"], "Config"); assert_eq!(r["kind"], "struct_item"); } #[test] fn test_read_symbol_not_found_fallback() { - let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "nonexistent_fn", false, None); + let r = read_symbol(RUST_FIXTURE, "h", "test.rs", "nonexistent_fn", false, None, None); assert_eq!(r["fallback"], true); assert_eq!(r["reason"], "not_found"); assert!(r["suggestion"].as_str().unwrap().contains("grep")); @@ -427,7 +456,7 @@ struct Config { #[test] fn test_read_symbol_no_grammar_fallback() { let src = "def hello():\n print('hi')\n"; - let r = read_symbol(src, "h", "test.py", "hello", false, None); + let r = read_symbol(src, "h", "test.py", "hello", false, None, None); assert_eq!(r["fallback"], true); assert_eq!(r["reason"], "no_grammar"); assert!(r["suggestion"].as_str().unwrap().contains("不支持")); @@ -448,7 +477,7 @@ function helper() {} "#; - let r = read_symbol(vue, "h", "App.vue", "greet", false, None); + let r = read_symbol(vue, "h", "App.vue", "greet", false, None, None); assert_eq!(r["fallback"], serde_json::Value::Null); assert_eq!(r["symbol"], "greet"); assert_eq!(r["kind"], "function_declaration"); @@ -456,17 +485,53 @@ function helper() {} let start = r["line_start"].as_u64().unwrap(); assert!(start >= 4, "Vue 行号应映射回原文件(script 段偏移后),实际 {}", start); let calls = r["calls"].as_array().unwrap(); - assert!(calls.iter().any(|c| c == "helper")); + // Phase2:calls 每项是 {name, line} 对象 + assert!(calls.iter().any(|c| c["name"] == "helper")); } #[test] fn test_read_symbol_vue_no_script_fallback() { let vue = "\n"; - let r = read_symbol(vue, "h", "App.vue", "greet", false, None); + let r = read_symbol(vue, "h", "App.vue", "greet", false, None, None); assert_eq!(r["fallback"], true); assert_eq!(r["reason"], "vue_no_script"); } + /// Phase2 drill:下钻目标符号(read_symbol)内的嵌套子定义(parse)。 + /// RUST_FIXTURE 中 read_symbol 函数体内调用了 parse,但 parse 是顶层函数(非 read_symbol 子树内), + /// 故用带嵌套局部函数的 fixture 验证:outer 内定义 inner,drill="inner" 应命中并返 full 态。 + #[test] + fn test_read_symbol_drill_hits_nested() { + let src = r#" +fn outer() { + fn inner(x: i32) -> i32 { + x + 1 + } + inner(42) +} +"#; + // 外层骨架态,确认 outer 能解析到 + let skeleton = read_symbol(src, "h", "t.rs", "outer", false, None, None); + assert_eq!(skeleton["symbol"], "outer"); + // 下钻 inner:应在 outer 子树内命中,返 full 态 + let drill = read_symbol(src, "h", "t.rs", "outer", false, None, Some("inner")); + assert_eq!(drill["fallback"], serde_json::Value::Null, "drill 命中应返符号非兜底"); + assert_eq!(drill["symbol"], "inner"); + assert_eq!(drill["mode"], "full", "drill 命中走 full 态(完整定义体)"); + let content = drill["content"].as_str().unwrap(); + assert!(content.contains("fn inner"), "drill content 应含 inner 定义体"); + } + + /// Phase2 drill 未命中兜底:下钻外层符号内不存在的子定义名 → fallback reason=drill_not_found。 + #[test] + fn test_read_symbol_drill_not_found_fallback() { + let src = "fn outer() {\n fn inner() {}\n}\n"; + let drill = read_symbol(src, "h", "t.rs", "outer", false, None, Some("nonexistent")); + assert_eq!(drill["fallback"], true); + assert_eq!(drill["reason"], "drill_not_found"); + assert!(drill["suggestion"].as_str().unwrap().contains("nonexistent")); + } + #[test] fn test_extract_vue_script() { let vue = "a\nb\n"; @@ -495,11 +560,13 @@ function helper() {} let mut parser = Parser::new(); parser.set_language(&Language::from(tree_sitter_rust::LANGUAGE)).unwrap(); let tree = parser.parse(src, None).unwrap(); - let calls = collect_calls(tree.root_node(), src.as_bytes()); - // b 去重只出现一次 - assert!(calls.iter().any(|c| c == "b")); - assert_eq!(calls.iter().filter(|c| *c == "b").count(), 1); - assert!(calls.iter().any(|c| c == "c")); + let calls = collect_calls(tree.root_node(), src.as_bytes(), 0); + // b 去重只出现一次(Phase2:每项是 {name, line} 对象) + assert!(calls.iter().any(|c| c["name"] == "b")); + assert_eq!(calls.iter().filter(|c| c["name"] == "b").count(), 1); + assert!(calls.iter().any(|c| c["name"] == "c")); + // line 应为调用所在行(src 单行,a 在第 1 行) + assert_eq!(calls[0]["line"].as_u64().unwrap(), 1); } /// 多角度验证(用户要求:实现后看效果 + 多角度测试): @@ -514,7 +581,7 @@ function helper() {} let full_bytes = source.len(); // 骨架态:解析自身 read_symbol 函数(只给该符号,非整个文件) - let skeleton = read_symbol(&source, "h0", "code_intel.rs", "read_symbol", false, None); + let skeleton = read_symbol(&source, "h0", "code_intel.rs", "read_symbol", false, None, None); assert_ne!(skeleton["fallback"], true, "真实文件应解析出 read_symbol 符号"); assert_eq!(skeleton["symbol"], "read_symbol"); assert_eq!(skeleton["kind"], "function_item"); @@ -531,7 +598,7 @@ function helper() {} assert!(ratio > 5.0, "骨架密度应显著高于全文(降 >5x),实际 {:.1}x", ratio); // full 态:完整定义体(语法边界 100% 准) - let full_sym = read_symbol(&source, "h1", "code_intel.rs", "read_symbol", true, None); + let full_sym = read_symbol(&source, "h1", "code_intel.rs", "read_symbol", true, None, None); let content = full_sym["content"].as_str().expect("full 态应返 content"); assert!(content.contains("pub fn read_symbol")); // calls 应含 read_symbol 内部调用的函数(grammar_for/extract_symbol/fallback 等) diff --git a/src-tauri/src/commands/ai/prompt.rs b/src-tauri/src/commands/ai/prompt.rs index c2e79dc..6cf8ad5 100644 --- a/src-tauri/src/commands/ai/prompt.rs +++ b/src-tauri/src/commands/ai/prompt.rs @@ -69,6 +69,7 @@ fn system_prompt_parts(lang: &str) -> (&'static str, &'static str, &'static str) - Create/query projects, tasks, and ideas\n\ - Run workflows\n\ - Read file contents, list directories, create/write files\n\ + - To understand/locate a code symbol (function/struct/class) prefer read_symbol(path, symbol): returns a skeleton (signature + line range + internal calls, minimal and high-density), avoiding read_file dumping the whole file into the prompt; pass full=true for the full definition body; read_symbol supports .rs/.ts/.tsx/.js/.jsx/.vue — fall back to read_file only for other languages or when the symbol is not found\n\ - Get file metadata (size, line count, binary detection)\n\ - Append content to files, search files by name pattern\n\n\ ## Guidelines\n\ @@ -92,6 +93,7 @@ fn system_prompt_parts(lang: &str) -> (&'static str, &'static str, &'static str) - 创建/查询项目、任务、灵感\n\ - 运行工作流\n\ - 读取文件内容、列出目录、创建/写入文件\n\ + - 理解/定位代码符号(函数/结构体/类等)优先用 read_symbol(path, symbol):返回骨架(签名+行范围+内部调用,极小高密度),避免 read_file 全文回灌撑爆 prompt;需完整定义体传 full=true;read_symbol 支持 .rs/.ts/.tsx/.js/.jsx/.vue,其他语言或未找到符号时再退回 read_file\n\ - 获取文件元信息(大小、行数、二进制检测)\n\ - 追加写入文件、按名称模式搜索文件\n\n\ ## 行为准则\n\ diff --git a/src-tauri/src/commands/ai/tool_registry.rs b/src-tauri/src/commands/ai/tool_registry.rs index 50195c5..529c495 100644 --- a/src-tauri/src/commands/ai/tool_registry.rs +++ b/src-tauri/src/commands/ai/tool_registry.rs @@ -948,7 +948,7 @@ fn register_file_tools( // ── 文件系统 ── registry.register( - "read_file", "读取文件内容,返回文本内容。支持 offset/limit 分页;传入 search 则在文件内容中搜索匹配行(大小写敏感,字符串包含匹配),返回 matches 数组(限50条)", + "read_file", "读取文件内容,返回文本内容。支持 offset/limit 分页;传入 search 则在文件内容中搜索匹配行(大小写敏感,字符串包含匹配),返回 matches 数组(限50条)。仅需函数/类型符号的签名或定义体时优先用 read_symbol(骨架/全文,避免 prompt 爆)", df_ai::ai_tools::object_schema(vec![("path", "string", true), ("offset", "integer", false), ("limit", "integer", false), ("search", "string", false)]), RiskLevel::Low, { let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| { @@ -1044,7 +1044,7 @@ fn register_file_tools( ); registry.register( "read_symbol", "AST 符号解析(信息密度驱动,治 read_file 全文回灌 prompt 爆)。提取函数/结构体/类等符号:默认返回骨架(签名+行范围+内部调用,极小高密度);传 full=true 取完整定义体。不支持/未找到时回退提示用 grep+read_file。Phase1 支持 .rs/.ts/.tsx/.js/.jsx/.vue。", - df_ai::ai_tools::object_schema(vec![("path", "string", true), ("symbol", "string", true), ("full", "boolean", false), ("kind", "string", false)]), + df_ai::ai_tools::object_schema(vec![("path", "string", true), ("symbol", "string", true), ("full", "boolean", false), ("kind", "string", false), ("drill", "string", false)]), RiskLevel::Low, { let allowed_dirs = allowed_dirs.clone(); Box::new(move |args: serde_json::Value| { let allowed_dirs = allowed_dirs.clone(); @@ -1058,6 +1058,7 @@ fn register_file_tools( let symbol = args["symbol"].as_str().ok_or_else(|| anyhow::anyhow!("缺少 symbol 参数"))?; let full = args["full"].as_bool().unwrap_or(false); let kind_hint = args["kind"].as_str(); + let drill = args["drill"].as_str().filter(|s| !s.is_empty()); use tokio::fs::File; use tokio::io::AsyncReadExt; let mut file = File::open(path).await @@ -1081,7 +1082,7 @@ fn register_file_tools( } // 调 code_intel 纯函数(三态 + 兜底,不 panic) Ok(crate::commands::ai::code_intel::read_symbol( - &content, &file_hash, path, symbol, full, kind_hint, + &content, &file_hash, path, symbol, full, kind_hint, drill, )) }) })},