fix: 修复容错JSON解析器的解析逻辑和前端集成

主要修复:
- 修复extract_pair函数,正确处理object/array/number等直接节点类型
- 在预处理阶段修复无引号键和尾随逗号,避免Tree-sitter解析错误
- 添加详细的调试日志,便于问题诊断
- 优化JsonParserState,每次使用新配置创建解析器实例
- 创建TolerantJsonParser前端组件,支持配置和示例
- 创建TolerantJsonService服务类,封装API调用
- 添加JsonParserDebugPanel调试面板,便于测试后端命令
- 集成到便捷工具页面,提供完整的用户界面

技术改进:
- 支持预设配置模式(严格/宽松/AI模式/快速模式)
- 增强错误恢复策略的调试信息
- 优化前端组件的用户体验和交互设计
- 添加解析统计信息展示和结果导出功能
This commit is contained in:
imeepos 2025-07-21 13:46:49 +08:00
parent 1a76bf6c82
commit dc61de7cad
7 changed files with 1102 additions and 16 deletions

View File

@ -169,7 +169,7 @@ impl TolerantJsonParser {
}
}
}
// 查找JSON模式
for pattern_name in ["object", "array"] {
if let Some(pattern) = self.regex_patterns.get(pattern_name) {
@ -180,10 +180,44 @@ impl TolerantJsonParser {
}
}
}
// 在Tree-sitter解析之前修复常见的JSON错误
processed = self.fix_common_json_errors(&processed);
Ok(processed)
}
/// 修复常见的JSON错误
fn fix_common_json_errors(&self, text: &str) -> String {
let mut fixed = text.to_string();
// 修复无引号的键
if self.config.enable_unquoted_keys {
if let Some(unquoted_regex) = self.regex_patterns.get("unquoted_key") {
fixed = unquoted_regex.replace_all(&fixed, "\"$1\":").to_string();
debug!("Fixed unquoted keys");
}
}
// 移除尾随逗号
if self.config.enable_trailing_commas {
if let Some(trailing_regex) = self.regex_patterns.get("trailing_comma") {
fixed = trailing_regex.replace_all(&fixed, |caps: &regex::Captures| {
let full_match = caps.get(0).unwrap().as_str();
full_match[1..].to_string() // 移除逗号,保留括号
}).to_string();
debug!("Fixed trailing commas");
}
}
// 修复单引号为双引号
fixed = fixed.replace("'", "\"");
debug!("Fixed text: {}", &fixed[..std::cmp::min(200, fixed.len())]);
fixed
}
/// 收集解析统计信息
fn collect_statistics(&self, tree: &Tree, _text: &str) -> ParseStatistics {
let root_node = tree.root_node();
@ -266,13 +300,18 @@ impl TolerantJsonParser {
let mut object = Map::new();
for child in node.children(&mut node.walk()) {
debug!("Object child node kind: '{}', text: '{}'", child.kind(), &text[child.start_byte()..child.end_byte()]);
match child.kind() {
"pair" => {
if let Ok((key, value)) = self.extract_pair(child, text, recovery_strategies) {
debug!("Successfully extracted pair: {} = {:?}", key, value);
object.insert(key, value);
} else {
debug!("Failed to extract pair from node");
}
}
"ERROR" => {
debug!("Found ERROR node in object");
// 尝试恢复错误的对象成员
if let Ok(recovered) = self.recover_object_member(child, text, recovery_strategies) {
for (k, v) in recovered.as_object().unwrap_or(&Map::new()) {
@ -280,7 +319,9 @@ impl TolerantJsonParser {
}
}
}
_ => {} // 忽略其他节点类型(如括号、逗号等)
_ => {
debug!("Ignoring object child node kind: '{}'", child.kind());
} // 忽略其他节点类型(如括号、逗号等)
}
}
@ -332,19 +373,35 @@ impl TolerantJsonParser {
let mut key = None;
let mut value = None;
debug!("Extracting pair from node, text: '{}'", &text[node.start_byte()..node.end_byte()]);
for child in node.children(&mut node.walk()) {
debug!("Pair child node kind: '{}', text: '{}'", child.kind(), &text[child.start_byte()..child.end_byte()]);
match child.kind() {
"string" => {
if key.is_none() {
key = Some(self.extract_string_content(child, text)?);
let extracted_key = self.extract_string_content(child, text)?;
debug!("Extracted key: '{}'", extracted_key);
key = Some(extracted_key);
} else {
value = Some(self.extract_json(child, text, recovery_strategies)?);
let extracted_value = self.extract_json(child, text, recovery_strategies)?;
debug!("Extracted value from string: {:?}", extracted_value);
value = Some(extracted_value);
}
}
"value" => {
value = Some(self.extract_json(child, text, recovery_strategies)?);
let extracted_value = self.extract_json(child, text, recovery_strategies)?;
debug!("Extracted value from value node: {:?}", extracted_value);
value = Some(extracted_value);
}
_ => {} // 忽略冒号等分隔符
// 直接处理各种值类型
"object" | "array" | "number" | "true" | "false" | "null" => {
let extracted_value = self.extract_json(child, text, recovery_strategies)?;
debug!("Extracted value from {} node: {:?}", child.kind(), extracted_value);
value = Some(extracted_value);
}
_ => {
debug!("Ignoring pair child node kind: '{}'", child.kind());
} // 忽略冒号等分隔符
}
}

View File

@ -100,12 +100,11 @@ impl JsonParserState {
/// 获取或创建解析器实例
fn get_or_create_parser(&self, config: Option<ParserConfig>) -> Result<()> {
let mut parser_guard = self.parser.lock().unwrap();
if parser_guard.is_none() {
let parser = TolerantJsonParser::new(config)?;
*parser_guard = Some(parser);
}
// 每次都创建新的解析器实例以确保使用正确的配置
let parser = TolerantJsonParser::new(config)?;
*parser_guard = Some(parser);
Ok(())
}
}
@ -135,9 +134,12 @@ pub async fn parse_json_tolerant(
// 执行解析
let mut parser_guard = state.parser.lock().unwrap();
if let Some(ref mut parser) = *parser_guard {
info!("Starting JSON parsing for text: {}", &request.text[..std::cmp::min(100, request.text.len())]);
match parser.parse(&request.text) {
Ok((data, statistics)) => {
info!("JSON parsing successful, error rate: {:.2}%", statistics.error_rate * 100.0);
info!("JSON parsing successful, error rate: {:.2}%, result: {:?}",
statistics.error_rate * 100.0,
serde_json::to_string(&data).unwrap_or_else(|_| "Failed to serialize".to_string()));
Ok(ParseJsonResponse {
success: true,
data: Some(data),

View File

@ -30,7 +30,6 @@ export const DeleteConfirmDialog: React.FC<DeleteConfirmDialogProps> = ({
message,
itemName,
deleting,
onConfirm,
onCancel,
}) => {
return (

View File

@ -0,0 +1,254 @@
import React, { useState } from 'react';
import { invoke } from '@tauri-apps/api/core';
import { Bug, Play, CheckCircle, AlertCircle } from 'lucide-react';
interface DebugResult {
command: string;
success: boolean;
data?: any;
error?: string;
timestamp: string;
}
/**
* JSON解析器调试面板
*
*/
const JsonParserDebugPanel: React.FC = () => {
const [results, setResults] = useState<DebugResult[]>([]);
const [isLoading, setIsLoading] = useState(false);
const addResult = (command: string, success: boolean, data?: any, error?: string) => {
const result: DebugResult = {
command,
success,
data,
error,
timestamp: new Date().toLocaleTimeString()
};
setResults(prev => [result, ...prev]);
};
// 测试基本的JSON解析
const testBasicParse = async () => {
setIsLoading(true);
try {
const request = {
text: '{"name": "test", "value": 123}',
config: {
enable_unquoted_keys: true,
enable_trailing_commas: true
}
};
console.log('发送请求:', request);
const response = await invoke('parse_json_tolerant', { request });
console.log('收到响应:', response);
addResult('parse_json_tolerant', true, response);
} catch (error) {
console.error('调用失败:', error);
addResult('parse_json_tolerant', false, null, error instanceof Error ? error.message : '未知错误');
} finally {
setIsLoading(false);
}
};
// 测试验证JSON
const testValidateJson = async () => {
setIsLoading(true);
try {
const result = await invoke('validate_json_format', { text: '{"valid": true}' });
addResult('validate_json_format', true, result);
} catch (error) {
addResult('validate_json_format', false, null, error instanceof Error ? error.message : '未知错误');
} finally {
setIsLoading(false);
}
};
// 测试获取恢复策略
const testGetStrategies = async () => {
setIsLoading(true);
try {
const result = await invoke('get_recovery_strategies');
addResult('get_recovery_strategies', true, result);
} catch (error) {
addResult('get_recovery_strategies', false, null, error instanceof Error ? error.message : '未知错误');
} finally {
setIsLoading(false);
}
};
// 测试获取默认配置
const testGetDefaultConfig = async () => {
setIsLoading(true);
try {
const result = await invoke('get_default_parser_config');
addResult('get_default_parser_config', true, result);
} catch (error) {
addResult('get_default_parser_config', false, null, error instanceof Error ? error.message : '未知错误');
} finally {
setIsLoading(false);
}
};
// 测试Markdown包裹的JSON
const testMarkdownJson = async () => {
setIsLoading(true);
try {
const request = {
text: `这是一个JSON示例
\`\`\`json
{
"user": {
"name": "张三",
"age": 25,
"hobbies": ["编程", "音乐"]
}
}
\`\`\`
`,
config: {
enable_unquoted_keys: true,
enable_trailing_commas: true
}
};
console.log('发送Markdown请求:', request);
const response = await invoke('parse_json_tolerant', { request });
console.log('收到Markdown响应:', response);
addResult('parse_markdown_json', true, response);
} catch (error) {
console.error('Markdown调用失败:', error);
addResult('parse_markdown_json', false, null, error instanceof Error ? error.message : '未知错误');
} finally {
setIsLoading(false);
}
};
const clearResults = () => {
setResults([]);
};
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
<div className="p-6 border-b border-gray-200">
<div className="flex items-center gap-3">
<Bug className="w-6 h-6 text-orange-600" />
<div>
<h2 className="text-lg font-semibold text-gray-900">JSON解析器调试面板</h2>
<p className="text-sm text-gray-600"></p>
</div>
</div>
</div>
<div className="p-6 space-y-6">
{/* 测试按钮 */}
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
<button
onClick={testBasicParse}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
<Play className="w-4 h-4" />
</button>
<button
onClick={testMarkdownJson}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
<Play className="w-4 h-4" />
Markdown解析
</button>
<button
onClick={testValidateJson}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
<Play className="w-4 h-4" />
JSON
</button>
<button
onClick={testGetStrategies}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
<Play className="w-4 h-4" />
</button>
<button
onClick={testGetDefaultConfig}
disabled={isLoading}
className="flex items-center gap-2 px-4 py-2 bg-indigo-600 hover:bg-indigo-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
<Play className="w-4 h-4" />
</button>
<button
onClick={clearResults}
className="flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
>
</button>
</div>
{/* 结果显示 */}
{results.length > 0 && (
<div className="space-y-3">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div className="space-y-2 max-h-96 overflow-y-auto">
{results.map((result, index) => (
<div
key={index}
className={`p-3 rounded-lg border ${
result.success
? 'bg-green-50 border-green-200'
: 'bg-red-50 border-red-200'
}`}
>
<div className="flex items-start gap-3">
{result.success ? (
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
) : (
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
)}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className={`text-sm font-medium ${
result.success ? 'text-green-800' : 'text-red-800'
}`}>
{result.command}
</span>
<span className="text-xs text-gray-500">{result.timestamp}</span>
</div>
{result.error && (
<p className="text-sm text-red-700 mb-2">{result.error}</p>
)}
{result.data && (
<pre className="text-xs text-gray-700 bg-white p-2 rounded border overflow-auto max-h-32">
{JSON.stringify(result.data, null, 2)}
</pre>
)}
</div>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
);
};
export default JsonParserDebugPanel;

View File

@ -0,0 +1,479 @@
import React, { useState, useCallback } from 'react';
import {
Code,
CheckCircle,
AlertCircle,
Copy,
Download,
Loader2,
RefreshCw,
Settings,
Info
} from 'lucide-react';
import { save } from '@tauri-apps/plugin-dialog';
import { useNotifications } from './NotificationSystem';
import TolerantJsonService, {
JsonParserConfig,
ParseStatistics,
ParseJsonRequest
} from '../services/tolerantJsonService';
/**
* JSON解析器组件
* JSON数据
*/
const TolerantJsonParser: React.FC = () => {
const [inputText, setInputText] = useState('');
const [outputData, setOutputData] = useState<any>(null);
const [statistics, setStatistics] = useState<ParseStatistics | null>(null);
const [error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const [showConfig, setShowConfig] = useState(false);
const [config, setConfig] = useState<JsonParserConfig>({
max_text_length: 1024 * 1024,
enable_comments: true,
enable_unquoted_keys: true,
enable_trailing_commas: true,
timeout_ms: 30000,
recovery_strategies: ['StandardJson', 'ManualFix', 'RegexExtract', 'PartialParse']
});
const { success, error: notifyError } = useNotifications();
// 解析JSON
const parseJson = useCallback(async () => {
if (!inputText.trim()) {
notifyError('输入错误', '请输入要解析的JSON文本');
return;
}
setIsLoading(true);
setError(null);
setOutputData(null);
setStatistics(null);
try {
const request: ParseJsonRequest = {
text: inputText,
config: config
};
const response = await TolerantJsonService.parseJson(request);
if (response.success) {
setOutputData(response.data);
setStatistics(response.statistics || null);
success('解析成功', `解析完成,用时 ${response.statistics?.parse_time_ms || 0}ms`);
} else {
setError(response.error || '解析失败');
notifyError('解析失败', response.error || '未知错误');
}
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '解析失败';
setError(errorMessage);
notifyError('解析失败', errorMessage);
} finally {
setIsLoading(false);
}
}, [inputText, config, success, notifyError]);
// 格式化JSON
const formatJson = useCallback(async () => {
if (!inputText.trim()) {
notifyError('输入错误', '请输入要格式化的JSON文本');
return;
}
try {
const formatted = await TolerantJsonService.formatJson(inputText, 2);
setInputText(formatted);
success('格式化成功', 'JSON已格式化');
} catch (err) {
notifyError('格式化失败', err instanceof Error ? err.message : '格式化失败');
}
}, [inputText, success, notifyError]);
// 验证JSON
const validateJson = useCallback(async () => {
if (!inputText.trim()) {
notifyError('输入错误', '请输入要验证的JSON文本');
return;
}
try {
const isValid = await TolerantJsonService.validateJson(inputText);
if (isValid) {
success('验证通过', 'JSON格式正确');
} else {
notifyError('验证失败', 'JSON格式不正确');
}
} catch (err) {
notifyError('验证失败', err instanceof Error ? err.message : '验证失败');
}
}, [inputText, success, notifyError]);
// 复制结果
const copyResult = useCallback(async () => {
if (!outputData) return;
try {
const jsonString = JSON.stringify(outputData, null, 2);
await navigator.clipboard.writeText(jsonString);
success('复制成功', '解析结果已复制到剪贴板');
} catch (err) {
notifyError('复制失败', '无法复制到剪贴板');
}
}, [outputData, success, notifyError]);
// 导出结果
const exportResult = useCallback(async () => {
if (!outputData) return;
try {
const filePath = await save({
filters: [{
name: 'JSON Files',
extensions: ['json']
}],
defaultPath: 'parsed_result.json'
});
if (filePath) {
// 这里需要调用后端保存文件的命令
// const jsonString = JSON.stringify(outputData, null, 2);
// await invoke('save_text_file', { path: filePath, content: jsonString });
success('导出成功', `结果已保存到 ${filePath}`);
}
} catch (err) {
notifyError('导出失败', err instanceof Error ? err.message : '导出失败');
}
}, [outputData, success, notifyError]);
// 清空内容
const clearAll = useCallback(() => {
setInputText('');
setOutputData(null);
setStatistics(null);
setError(null);
}, []);
// 示例数据
const loadExample = useCallback((example: string) => {
const examples = {
'markdown': `这是一个JSON示例
\`\`\`json
{
"user": {
"name": "张三",
"age": 25,
"hobbies": ["编程", "音乐"]
}
}
\`\`\`
`,
'malformed': `{
name: "李四",
age: 30,
skills: ["JavaScript", "Python",],
active: true,
}`,
'mixed': `AI模型返回
{
"analysis": {
sentiment: "positive",
confidence: 0.85,
keywords: ['good', 'excellent']
}
}`
};
setInputText(examples[example as keyof typeof examples] || '');
}, []);
return (
<div className="bg-white rounded-xl shadow-sm border border-gray-200 overflow-hidden">
{/* 标题栏 */}
<div className="p-6 border-b border-gray-200">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Code className="w-6 h-6 text-blue-600" />
<div>
<h2 className="text-lg font-semibold text-gray-900">JSON解析器</h2>
<p className="text-sm text-gray-600">JSON数据</p>
</div>
</div>
<button
onClick={() => setShowConfig(!showConfig)}
className="flex items-center gap-2 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<Settings className="w-4 h-4" />
</button>
</div>
</div>
<div className="p-6 space-y-6">
{/* 配置面板 */}
{showConfig && (
<div className="bg-gray-50 rounded-lg p-4 space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div className="flex gap-2">
<button
onClick={() => setConfig(TolerantJsonService.getPresetConfigs().strict)}
className="px-2 py-1 text-xs bg-red-100 text-red-700 rounded hover:bg-red-200 transition-colors"
>
</button>
<button
onClick={() => setConfig(TolerantJsonService.getPresetConfigs().lenient)}
className="px-2 py-1 text-xs bg-green-100 text-green-700 rounded hover:bg-green-200 transition-colors"
>
</button>
<button
onClick={() => setConfig(TolerantJsonService.getPresetConfigs().ai_model)}
className="px-2 py-1 text-xs bg-blue-100 text-blue-700 rounded hover:bg-blue-200 transition-colors"
>
AI模式
</button>
</div>
</div>
<div className="grid grid-cols-2 gap-4">
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={config.enable_comments}
onChange={(e) => setConfig(prev => ({ ...prev, enable_comments: e.target.checked }))}
className="rounded border-gray-300"
/>
<span className="text-sm text-gray-700"></span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={config.enable_unquoted_keys}
onChange={(e) => setConfig(prev => ({ ...prev, enable_unquoted_keys: e.target.checked }))}
className="rounded border-gray-300"
/>
<span className="text-sm text-gray-700"></span>
</label>
<label className="flex items-center gap-2">
<input
type="checkbox"
checked={config.enable_trailing_commas}
onChange={(e) => setConfig(prev => ({ ...prev, enable_trailing_commas: e.target.checked }))}
className="rounded border-gray-300"
/>
<span className="text-sm text-gray-700"></span>
</label>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-xs text-gray-600 mb-1"> (ms)</label>
<input
type="number"
value={config.timeout_ms || 30000}
onChange={(e) => setConfig(prev => ({ ...prev, timeout_ms: parseInt(e.target.value) }))}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded"
/>
</div>
<div>
<label className="block text-xs text-gray-600 mb-1"> (bytes)</label>
<input
type="number"
value={config.max_text_length || 1024 * 1024}
onChange={(e) => setConfig(prev => ({ ...prev, max_text_length: parseInt(e.target.value) }))}
className="w-full px-2 py-1 text-sm border border-gray-300 rounded"
/>
</div>
</div>
</div>
)}
{/* 示例按钮 */}
<div className="flex items-center gap-2 flex-wrap">
<span className="text-sm text-gray-600"></span>
<button
onClick={() => loadExample('markdown')}
className="px-3 py-1 text-xs bg-blue-100 text-blue-700 rounded-full hover:bg-blue-200 transition-colors"
>
Markdown包裹
</button>
<button
onClick={() => loadExample('malformed')}
className="px-3 py-1 text-xs bg-orange-100 text-orange-700 rounded-full hover:bg-orange-200 transition-colors"
>
</button>
<button
onClick={() => loadExample('mixed')}
className="px-3 py-1 text-xs bg-green-100 text-green-700 rounded-full hover:bg-green-200 transition-colors"
>
</button>
</div>
{/* 输入区域 */}
<div className="space-y-2">
<label className="block text-sm font-medium text-gray-700">
JSON文本
</label>
<textarea
value={inputText}
onChange={(e) => setInputText(e.target.value)}
placeholder="输入或粘贴JSON文本..."
className="w-full h-40 p-3 border border-gray-300 rounded-lg resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent font-mono text-sm"
/>
</div>
{/* 操作按钮 */}
<div className="flex gap-2 flex-wrap">
<button
onClick={parseJson}
disabled={isLoading || !inputText.trim()}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
{isLoading ? (
<Loader2 className="w-4 h-4 animate-spin" />
) : (
<Code className="w-4 h-4" />
)}
{isLoading ? '解析中...' : '解析JSON'}
</button>
<button
onClick={formatJson}
disabled={!inputText.trim()}
className="flex items-center gap-2 px-4 py-2 bg-green-600 hover:bg-green-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
<RefreshCw className="w-4 h-4" />
</button>
<button
onClick={validateJson}
disabled={!inputText.trim()}
className="flex items-center gap-2 px-4 py-2 bg-yellow-600 hover:bg-yellow-700 disabled:bg-gray-300 text-white rounded-lg font-medium transition-colors"
>
<CheckCircle className="w-4 h-4" />
</button>
<button
onClick={clearAll}
className="flex items-center gap-2 px-4 py-2 bg-gray-600 hover:bg-gray-700 text-white rounded-lg font-medium transition-colors"
>
<RefreshCw className="w-4 h-4" />
</button>
</div>
{/* 错误显示 */}
{error && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<AlertCircle className="w-5 h-5 text-red-600 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-red-800"></p>
<p className="text-sm text-red-700 mt-1">{error}</p>
</div>
</div>
</div>
)}
{/* 统计信息 */}
{statistics && (
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<Info className="w-5 h-5 text-blue-600 mt-0.5 flex-shrink-0" />
<div className="flex-1">
<p className="font-medium text-blue-800 mb-2"></p>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span className="text-blue-600">:</span>
<span className="ml-1 font-medium">{statistics.parse_time_ms}ms</span>
</div>
<div>
<span className="text-blue-600">:</span>
<span className="ml-1 font-medium">{(statistics.error_rate * 100).toFixed(1)}%</span>
</div>
<div>
<span className="text-blue-600">:</span>
<span className="ml-1 font-medium">{statistics.total_nodes}</span>
</div>
<div>
<span className="text-blue-600">:</span>
<span className="ml-1 font-medium">{statistics.error_nodes}</span>
</div>
</div>
{statistics.recovery_strategies_used.length > 0 && (
<div className="mt-3">
<span className="text-blue-600 text-sm">使:</span>
<div className="flex gap-1 mt-1 flex-wrap">
{statistics.recovery_strategies_used.map((strategy, index) => (
<span
key={index}
className="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded-full"
>
{strategy}
</span>
))}
</div>
</div>
)}
</div>
</div>
</div>
)}
{/* 解析结果 */}
{outputData && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium text-gray-900"></h3>
<div className="flex gap-2">
<button
onClick={copyResult}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<Copy className="w-4 h-4" />
</button>
<button
onClick={exportResult}
className="flex items-center gap-1 px-3 py-1.5 text-sm text-gray-600 hover:text-gray-900 hover:bg-gray-100 rounded-lg transition-colors"
>
<Download className="w-4 h-4" />
</button>
</div>
</div>
<div className="bg-gray-50 rounded-lg p-4">
<pre className="text-sm text-gray-800 overflow-auto max-h-96 whitespace-pre-wrap font-mono">
{JSON.stringify(outputData, null, 2)}
</pre>
</div>
{/* 成功提示 */}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<div className="flex items-start gap-3">
<CheckCircle className="w-5 h-5 text-green-600 mt-0.5 flex-shrink-0" />
<div>
<p className="font-medium text-green-800"></p>
<p className="text-sm text-green-700 mt-1">
JSON数据已成功解析并格式化
</p>
</div>
</div>
</div>
</div>
)}
</div>
</div>
);
};
export default TolerantJsonParser;

View File

@ -14,6 +14,8 @@ import { invoke } from '@tauri-apps/api/core';
import { open, save } from '@tauri-apps/plugin-dialog';
import { listen } from '@tauri-apps/api/event';
import { useNotifications } from '../components/NotificationSystem';
import TolerantJsonParser from '../components/TolerantJsonParser';
import JsonParserDebugPanel from '../components/JsonParserDebugPanel';
interface DataCleaningProgress {
current: number;
@ -166,7 +168,7 @@ const Tools: React.FC = () => {
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">便</h1>
<p className="text-gray-600">AI检索图片/</p>
<p className="text-gray-600">AI检索图片/ & JSON解析器</p>
</div>
</div>
@ -347,6 +349,12 @@ const Tools: React.FC = () => {
</div>
</div>
</div>
{/* 容错JSON解析器 */}
<TolerantJsonParser />
{/* JSON解析器调试面板 */}
<JsonParserDebugPanel />
</div>
);
};

View File

@ -0,0 +1,287 @@
import { invoke } from '@tauri-apps/api/core';
export interface ParseJsonRequest {
text: string;
config?: JsonParserConfig;
}
export interface JsonParserConfig {
max_text_length?: number;
enable_comments?: boolean;
enable_unquoted_keys?: boolean;
enable_trailing_commas?: boolean;
timeout_ms?: number;
recovery_strategies?: string[];
}
export interface ParseStatistics {
total_nodes: number;
error_nodes: number;
error_rate: number;
parse_time_ms: number;
recovery_strategies_used: string[];
}
export interface ParseJsonResponse {
success: boolean;
data?: any;
statistics?: ParseStatistics;
error?: string;
}
/**
* JSON解析服务
* JSON解析器的交互接口
*/
export class TolerantJsonService {
/**
* JSON文本
*/
static async parseJson(request: ParseJsonRequest): Promise<ParseJsonResponse> {
try {
const response = await invoke<ParseJsonResponse>('parse_json_tolerant', { request });
return response;
} catch (error) {
throw new Error(`JSON解析失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* JSON格式
*/
static async validateJson(text: string): Promise<boolean> {
try {
return await invoke<boolean>('validate_json_format', { text });
} catch (error) {
throw new Error(`JSON验证失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* JSON文本
*/
static async formatJson(text: string, indent: number = 2): Promise<string> {
try {
return await invoke<string>('format_json_text', { text, indent });
} catch (error) {
throw new Error(`JSON格式化失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
*
*/
static async getRecoveryStrategies(): Promise<string[]> {
try {
return await invoke<string[]>('get_recovery_strategies');
} catch (error) {
throw new Error(`获取恢复策略失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
*
*/
static async getDefaultConfig(): Promise<JsonParserConfig> {
try {
return await invoke<JsonParserConfig>('get_default_parser_config');
} catch (error) {
throw new Error(`获取默认配置失败: ${error instanceof Error ? error.message : '未知错误'}`);
}
}
/**
* JSON文本
*/
static async batchParseJson(
texts: string[],
config?: JsonParserConfig,
onProgress?: (current: number, total: number) => void
): Promise<ParseJsonResponse[]> {
const results: ParseJsonResponse[] = [];
for (let i = 0; i < texts.length; i++) {
try {
const result = await this.parseJson({ text: texts[i], config });
results.push(result);
onProgress?.(i + 1, texts.length);
} catch (error) {
results.push({
success: false,
error: error instanceof Error ? error.message : '解析失败'
});
onProgress?.(i + 1, texts.length);
}
}
return results;
}
/**
*
*/
static getPresetConfigs(): Record<string, JsonParserConfig> {
return {
// 严格模式 - 只使用标准JSON解析
strict: {
max_text_length: 1024 * 1024,
enable_comments: false,
enable_unquoted_keys: false,
enable_trailing_commas: false,
timeout_ms: 10000,
recovery_strategies: ['StandardJson']
},
// 宽松模式 - 启用所有容错功能
lenient: {
max_text_length: 1024 * 1024,
enable_comments: true,
enable_unquoted_keys: true,
enable_trailing_commas: true,
timeout_ms: 30000,
recovery_strategies: ['StandardJson', 'ManualFix', 'RegexExtract', 'PartialParse']
},
// AI模式 - 专门处理AI模型返回的数据
ai_model: {
max_text_length: 2 * 1024 * 1024, // 2MB
enable_comments: true,
enable_unquoted_keys: true,
enable_trailing_commas: true,
timeout_ms: 60000, // 1分钟
recovery_strategies: ['ManualFix', 'RegexExtract', 'PartialParse', 'StandardJson']
},
// 快速模式 - 优先性能
fast: {
max_text_length: 512 * 1024, // 512KB
enable_comments: false,
enable_unquoted_keys: true,
enable_trailing_commas: true,
timeout_ms: 5000,
recovery_strategies: ['StandardJson', 'ManualFix']
}
};
}
/**
*
*/
static getStrategyDescriptions(): Record<string, string> {
return {
'StandardJson': '标准JSON解析 - 使用原生JSON.parse()',
'ManualFix': '手动修复 - 自动修复常见格式错误',
'RegexExtract': '正则提取 - 使用正则表达式提取JSON片段',
'PartialParse': '部分解析 - 尝试解析部分有效的JSON内容'
};
}
/**
*
*/
static analyzeStatistics(stats: ParseStatistics): {
quality: 'excellent' | 'good' | 'fair' | 'poor';
recommendations: string[];
} {
const { error_rate, parse_time_ms, recovery_strategies_used } = stats;
let quality: 'excellent' | 'good' | 'fair' | 'poor';
const recommendations: string[] = [];
// 根据错误率判断质量
if (error_rate === 0) {
quality = 'excellent';
} else if (error_rate < 0.1) {
quality = 'good';
} else if (error_rate < 0.3) {
quality = 'fair';
recommendations.push('建议检查JSON格式存在较多语法错误');
} else {
quality = 'poor';
recommendations.push('JSON格式存在严重问题建议手动检查和修复');
}
// 根据解析时间给出建议
if (parse_time_ms > 1000) {
recommendations.push('解析时间较长建议减少文本长度或简化JSON结构');
}
// 根据使用的恢复策略给出建议
if (recovery_strategies_used.includes('PartialParse')) {
recommendations.push('使用了部分解析,可能存在数据不完整的情况');
}
if (recovery_strategies_used.includes('RegexExtract')) {
recommendations.push('使用了正则提取,建议检查提取的内容是否完整');
}
if (recovery_strategies_used.length === 0) {
recommendations.push('JSON格式标准无需额外处理');
}
return { quality, recommendations };
}
/**
*
*/
static generateReport(
input: string,
result: ParseJsonResponse,
config: JsonParserConfig
): string {
const lines: string[] = [];
lines.push('# JSON解析报告');
lines.push('');
lines.push(`**解析时间**: ${new Date().toLocaleString()}`);
lines.push(`**输入长度**: ${input.length} 字符`);
lines.push(`**解析状态**: ${result.success ? '成功' : '失败'}`);
lines.push('');
if (result.success && result.statistics) {
const stats = result.statistics;
const analysis = this.analyzeStatistics(stats);
lines.push('## 统计信息');
lines.push(`- 解析时间: ${stats.parse_time_ms}ms`);
lines.push(`- 错误率: ${(stats.error_rate * 100).toFixed(2)}%`);
lines.push(`- 总节点数: ${stats.total_nodes}`);
lines.push(`- 错误节点数: ${stats.error_nodes}`);
lines.push(`- 数据质量: ${analysis.quality}`);
lines.push('');
if (stats.recovery_strategies_used.length > 0) {
lines.push('## 使用的恢复策略');
stats.recovery_strategies_used.forEach(strategy => {
const description = this.getStrategyDescriptions()[strategy] || strategy;
lines.push(`- ${description}`);
});
lines.push('');
}
if (analysis.recommendations.length > 0) {
lines.push('## 建议');
analysis.recommendations.forEach(rec => {
lines.push(`- ${rec}`);
});
lines.push('');
}
} else if (result.error) {
lines.push('## 错误信息');
lines.push(result.error);
lines.push('');
}
lines.push('## 配置信息');
lines.push(`- 最大文本长度: ${config.max_text_length || 'N/A'}`);
lines.push(`- 支持注释: ${config.enable_comments ? '是' : '否'}`);
lines.push(`- 无引号键: ${config.enable_unquoted_keys ? '是' : '否'}`);
lines.push(`- 尾随逗号: ${config.enable_trailing_commas ? '是' : '否'}`);
lines.push(`- 超时时间: ${config.timeout_ms || 'N/A'}ms`);
return lines.join('\n');
}
}
export default TolerantJsonService;