From 1a76bf6c824fa43ac3b7a4cfd8961698b241b99f Mon Sep 17 00:00:00 2001 From: imeepos Date: Mon, 21 Jul 2025 13:27:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=AE=9E=E7=8E=B0=E5=9F=BA=E4=BA=8ETre?= =?UTF-8?q?e-sitter=E7=9A=84=E5=AE=B9=E9=94=99JSON=E8=A7=A3=E6=9E=90?= =?UTF-8?q?=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 添加TolerantJsonParser核心解析器,支持多种错误恢复策略 - 实现CachedTolerantJsonParser带缓存的解析器,提升性能 - 支持Markdown代码块提取、无引号键、尾随逗号等容错功能 - 添加完整的Tauri命令接口,支持前端调用 - 包含全面的单元测试和集成测试 - 提供详细的API文档和使用示例 主要功能: - 标准JSON解析、手动修复、正则提取、部分解析等恢复策略 - 支持处理大模型返回的不规范JSON数据 - 内置缓存机制,支持高频解析场景 - 详细的解析统计信息和错误报告 - 遵循Tauri开发规范的分层架构设计 --- Cargo.lock | 118 +-- apps/desktop/src-tauri/Cargo.toml | 3 + .../src-tauri/src/infrastructure/mod.rs | 1 + .../infrastructure/tolerant_json_parser.rs | 768 ++++++++++++++++++ .../tolerant_json_parser_README.md | 295 +++++++ .../tolerant_json_parser_examples.rs | 321 ++++++++ apps/desktop/src-tauri/src/lib.rs | 9 +- .../src/presentation/commands/mod.rs | 1 + .../commands/tolerant_json_commands.rs | 292 +++++++ 9 files changed, 1731 insertions(+), 77 deletions(-) create mode 100644 apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser.rs create mode 100644 apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser_README.md create mode 100644 apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser_examples.rs create mode 100644 apps/desktop/src-tauri/src/presentation/commands/tolerant_json_commands.rs diff --git a/Cargo.lock b/Cargo.lock index 22ba76e..d4375c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -479,17 +479,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "02260d489095346e5cafd04dea8e8cb54d1d74fcd759022a9b72986ebe9a1257" dependencies = [ "serde", - "toml 0.8.23", + "toml", ] [[package]] name = "cc" -version = "1.2.29" +version = "1.0.106" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362" -dependencies = [ - "shlex", -] +checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2" [[package]] name = "cesu8" @@ -925,14 +922,14 @@ checksum = "1c7a8fb8a9fbf66c1f703fe16184d10ca0ee9d23be5b4436400408ba54a95005" [[package]] name = "embed-resource" -version = "3.0.5" +version = "3.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c6d81016d6c977deefb2ef8d8290da019e27cc26167e102185da528e6c0ab38" +checksum = "0963f530273dc3022ab2bdc3fcd6d488e850256f2284a82b7413cb9481ee85dd" dependencies = [ "cc", "memchr", "rustc_version", - "toml 0.9.2", + "toml", "vswhom", "winreg 0.55.0", ] @@ -2315,6 +2312,7 @@ dependencies = [ "dirs 5.0.1", "lazy_static", "md5", + "regex", "reqwest 0.11.27", "rusqlite", "serde", @@ -2328,10 +2326,12 @@ dependencies = [ "thiserror 1.0.69", "tokio", "tokio-test", - "toml 0.8.23", + "toml", "tracing", "tracing-appender", "tracing-subscriber", + "tree-sitter", + "tree-sitter-json", "uuid", ] @@ -3776,15 +3776,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_spanned" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" -dependencies = [ - "serde", -] - [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -3881,12 +3872,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - [[package]] name = "signal-hook-registry" version = "1.4.5" @@ -4116,7 +4101,7 @@ dependencies = [ "cfg-expr", "heck 0.5.0", "pkg-config", - "toml 0.8.23", + "toml", "version-compare", ] @@ -4245,7 +4230,7 @@ dependencies = [ "serde_json", "tauri-utils", "tauri-winres", - "toml 0.8.23", + "toml", "walkdir", ] @@ -4303,7 +4288,7 @@ dependencies = [ "serde", "serde_json", "tauri-utils", - "toml 0.8.23", + "toml", "walkdir", ] @@ -4343,7 +4328,7 @@ dependencies = [ "tauri-plugin", "tauri-utils", "thiserror 2.0.12", - "toml 0.8.23", + "toml", "url", ] @@ -4449,7 +4434,7 @@ dependencies = [ "serde_with", "swift-rs", "thiserror 2.0.12", - "toml 0.8.23", + "toml", "url", "urlpattern", "uuid", @@ -4464,7 +4449,7 @@ checksum = "e8d321dbc6f998d825ab3f0d62673e810c861aac2d0de2cc2c395328f1d113b4" dependencies = [ "embed-resource", "indexmap 2.10.0", - "toml 0.8.23", + "toml", ] [[package]] @@ -4667,26 +4652,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_edit 0.22.27", ] -[[package]] -name = "toml" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac" -dependencies = [ - "indexmap 2.10.0", - "serde", - "serde_spanned 1.0.0", - "toml_datetime 0.7.0", - "toml_parser", - "toml_writer", - "winnow 0.7.12", -] - [[package]] name = "toml_datetime" version = "0.6.11" @@ -4696,15 +4666,6 @@ dependencies = [ "serde", ] -[[package]] -name = "toml_datetime" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" -dependencies = [ - "serde", -] - [[package]] name = "toml_edit" version = "0.19.15" @@ -4712,7 +4673,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.10.0", - "toml_datetime 0.6.11", + "toml_datetime", "winnow 0.5.40", ] @@ -4723,7 +4684,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "70f427fce4d84c72b5b732388bf4a9f4531b53f74e2887e3ecb2481f68f66d81" dependencies = [ "indexmap 2.10.0", - "toml_datetime 0.6.11", + "toml_datetime", "winnow 0.5.40", ] @@ -4735,33 +4696,18 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.10.0", "serde", - "serde_spanned 0.6.9", - "toml_datetime 0.6.11", + "serde_spanned", + "toml_datetime", "toml_write", "winnow 0.7.12", ] -[[package]] -name = "toml_parser" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30" -dependencies = [ - "winnow 0.7.12", -] - [[package]] name = "toml_write" version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" -[[package]] -name = "toml_writer" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64" - [[package]] name = "tower" version = "0.5.2" @@ -4903,6 +4849,26 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "tree-sitter" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e747b1f9b7b931ed39a548c1fae149101497de3c1fc8d9e18c62c1a66c683d3d" +dependencies = [ + "cc", + "regex", +] + +[[package]] +name = "tree-sitter-json" +version = "0.20.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a9a38a9c679b55cc8d17350381ec08d69fa1a17a53fcf197f344516e485ed4d" +dependencies = [ + "cc", + "tree-sitter", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index 9653446..754b238 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -39,6 +39,9 @@ tracing-subscriber = { version = "0.3", features = ["env-filter", "chrono"] } tracing-appender = "0.2" reqwest = { version = "0.11", features = ["json", "multipart"] } toml = "0.8" +tree-sitter = "0.20" +tree-sitter-json = "0.20" +regex = "1.10" [dev-dependencies] tempfile = "3.8" diff --git a/apps/desktop/src-tauri/src/infrastructure/mod.rs b/apps/desktop/src-tauri/src/infrastructure/mod.rs index bb8e0db..168daa3 100644 --- a/apps/desktop/src-tauri/src/infrastructure/mod.rs +++ b/apps/desktop/src-tauri/src/infrastructure/mod.rs @@ -10,3 +10,4 @@ pub mod monitoring; pub mod logging; pub mod gemini_service; pub mod video_generation_service; +pub mod tolerant_json_parser; diff --git a/apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser.rs b/apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser.rs new file mode 100644 index 0000000..39e415c --- /dev/null +++ b/apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser.rs @@ -0,0 +1,768 @@ +/// 基于Tree-sitter的大模型JSON容错解析器 +/// 遵循Tauri开发规范,提供高性能、安全的JSON解析能力 +use anyhow::{anyhow, Result}; +use regex::Regex; +use serde_json::{Map, Value}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use tree_sitter::{Node, Parser, Tree}; +use tracing::{debug, info, warn}; + +/// 解析结果统计信息 +#[derive(Debug, Clone, serde::Serialize)] +pub struct ParseStatistics { + pub total_nodes: usize, + pub error_nodes: usize, + pub error_rate: f64, + pub parse_time_ms: u64, + pub recovery_strategies_used: Vec, +} + +/// 错误恢复策略类型 +#[derive(Debug, Clone)] +pub enum RecoveryStrategy { + StandardJson, + ManualFix, + RegexExtract, + PartialParse, +} + +/// 容错JSON解析器配置 +#[derive(Debug, Clone)] +pub struct ParserConfig { + pub max_text_length: usize, + pub enable_comments: bool, + pub enable_unquoted_keys: bool, + pub enable_trailing_commas: bool, + pub timeout_ms: u64, + pub recovery_strategies: Vec, +} + +impl Default for ParserConfig { + fn default() -> Self { + Self { + max_text_length: 1024 * 1024, // 1MB + enable_comments: true, + enable_unquoted_keys: true, + enable_trailing_commas: true, + timeout_ms: 30000, // 30秒 + recovery_strategies: vec![ + RecoveryStrategy::StandardJson, + RecoveryStrategy::ManualFix, + RecoveryStrategy::RegexExtract, + RecoveryStrategy::PartialParse, + ], + } + } +} + +/// 容错JSON解析器 +pub struct TolerantJsonParser { + parser: Parser, + config: ParserConfig, + regex_patterns: HashMap, +} + +impl TolerantJsonParser { + /// 创建新的容错JSON解析器实例 + pub fn new(config: Option) -> Result { + let mut parser = Parser::new(); + + // 设置JSON语言 + let language = tree_sitter_json::language(); + parser.set_language(language) + .map_err(|e| anyhow!("Failed to set JSON language: {}", e))?; + + let config = config.unwrap_or_default(); + let regex_patterns = Self::init_regex_patterns()?; + + Ok(Self { + parser, + config, + regex_patterns, + }) + } + + /// 初始化正则表达式模式 + fn init_regex_patterns() -> Result> { + let mut patterns = HashMap::new(); + + // JSON对象模式 + patterns.insert( + "object".to_string(), + Regex::new(r"\{[^{}]*(?:\{[^{}]*\}[^{}]*)*\}")?, + ); + + // JSON数组模式 + patterns.insert( + "array".to_string(), + Regex::new(r"\[[^\[\]]*(?:\[[^\[\]]*\][^\[\]]*)*\]")?, + ); + + // Markdown代码块模式 + patterns.insert( + "markdown_fence".to_string(), + Regex::new(r"(?s)```(?:json)?\s*\n?(.*?)\n?```")?, + ); + + // 无引号键模式 + patterns.insert( + "unquoted_key".to_string(), + Regex::new(r"(\w+):")?, + ); + + // 尾随逗号模式 + patterns.insert( + "trailing_comma".to_string(), + Regex::new(r",\s*[}\]]")?, + ); + + Ok(patterns) + } + + /// 解析JSON文本,返回解析结果和统计信息 + pub fn parse(&mut self, text: &str) -> Result<(Value, ParseStatistics)> { + let start_time = std::time::Instant::now(); + + // 检查文本长度限制 + if text.len() > self.config.max_text_length { + return Err(anyhow!("Text too long: {} bytes", text.len())); + } + + info!("Starting tolerant JSON parsing, text length: {}", text.len()); + + // 预处理文本 + let cleaned_text = self.preprocess_text(text)?; + debug!("Preprocessed text length: {}", cleaned_text.len()); + + // 使用Tree-sitter解析 + let tree = self.parser.parse(&cleaned_text, None) + .ok_or_else(|| anyhow!("Failed to parse text with tree-sitter"))?; + + let root_node = tree.root_node(); + + // 收集统计信息 + let mut stats = self.collect_statistics(&tree, &cleaned_text); + + // 提取和修复JSON + let mut recovery_strategies_used = Vec::new(); + let result = self.extract_json(root_node, &cleaned_text, &mut recovery_strategies_used)?; + + stats.parse_time_ms = start_time.elapsed().as_millis() as u64; + stats.recovery_strategies_used = recovery_strategies_used; + + info!("JSON parsing completed in {}ms", stats.parse_time_ms); + + Ok((result, stats)) + } + + /// 预处理文本,提取可能的JSON内容 + fn preprocess_text(&self, text: &str) -> Result { + let mut processed = text.to_string(); + + // 移除Markdown代码块标记 + if let Some(markdown_regex) = self.regex_patterns.get("markdown_fence") { + if let Some(captures) = markdown_regex.captures(&processed) { + if let Some(json_content) = captures.get(1) { + processed = json_content.as_str().to_string(); + debug!("Extracted JSON from markdown fence"); + } + } + } + + // 查找JSON模式 + for pattern_name in ["object", "array"] { + if let Some(pattern) = self.regex_patterns.get(pattern_name) { + if let Some(mat) = pattern.find(&processed) { + processed = mat.as_str().to_string(); + debug!("Extracted JSON using {} pattern", pattern_name); + break; + } + } + } + + Ok(processed) + } + + /// 收集解析统计信息 + fn collect_statistics(&self, tree: &Tree, _text: &str) -> ParseStatistics { + let root_node = tree.root_node(); + let mut total_nodes = 0; + let mut error_nodes = 0; + + self.count_nodes(root_node, &mut total_nodes, &mut error_nodes); + + let error_rate = if total_nodes > 0 { + error_nodes as f64 / total_nodes as f64 + } else { + 0.0 + }; + + ParseStatistics { + total_nodes, + error_nodes, + error_rate, + parse_time_ms: 0, // 将在parse方法中设置 + recovery_strategies_used: Vec::new(), // 将在parse方法中设置 + } + } + + /// 递归计算节点数量 + fn count_nodes(&self, node: Node, total: &mut usize, errors: &mut usize) { + *total += 1; + + if node.is_error() { + *errors += 1; + } + + for child in node.children(&mut node.walk()) { + self.count_nodes(child, total, errors); + } + } + + /// 从语法树中提取和修复JSON + fn extract_json( + &self, + node: Node, + text: &str, + recovery_strategies: &mut Vec, + ) -> Result { + match node.kind() { + "document" => { + if node.child_count() > 0 { + self.extract_json(node.child(0).unwrap(), text, recovery_strategies) + } else { + Ok(Value::Null) + } + } + "value" => { + if node.child_count() > 0 { + self.extract_json(node.child(0).unwrap(), text, recovery_strategies) + } else { + self.recover_from_error(node, text, recovery_strategies) + } + } + "object" => self.extract_object(node, text, recovery_strategies), + "array" => self.extract_array(node, text, recovery_strategies), + "string" => self.extract_string(node, text), + "number" => self.extract_number(node, text), + "true" | "false" => self.extract_boolean(node, text), + "null" => Ok(Value::Null), + "ERROR" => self.recover_from_error(node, text, recovery_strategies), + _ => { + warn!("Unknown node kind: {}", node.kind()); + self.recover_from_error(node, text, recovery_strategies) + } + } + } + + /// 提取JSON对象 + fn extract_object( + &self, + node: Node, + text: &str, + recovery_strategies: &mut Vec, + ) -> Result { + let mut object = Map::new(); + + for child in node.children(&mut node.walk()) { + match child.kind() { + "pair" => { + if let Ok((key, value)) = self.extract_pair(child, text, recovery_strategies) { + object.insert(key, value); + } + } + "ERROR" => { + // 尝试恢复错误的对象成员 + if let Ok(recovered) = self.recover_object_member(child, text, recovery_strategies) { + for (k, v) in recovered.as_object().unwrap_or(&Map::new()) { + object.insert(k.clone(), v.clone()); + } + } + } + _ => {} // 忽略其他节点类型(如括号、逗号等) + } + } + + Ok(Value::Object(object)) + } + + /// 提取JSON数组 + fn extract_array( + &self, + node: Node, + text: &str, + recovery_strategies: &mut Vec, + ) -> Result { + let mut array = Vec::new(); + + for child in node.children(&mut node.walk()) { + match child.kind() { + "value" => { + if let Ok(value) = self.extract_json(child, text, recovery_strategies) { + array.push(value); + } + } + // Also handle direct primitive types + "number" | "string" | "true" | "false" | "null" | "object" | "array" => { + if let Ok(value) = self.extract_json(child, text, recovery_strategies) { + array.push(value); + } + } + "ERROR" => { + // 尝试恢复错误的数组元素 + if let Ok(recovered) = self.recover_from_error(child, text, recovery_strategies) { + array.push(recovered); + } + } + _ => {} // 忽略其他节点类型(如括号、逗号等) + } + } + + Ok(Value::Array(array)) + } + + /// 提取键值对 + fn extract_pair( + &self, + node: Node, + text: &str, + recovery_strategies: &mut Vec, + ) -> Result<(String, Value)> { + let mut key = None; + let mut value = None; + + for child in node.children(&mut node.walk()) { + match child.kind() { + "string" => { + if key.is_none() { + key = Some(self.extract_string_content(child, text)?); + } else { + value = Some(self.extract_json(child, text, recovery_strategies)?); + } + } + "value" => { + value = Some(self.extract_json(child, text, recovery_strategies)?); + } + _ => {} // 忽略冒号等分隔符 + } + } + + match (key, value) { + (Some(k), Some(v)) => Ok((k, v)), + _ => Err(anyhow!("Invalid key-value pair")), + } + } + + /// 提取字符串值 + fn extract_string(&self, node: Node, text: &str) -> Result { + let content = self.extract_string_content(node, text)?; + Ok(Value::String(content)) + } + + /// 提取字符串内容(去除引号) + fn extract_string_content(&self, node: Node, text: &str) -> Result { + let raw_text = &text[node.start_byte()..node.end_byte()]; + + // 移除前后的引号 + if raw_text.len() >= 2 && raw_text.starts_with('"') && raw_text.ends_with('"') { + Ok(raw_text[1..raw_text.len()-1].to_string()) + } else { + // 处理无引号的字符串 + Ok(raw_text.to_string()) + } + } + + /// 提取数字值 + fn extract_number(&self, node: Node, text: &str) -> Result { + let raw_text = &text[node.start_byte()..node.end_byte()]; + + // 尝试解析为整数 + if let Ok(int_val) = raw_text.parse::() { + Ok(Value::Number(serde_json::Number::from(int_val))) + } + // 尝试解析为浮点数 + else if let Ok(float_val) = raw_text.parse::() { + if let Some(num) = serde_json::Number::from_f64(float_val) { + Ok(Value::Number(num)) + } else { + Err(anyhow!("Invalid float number: {}", raw_text)) + } + } else { + Err(anyhow!("Invalid number: {}", raw_text)) + } + } + + /// 提取布尔值 + fn extract_boolean(&self, node: Node, text: &str) -> Result { + let raw_text = &text[node.start_byte()..node.end_byte()]; + match raw_text { + "true" => Ok(Value::Bool(true)), + "false" => Ok(Value::Bool(false)), + _ => Err(anyhow!("Invalid boolean: {}", raw_text)), + } + } + + /// 从错误节点恢复数据 + fn recover_from_error( + &self, + node: Node, + text: &str, + recovery_strategies: &mut Vec, + ) -> Result { + let error_text = &text[node.start_byte()..node.end_byte()].trim(); + debug!("Attempting to recover from error: {}", error_text); + + // 尝试不同的恢复策略 + for strategy in &self.config.recovery_strategies { + match strategy { + RecoveryStrategy::StandardJson => { + if let Ok(result) = self.try_standard_json_parse(error_text) { + recovery_strategies.push("StandardJson".to_string()); + return Ok(result); + } + } + RecoveryStrategy::ManualFix => { + if let Ok(result) = self.try_manual_fix(error_text) { + recovery_strategies.push("ManualFix".to_string()); + return Ok(result); + } + } + RecoveryStrategy::RegexExtract => { + if let Ok(result) = self.try_regex_extract(error_text) { + recovery_strategies.push("RegexExtract".to_string()); + return Ok(result); + } + } + RecoveryStrategy::PartialParse => { + if let Ok(result) = self.try_partial_parse(error_text) { + recovery_strategies.push("PartialParse".to_string()); + return Ok(result); + } + } + } + } + + // 如果所有策略都失败,返回原始文本作为字符串 + warn!("All recovery strategies failed for: {}", error_text); + Ok(Value::String(error_text.to_string())) + } + + /// 尝试标准JSON解析 + fn try_standard_json_parse(&self, text: &str) -> Result { + serde_json::from_str(text).map_err(|e| anyhow!("Standard JSON parse failed: {}", e)) + } + + /// 尝试手动修复常见的JSON错误 + fn try_manual_fix(&self, text: &str) -> Result { + let mut fixed_text = text.to_string(); + + // 修复无引号的键 + if let Some(unquoted_regex) = self.regex_patterns.get("unquoted_key") { + fixed_text = unquoted_regex.replace_all(&fixed_text, "\"$1\":").to_string(); + } + + // 移除尾随逗号 + if let Some(trailing_regex) = self.regex_patterns.get("trailing_comma") { + fixed_text = trailing_regex.replace_all(&fixed_text, |caps: ®ex::Captures| { + let full_match = caps.get(0).unwrap().as_str(); + full_match[1..].to_string() // 移除逗号,保留括号 + }).to_string(); + } + + // 修复单引号为双引号 + fixed_text = fixed_text.replace("'", "\""); + + // 尝试解析修复后的文本 + serde_json::from_str(&fixed_text).map_err(|e| anyhow!("Manual fix failed: {}", e)) + } + + /// 尝试正则表达式提取 + fn try_regex_extract(&self, text: &str) -> Result { + // 尝试提取对象 + if let Some(object_regex) = self.regex_patterns.get("object") { + if let Some(mat) = object_regex.find(text) { + return self.try_standard_json_parse(mat.as_str()); + } + } + + // 尝试提取数组 + if let Some(array_regex) = self.regex_patterns.get("array") { + if let Some(mat) = array_regex.find(text) { + return self.try_standard_json_parse(mat.as_str()); + } + } + + Err(anyhow!("No valid JSON pattern found")) + } + + /// 尝试部分解析 + fn try_partial_parse(&self, text: &str) -> Result { + // 尝试解析文本的各个部分 + let lines: Vec<&str> = text.lines().collect(); + + for i in 0..lines.len() { + for j in (i+1)..=lines.len() { + let partial_text = lines[i..j].join("\n"); + if let Ok(value) = serde_json::from_str::(&partial_text) { + return Ok(value); + } + } + } + + Err(anyhow!("Partial parse failed")) + } + + /// 恢复错误的对象成员 + fn recover_object_member( + &self, + node: Node, + text: &str, + recovery_strategies: &mut Vec, + ) -> Result { + let error_text = &text[node.start_byte()..node.end_byte()]; + + // 尝试解析为单个键值对 + if let Some(colon_pos) = error_text.find(':') { + let key_part = error_text[..colon_pos].trim(); + let value_part = error_text[colon_pos+1..].trim(); + + // 清理键名 + let clean_key = key_part.trim_matches(|c| c == '"' || c == '\'' || c == ' '); + + // 尝试解析值 + if let Ok(value) = self.try_standard_json_parse(value_part) { + let mut object = Map::new(); + object.insert(clean_key.to_string(), value); + recovery_strategies.push("ObjectMemberRecovery".to_string()); + return Ok(Value::Object(object)); + } + } + + Err(anyhow!("Failed to recover object member")) + } +} + +/// 带缓存的容错JSON解析器 +pub struct CachedTolerantJsonParser { + parser: Arc>, + cache: Arc>>, + max_cache_size: usize, +} + +impl CachedTolerantJsonParser { + /// 创建新的带缓存的解析器实例 + pub fn new(config: Option, max_cache_size: Option) -> Result { + let parser = TolerantJsonParser::new(config)?; + let max_cache_size = max_cache_size.unwrap_or(1000); + + Ok(Self { + parser: Arc::new(Mutex::new(parser)), + cache: Arc::new(Mutex::new(HashMap::new())), + max_cache_size, + }) + } + + /// 解析JSON文本,使用缓存机制 + pub fn parse(&self, text: &str) -> Result<(Value, ParseStatistics)> { + // 计算文本哈希作为缓存键 + let cache_key = self.compute_cache_key(text); + + // 检查缓存 + { + let cache = self.cache.lock().unwrap(); + if let Some(cached_result) = cache.get(&cache_key) { + debug!("Cache hit for JSON parsing"); + return Ok(cached_result.clone()); + } + } + + // 缓存未命中,执行解析 + let result = { + let mut parser = self.parser.lock().unwrap(); + parser.parse(text)? + }; + + // 存储到缓存 + { + let mut cache = self.cache.lock().unwrap(); + + // 如果缓存已满,移除最旧的条目 + if cache.len() >= self.max_cache_size { + // 简单的LRU实现:移除第一个条目 + if let Some(first_key) = cache.keys().next().cloned() { + cache.remove(&first_key); + } + } + + cache.insert(cache_key, result.clone()); + } + + debug!("Cached new JSON parsing result"); + Ok(result) + } + + /// 计算缓存键 + fn compute_cache_key(&self, text: &str) -> String { + use std::collections::hash_map::DefaultHasher; + use std::hash::{Hash, Hasher}; + + let mut hasher = DefaultHasher::new(); + text.hash(&mut hasher); + format!("{:x}", hasher.finish()) + } + + /// 清空缓存 + pub fn clear_cache(&self) { + let mut cache = self.cache.lock().unwrap(); + cache.clear(); + info!("JSON parser cache cleared"); + } + + /// 获取缓存统计信息 + pub fn get_cache_stats(&self) -> (usize, usize) { + let cache = self.cache.lock().unwrap(); + (cache.len(), self.max_cache_size) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + /// 创建测试用的解析器实例 + fn create_test_parser() -> TolerantJsonParser { + TolerantJsonParser::new(None).expect("Failed to create test parser") + } + + #[test] + fn test_standard_json_parsing() { + let mut parser = create_test_parser(); + + // Test simple cases first + let simple_cases = vec![ + (r#"true"#, json!(true)), + (r#"false"#, json!(false)), + (r#"null"#, json!(null)), + (r#"42"#, json!(42)), + (r#""hello world""#, json!("hello world")), + ]; + + for (input, expected) in simple_cases { + let result = parser.parse(input); + assert!(result.is_ok(), "Failed to parse: {}", input); + + let (parsed_value, stats) = result.unwrap(); + println!("Input: {}, Parsed: {:?}, Expected: {:?}", input, parsed_value, expected); + assert_eq!(parsed_value, expected, "Mismatch for input: {}", input); + assert_eq!(stats.error_rate, 0.0, "Should have no errors for valid JSON"); + } + + // Test more complex cases + let complex_cases = vec![ + (r#"{"name": "test"}"#, json!({"name": "test"})), + (r#"[1, 2, 3]"#, json!([1, 2, 3])), + ]; + + for (input, expected) in complex_cases { + let result = parser.parse(input); + assert!(result.is_ok(), "Failed to parse: {}", input); + + let (parsed_value, _stats) = result.unwrap(); + println!("Input: {}, Parsed: {:?}, Expected: {:?}", input, parsed_value, expected); + assert_eq!(parsed_value, expected, "Mismatch for input: {}", input); + } + } + + #[test] + fn test_markdown_wrapped_json() { + let mut parser = create_test_parser(); + + // Simple test case + let input = r#"```json +{"name": "test"} +```"#; + let expected = json!({"name": "test"}); + + let result = parser.parse(input); + assert!(result.is_ok(), "Failed to parse markdown wrapped JSON: {}", input); + + let (parsed_value, _) = result.unwrap(); + println!("Markdown Input: {}", input); + println!("Parsed: {:?}", parsed_value); + println!("Expected: {:?}", expected); + assert_eq!(parsed_value, expected, "Mismatch for markdown input: {}", input); + } + + #[test] + fn test_empty_input() { + let mut parser = create_test_parser(); + + let result = parser.parse(""); + assert!(result.is_ok(), "Should handle empty input"); + + let (parsed_value, _) = result.unwrap(); + assert_eq!(parsed_value, json!(null), "Empty input should parse to null"); + } + + #[test] + fn test_cached_parser() { + let cached_parser = CachedTolerantJsonParser::new(None, Some(10)).unwrap(); + + // Test with simple object first + let test_json = r#"{"name": "test"}"#; + let expected = json!({"name": "test"}); + + println!("Testing cached parser with: {}", test_json); + + // First parse - should cache the result + let result1 = cached_parser.parse(test_json); + assert!(result1.is_ok(), "First parse failed"); + let (parsed1, _) = result1.unwrap(); + println!("First parse result: {:?}", parsed1); + assert_eq!(parsed1, expected); + + // Second parse - should use cache + let result2 = cached_parser.parse(test_json); + assert!(result2.is_ok(), "Second parse failed"); + let (parsed2, _) = result2.unwrap(); + println!("Second parse result: {:?}", parsed2); + assert_eq!(parsed2, expected); + + // Check cache stats + let (cache_size, max_size) = cached_parser.get_cache_stats(); + assert_eq!(cache_size, 1); + assert_eq!(max_size, 10); + + // Clear cache + cached_parser.clear_cache(); + let (cache_size_after_clear, _) = cached_parser.get_cache_stats(); + assert_eq!(cache_size_after_clear, 0); + } + + #[test] + fn test_error_recovery_strategies() { + let mut parser = create_test_parser(); + + // Test unquoted keys - simplified + let unquoted_json = r#"{name: "test"}"#; + let result = parser.parse(unquoted_json); + println!("Unquoted keys test input: {}", unquoted_json); + assert!(result.is_ok(), "Unquoted keys parsing failed"); + let (parsed, stats) = result.unwrap(); + println!("Unquoted keys result: {:?}", parsed); + println!("Recovery strategies used: {:?}", stats.recovery_strategies_used); + assert_eq!(parsed, json!({"name": "test"})); + + // Test trailing commas - simplified + let trailing_comma_json = r#"{"name": "test",}"#; + let result = parser.parse(trailing_comma_json); + println!("Trailing comma test input: {}", trailing_comma_json); + assert!(result.is_ok(), "Trailing comma parsing failed"); + let (parsed, stats) = result.unwrap(); + println!("Trailing comma result: {:?}", parsed); + println!("Recovery strategies used: {:?}", stats.recovery_strategies_used); + assert_eq!(parsed, json!({"name": "test"})); + } +} diff --git a/apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser_README.md b/apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser_README.md new file mode 100644 index 0000000..6dbf8e0 --- /dev/null +++ b/apps/desktop/src-tauri/src/infrastructure/tolerant_json_parser_README.md @@ -0,0 +1,295 @@ +# 容错JSON解析器 (Tolerant JSON Parser) + +基于Tree-sitter的大模型JSON容错解析器,专为处理大模型返回的不规范JSON数据而设计。 + +## 功能特性 + +### 🚀 核心功能 +- **容错解析**: 处理各种JSON格式错误,包括语法错误、格式不一致等 +- **多种恢复策略**: 标准JSON解析、手动修复、正则提取、部分解析 +- **Markdown支持**: 自动提取Markdown代码块中的JSON内容 +- **性能优化**: 内置缓存机制,支持高频解析场景 +- **详细统计**: 提供解析统计信息,包括错误率、恢复策略使用情况 + +### 🛠️ 支持的错误类型 +- **无引号键**: `{name: "value"}` → `{"name": "value"}` +- **尾随逗号**: `{"key": "value",}` → `{"key": "value"}` +- **单引号**: `{'key': 'value'}` → `{"key": "value"}` +- **Markdown包裹**: 自动提取 ```json 代码块中的内容 +- **混合内容**: 从包含解释性文本的内容中提取JSON +- **部分截断**: 处理不完整的JSON数据 + +## 快速开始 + +### Rust后端使用 + +```rust +use crate::infrastructure::tolerant_json_parser::{TolerantJsonParser, ParserConfig}; + +// 创建解析器实例 +let mut parser = TolerantJsonParser::new(None)?; + +// 解析JSON文本 +let json_text = r#" +这是一个JSON示例: +```json +{name: "test", value: 123,} +``` +"#; + +let (parsed_value, stats) = parser.parse(json_text)?; +println!("解析结果: {:?}", parsed_value); +println!("错误率: {:.2}%", stats.error_rate * 100.0); +println!("使用的恢复策略: {:?}", stats.recovery_strategies_used); +``` + +### 带缓存的解析器 + +```rust +use crate::infrastructure::tolerant_json_parser::CachedTolerantJsonParser; + +// 创建带缓存的解析器 +let cached_parser = CachedTolerantJsonParser::new(None, Some(1000))?; + +// 解析JSON(自动缓存) +let (result, stats) = cached_parser.parse(json_text)?; + +// 获取缓存统计 +let (cache_size, max_size) = cached_parser.get_cache_stats(); +println!("缓存使用: {}/{}", cache_size, max_size); +``` + +### 自定义配置 + +```rust +use crate::infrastructure::tolerant_json_parser::{ParserConfig, RecoveryStrategy}; + +let config = ParserConfig { + max_text_length: 1024 * 1024, // 1MB + enable_comments: true, + enable_unquoted_keys: true, + enable_trailing_commas: true, + timeout_ms: 30000, + recovery_strategies: vec![ + RecoveryStrategy::StandardJson, + RecoveryStrategy::ManualFix, + RecoveryStrategy::RegexExtract, + ], +}; + +let mut parser = TolerantJsonParser::new(Some(config))?; +``` + +## Tauri命令接口 + +### 前端调用示例 + +```typescript +import { invoke } from '@tauri-apps/api/core'; + +// 解析JSON文本 +async function parseJsonTolerant(text: string) { + try { + const response = await invoke('parse_json_tolerant', { + request: { + text: text, + config: { + max_text_length: 1024 * 1024, + enable_comments: true, + enable_unquoted_keys: true, + enable_trailing_commas: true, + timeout_ms: 30000, + recovery_strategies: ['StandardJson', 'ManualFix', 'RegexExtract'] + } + } + }); + + if (response.success) { + console.log('解析成功:', response.data); + console.log('统计信息:', response.statistics); + return response.data; + } else { + console.error('解析失败:', response.error); + return null; + } + } catch (error) { + console.error('调用失败:', error); + return null; + } +} + +// 验证JSON格式 +async function validateJson(text: string): Promise { + return await invoke('validate_json_format', { text }); +} + +// 格式化JSON +async function formatJson(text: string, indent: number = 2): Promise { + return await invoke('format_json_text', { text, indent }); +} + +// 获取支持的恢复策略 +async function getRecoveryStrategies(): Promise { + return await invoke('get_recovery_strategies'); +} + +// 获取默认配置 +async function getDefaultConfig() { + return await invoke('get_default_parser_config'); +} +``` + +### React组件示例 + +```tsx +import React, { useState } from 'react'; + +interface JsonParserProps { + onResult?: (result: any) => void; +} + +export const JsonParser: React.FC = ({ onResult }) => { + const [input, setInput] = useState(''); + const [result, setResult] = useState(null); + const [error, setError] = useState(null); + const [loading, setLoading] = useState(false); + + const handleParse = async () => { + if (!input.trim()) return; + + setLoading(true); + setError(null); + + try { + const response = await invoke('parse_json_tolerant', { + request: { text: input } + }); + + if (response.success) { + setResult(response.data); + onResult?.(response.data); + } else { + setError(response.error); + } + } catch (err) { + setError(err instanceof Error ? err.message : '解析失败'); + } finally { + setLoading(false); + } + }; + + return ( +
+
+