优化: aichat 工具体验补强(切生成会话占位气泡治丢首响应/read_symbol符号级提示/失败引导/审批失败toast) + 修复 WorkflowDagDisplay 崩页与 aiChat i18n 缺key

This commit is contained in:
lxy
2026-08-09 21:34:53 +08:00
parent cad76ffdf2
commit 37040616bd
10 changed files with 335 additions and 21 deletions
+98
View File
@@ -92,6 +92,56 @@ pub fn is_supported_ext(ext: &str) -> bool {
matches!(ext, "rs" | "ts" | "tsx" | "js" | "jsx" | "vue" | "go" | "java" | "py")
}
/// 判断 source 内容是否真的含可提取的函数/类/结构体定义(AC-4 read_file 符号级命中提示)。
///
/// read_file 读取成功后据此决定是否追加「改用 read_symbol」提示:仅当内容里真的存在
/// DEFINITION_KINDS 定义节点才提示——比旧按扩展名判断更准,无定义的文件不瞎提示。
/// 与 read_symbol 同语法树逻辑(grammar_for + DEFINITION_KINDS + Vue 切 <script> 段借 ts),
/// 但不提取符号只探测存在性,开销低。**复用现有函数,不新写正则**。
/// 无 grammar / 解析失败 / Vue 无 script / 无定义节点 → false(不提示)。
pub(crate) fn contains_definition(source: &str, ext: &str) -> bool {
let ext_lower = ext.to_lowercase();
let (parse_source, parse_ext): (String, &str) = if ext_lower == "vue" {
// Vue 逻辑在 <script> 段,切段借 ts(与 read_symbol 的 extract_vue_script 同源)
match extract_vue_script(source) {
Some((script, _)) => (script, "ts"),
None => return false, // 纯 template/style 无可提取符号
}
} else {
(source.to_string(), ext_lower.as_str())
};
let grammar = match grammar_for(parse_ext) {
Some(g) => g,
None => return false, // 不支持的语言不提示
};
let mut parser = Parser::new();
if parser.set_language(&grammar).is_err() {
return false; // grammar 加载失败(ABI 不兼容)不提示
}
let tree = match parser.parse(&parse_source, None) {
Some(t) => t,
None => return false, // 解析失败不提示
};
tree_has_definition(&tree.root_node())
}
/// 递归遍历语法树,判断是否含任一 DEFINITION_KINDS 定义节点(contains_definition 的遍历内核)。
fn tree_has_definition(node: &Node<'_>) -> bool {
if DEFINITION_KINDS.contains(&node.kind()) {
return true;
}
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
if tree_has_definition(&child) {
return true;
}
}
false
}
/// 提取 Vue SFC 的 `<script>` 段内容(借 TS 解析)。
///
/// Vue 单文件组件逻辑在 `<script setup lang="ts">` 内,template/style 不做符号提取。
@@ -633,4 +683,52 @@ fn outer() {
let calls = skeleton["calls"].as_array().expect("骨架应含 calls");
assert!(!calls.is_empty(), "应提取出内部调用");
}
// ============================================================
// contains_definition — AC-4 read_file 符号级命中检测
// ============================================================
#[test]
fn test_contains_definition_hits_definitions() {
// 含 fn/struct 定义 → true(应提示改用 read_symbol)
assert!(contains_definition(RUST_FIXTURE, "rs"));
assert!(contains_definition("pub fn main() {}\n", "rs"));
assert!(contains_definition("struct Config { name: String }\n", "rs"));
// TS/JS
assert!(contains_definition("export function greet() { return 1; }", "ts"));
assert!(contains_definition("const handler = () => {};", "js"));
// Go / Java / Python
assert!(contains_definition("package main\nfunc main() {}\n", "go"));
assert!(contains_definition("class Foo { void bar() {} }", "java"));
assert!(contains_definition("def foo():\n pass\n", "py"));
}
#[test]
fn test_contains_definition_plain_text_false() {
// 无定义节点的纯文本/纯注释 → false(符号级命中,不瞎提示)
assert!(!contains_definition("just some text\nwith no definitions\n", "rs"));
assert!(!contains_definition("// 只有注释\n/* 多行注释 */\n", "rs"));
// 字符串字面量里有 fn 字样但不是定义
assert!(!contains_definition("let msg = \"fn main() {}\";\nconsole.log(msg);", "js"));
}
#[test]
fn test_contains_definition_unsupported_ext_or_parse_fail() {
// Ruby 无 grammar → false
assert!(!contains_definition("def foo()\n puts 'hi'\nend\n", "rb"));
// 无扩展名 → false
assert!(!contains_definition("any content", ""));
// 大小写不敏感("RS" 等价 "rs")
assert!(contains_definition("pub fn main() {}\n", "RS"));
}
#[test]
fn test_contains_definition_vue() {
// Vue 含 <script> 函数 → true
let vue = "<template><div>{{ msg }}</div></template>\n\n<script setup lang=\"ts\">\nfunction greet() {}\n</script>\n";
assert!(contains_definition(vue, "vue"));
// 无 <script> 段(纯 template/style)→ false
let no_script = "<template><div>hi</div></template>\n<style></style>";
assert!(!contains_definition(no_script, "vue"));
}
}