新增: AST代码智能Phase2(read_symbol drill下钻+调用点带行+prompt优先引导)

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 单测全绿
This commit is contained in:
2026-06-24 10:22:09 +08:00
parent 40a97a7655
commit 7f6aa1e782
3 changed files with 98 additions and 28 deletions

View File

@@ -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<Node> = 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<Node> = 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<String> {
/// Phase2:每项带行号(call_expression 所在行,1-based + Vue 偏移,与 start_row 同口径)。
fn collect_calls(node: Node, source: &[u8], line_offset: usize) -> Vec<serde_json::Value> {
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<String>, seen: &mut HashSet<String>) {
fn collect_calls_inner(
node: Node,
source: &[u8],
line_offset: usize,
out: &mut Vec<serde_json::Value>,
seen: &mut HashSet<String>,
) {
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() {}
<style scoped></style>
"#;
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 = "<template><div>hi</div></template>\n<style></style>";
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<script>let x = 1;</script>";
@@ -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 等)

View File

@@ -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=trueread_symbol 支持 .rs/.ts/.tsx/.js/.jsx/.vue其他语言或未找到符号时再退回 read_file\n\
- 获取文件元信息(大小、行数、二进制检测)\n\
- 追加写入文件、按名称模式搜索文件\n\n\
## 行为准则\n\

View File

@@ -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,
))
})
})},