diff --git a/backend/app/api/endpoints/ai_analyze.py b/backend/app/api/endpoints/ai_analyze.py index 49ab0cd..7bdd930 100644 --- a/backend/app/api/endpoints/ai_analyze.py +++ b/backend/app/api/endpoints/ai_analyze.py @@ -215,9 +215,12 @@ async def analyze_markdown( return result finally: - # 清理临时文件 - if os.path.exists(tmp_path): - os.unlink(tmp_path) + # 清理临时文件,确保在所有情况下都能清理 + try: + if tmp_path and os.path.exists(tmp_path): + os.unlink(tmp_path) + except Exception as cleanup_error: + logger.warning(f"临时文件清理失败: {tmp_path}, error: {cleanup_error}") except HTTPException: raise @@ -279,8 +282,12 @@ async def analyze_markdown_stream( ) finally: - if os.path.exists(tmp_path): - os.unlink(tmp_path) + # 清理临时文件,确保在所有情况下都能清理 + try: + if tmp_path and os.path.exists(tmp_path): + os.unlink(tmp_path) + except Exception as cleanup_error: + logger.warning(f"临时文件清理失败: {tmp_path}, error: {cleanup_error}") except HTTPException: raise @@ -289,7 +296,7 @@ async def analyze_markdown_stream( raise HTTPException(status_code=500, detail=f"流式分析失败: {str(e)}") -@router.get("/analyze/md/outline") +@router.post("/analyze/md/outline") async def get_markdown_outline( file: UploadFile = File(...) ): @@ -323,8 +330,12 @@ async def get_markdown_outline( result = await markdown_ai_service.extract_outline(tmp_path) return result finally: - if os.path.exists(tmp_path): - os.unlink(tmp_path) + # 清理临时文件,确保在所有情况下都能清理 + try: + if tmp_path and os.path.exists(tmp_path): + os.unlink(tmp_path) + except Exception as cleanup_error: + logger.warning(f"临时文件清理失败: {tmp_path}, error: {cleanup_error}") except Exception as e: logger.error(f"获取 Markdown 大纲失败: {str(e)}") diff --git a/backend/app/api/endpoints/health.py b/backend/app/api/endpoints/health.py index 2f239be..00f2049 100644 --- a/backend/app/api/endpoints/health.py +++ b/backend/app/api/endpoints/health.py @@ -19,26 +19,43 @@ async def health_check() -> Dict[str, Any]: 返回各数据库连接状态和应用信息 """ # 检查各数据库连接状态 - mysql_status = "connected" - mongodb_status = "connected" - redis_status = "connected" + mysql_status = "unknown" + mongodb_status = "unknown" + redis_status = "unknown" try: if mysql_db.async_engine is None: mysql_status = "disconnected" - except Exception: + else: + # 实际执行一次查询验证连接 + from sqlalchemy import text + async with mysql_db.async_engine.connect() as conn: + await conn.execute(text("SELECT 1")) + mysql_status = "connected" + except Exception as e: + logger.warning(f"MySQL 健康检查失败: {e}") mysql_status = "error" try: if mongodb.client is None: mongodb_status = "disconnected" - except Exception: + else: + # 实际 ping 验证 + await mongodb.client.admin.command('ping') + mongodb_status = "connected" + except Exception as e: + logger.warning(f"MongoDB 健康检查失败: {e}") mongodb_status = "error" try: - if not redis_db.is_connected: + if not redis_db.is_connected or redis_db.client is None: redis_status = "disconnected" - except Exception: + else: + # 实际执行 ping 验证 + await redis_db.client.ping() + redis_status = "connected" + except Exception as e: + logger.warning(f"Redis 健康检查失败: {e}") redis_status = "error" return { diff --git a/backend/app/api/endpoints/upload.py b/backend/app/api/endpoints/upload.py index d9d9ada..ca9c8df 100644 --- a/backend/app/api/endpoints/upload.py +++ b/backend/app/api/endpoints/upload.py @@ -5,6 +5,7 @@ from fastapi import APIRouter, UploadFile, File, HTTPException, Query from fastapi.responses import StreamingResponse from typing import Optional import logging +import os import pandas as pd import io @@ -126,7 +127,7 @@ async def upload_excel( content += f"... (共 {len(sheet_data['rows'])} 行)\n\n" doc_metadata = { - "filename": saved_path.split("/")[-1] if "/" in saved_path else saved_path.split("\\")[-1], + "filename": os.path.basename(saved_path), "original_filename": file.filename, "saved_path": saved_path, "file_size": len(content), @@ -253,7 +254,7 @@ async def export_excel( output.seek(0) # 生成文件名 - original_name = file_path.split('/')[-1] if '/' in file_path else file_path + original_name = os.path.basename(file_path) if columns: export_name = f"export_{sheet_name or 'data'}_{len(column_list) if columns else 'all'}_cols.xlsx" else: diff --git a/backend/app/instruction/__init__.py b/backend/app/instruction/__init__.py index e69de29..1386f3d 100644 --- a/backend/app/instruction/__init__.py +++ b/backend/app/instruction/__init__.py @@ -0,0 +1,15 @@ +""" +指令执行模块 + +注意: 此模块为可选功能,当前尚未实现。 +如需启用,请实现 intent_parser.py 和 executor.py +""" +from .intent_parser import IntentParser, DefaultIntentParser +from .executor import InstructionExecutor, DefaultInstructionExecutor + +__all__ = [ + "IntentParser", + "DefaultIntentParser", + "InstructionExecutor", + "DefaultInstructionExecutor", +] diff --git a/backend/app/instruction/executor.py b/backend/app/instruction/executor.py index e69de29..36292ce 100644 --- a/backend/app/instruction/executor.py +++ b/backend/app/instruction/executor.py @@ -0,0 +1,35 @@ +""" +指令执行器模块 + +将自然语言指令转换为可执行操作 + +注意: 此模块为可选功能,当前尚未实现。 +""" +from abc import ABC, abstractmethod +from typing import Any, Dict + + +class InstructionExecutor(ABC): + """指令执行器抽象基类""" + + @abstractmethod + async def execute(self, instruction: str, context: Dict[str, Any]) -> Dict[str, Any]: + """ + 执行指令 + + Args: + instruction: 解析后的指令 + context: 执行上下文 + + Returns: + 执行结果 + """ + pass + + +class DefaultInstructionExecutor(InstructionExecutor): + """默认指令执行器""" + + async def execute(self, instruction: str, context: Dict[str, Any]) -> Dict[str, Any]: + """暂未实现""" + raise NotImplementedError("指令执行功能暂未实现") diff --git a/backend/app/instruction/intent_parser.py b/backend/app/instruction/intent_parser.py index e69de29..49df250 100644 --- a/backend/app/instruction/intent_parser.py +++ b/backend/app/instruction/intent_parser.py @@ -0,0 +1,34 @@ +""" +意图解析器模块 + +解析用户自然语言指令,识别意图和参数 + +注意: 此模块为可选功能,当前尚未实现。 +""" +from abc import ABC, abstractmethod +from typing import Any, Dict, Tuple + + +class IntentParser(ABC): + """意图解析器抽象基类""" + + @abstractmethod + async def parse(self, text: str) -> Tuple[str, Dict[str, Any]]: + """ + 解析自然语言指令 + + Args: + text: 用户输入的自然语言 + + Returns: + (意图类型, 参数字典) + """ + pass + + +class DefaultIntentParser(IntentParser): + """默认意图解析器""" + + async def parse(self, text: str) -> Tuple[str, Dict[str, Any]]: + """暂未实现""" + raise NotImplementedError("意图解析功能暂未实现") diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index e764335..44ccbb5 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { RouterProvider } from 'react-router-dom'; -import { AuthProvider } from '@/context/AuthContext'; +import { AuthProvider } from '@/contexts/AuthContext'; import { TemplateFillProvider } from '@/context/TemplateFillContext'; import { router } from '@/routes'; import { Toaster } from 'sonner'; diff --git a/frontend/src/components/common/RouteGuard.tsx b/frontend/src/components/common/RouteGuard.tsx index 0b691e0..8a4288b 100644 --- a/frontend/src/components/common/RouteGuard.tsx +++ b/frontend/src/components/common/RouteGuard.tsx @@ -1,6 +1,6 @@ import React from 'react'; import { Navigate, useLocation } from 'react-router-dom'; -import { useAuth } from '@/context/AuthContext'; +import { useAuth } from '@/contexts/AuthContext'; export const RouteGuard: React.FC<{ children: React.ReactNode }> = ({ children }) => { const { user, loading } = useAuth(); diff --git a/frontend/src/context/AuthContext.tsx b/frontend/src/context/AuthContext.tsx deleted file mode 100644 index 524dc8d..0000000 --- a/frontend/src/context/AuthContext.tsx +++ /dev/null @@ -1,85 +0,0 @@ -import React, { createContext, useContext, useEffect, useState } from 'react'; -import { supabase } from '@/db/supabase'; -import { User } from '@supabase/supabase-js'; -import { Profile } from '@/types/types'; - -interface AuthContextType { - user: User | null; - profile: Profile | null; - signIn: (email: string, password: string) => Promise<{ error: any }>; - signUp: (email: string, password: string) => Promise<{ error: any }>; - signOut: () => Promise<{ error: any }>; - loading: boolean; -} - -const AuthContext = createContext(undefined); - -export const AuthProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { - const [user, setUser] = useState(null); - const [profile, setProfile] = useState(null); - const [loading, setLoading] = useState(true); - - useEffect(() => { - // Check active sessions and sets the user - supabase.auth.getSession().then(({ data: { session } }) => { - setUser(session?.user ?? null); - if (session?.user) fetchProfile(session.user.id); - else setLoading(false); - }); - - // Listen for changes on auth state (sign in, sign out, etc.) - const { data: { subscription } } = supabase.auth.onAuthStateChange((_event, session) => { - setUser(session?.user ?? null); - if (session?.user) fetchProfile(session.user.id); - else { - setProfile(null); - setLoading(false); - } - }); - - return () => subscription.unsubscribe(); - }, []); - - const fetchProfile = async (uid: string) => { - try { - const { data, error } = await supabase - .from('profiles') - .select('*') - .eq('id', uid) - .maybeSingle(); - - if (error) throw error; - setProfile(data); - } catch (err) { - console.error('Error fetching profile:', err); - } finally { - setLoading(false); - } - }; - - const signIn = async (email: string, password: string) => { - return await supabase.auth.signInWithPassword({ email, password }); - }; - - const signUp = async (email: string, password: string) => { - return await supabase.auth.signUp({ email, password }); - }; - - const signOut = async () => { - return await supabase.auth.signOut(); - }; - - return ( - - {children} - - ); -}; - -export const useAuth = () => { - const context = useContext(AuthContext); - if (context === undefined) { - throw new Error('useAuth must be used within an AuthProvider'); - } - return context; -}; diff --git a/frontend/src/db/backend-api.ts b/frontend/src/db/backend-api.ts index 59cc0ea..db5854d 100644 --- a/frontend/src/db/backend-api.ts +++ b/frontend/src/db/backend-api.ts @@ -1188,7 +1188,7 @@ export const aiApi = { try { const response = await fetch(url, { - method: 'GET', + method: 'POST', body: formData, }); diff --git a/frontend/src/pages/ExcelParse.tsx b/frontend/src/pages/ExcelParse.tsx deleted file mode 100644 index 8556025..0000000 --- a/frontend/src/pages/ExcelParse.tsx +++ /dev/null @@ -1,1015 +0,0 @@ -import React, { useState, useEffect } from 'react'; -import { useDropzone } from 'react-dropzone'; -import { - FileSpreadsheet, - Upload, - Trash2, - ChevronDown, - ChevronUp, - Table, - Info, - CheckCircle, - AlertCircle, - Loader2, - Sparkles, - FileText, - TrendingUp, - Download, - Brain, - Check, - X -} from 'lucide-react'; -import { Button } from '@/components/ui/button'; -import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card'; -import { Badge } from '@/components/ui/badge'; -import { Input } from '@/components/ui/input'; -import { Label } from '@/components/ui/label'; -import { Switch } from '@/components/ui/switch'; -import { Textarea } from '@/components/ui/textarea'; -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; -import { Checkbox } from '@/components/ui/checkbox'; -import { toast } from 'sonner'; -import { cn } from '@/lib/utils'; -import { backendApi, type ExcelParseResult, type ExcelUploadOptions, aiApi } from '@/db/backend-api'; -import { - Table as TableComponent, - TableBody, - TableCell, - TableHead, - TableHeader, - TableRow, -} from '@/components/ui/table'; -import { Markdown } from '@/components/ui/markdown'; -import { AIChartDisplay } from '@/components/ui/ai-chart-display'; -import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; - -const ExcelParse: React.FC = () => { - const [loading, setLoading] = useState(false); - const [analyzing, setAnalyzing] = useState(false); - const [analyzingForCharts, setAnalyzingForCharts] = useState(false); - const [exporting, setExporting] = useState(false); - const [parseResult, setParseResult] = useState(null); - const [aiAnalysis, setAiAnalysis] = useState(null); - const [analysisCharts, setAnalysisCharts] = useState(null); - const [uploadedFile, setUploadedFile] = useState(null); - const [expandedSheet, setExpandedSheet] = useState(null); - const [parseOptions, setParseOptions] = useState({ - parseAllSheets: false, - headerRow: 0 - }); - const [aiOptions, setAiOptions] = useState({ - userPrompt: '', - analysisType: 'general' as 'general' | 'summary' | 'statistics' | 'insights', - parseAllSheetsForAI: false - }); - const [analysisTypes, setAnalysisTypes] = useState>([]); - - // 导出相关状态 - const [exportDialogOpen, setExportDialogOpen] = useState(false); - const [selectedSheet, setSelectedSheet] = useState(''); - const [selectedColumns, setSelectedColumns] = useState>(new Set()); - const [selectAll, setSelectAll] = useState(false); - - // 获取支持的分析类型 - useEffect(() => { - aiApi.getAnalysisTypes() - .then(data => setAnalysisTypes(data.types)) - .catch(() => { - setAnalysisTypes([ - { value: 'general', label: '综合分析', description: '提供数据概览、关键发现、质量评估和建议' }, - { value: 'summary', label: '数据摘要', description: '快速了解数据的结构、范围和主要内容' }, - { value: 'statistics', label: '统计分析', description: '数值型列的统计信息和分类列的分布' }, - { value: 'insights', label: '深度洞察', description: '深入挖掘数据,提供异常值和业务建议' } - ]); - }); - }, []); - - const onDrop = async (acceptedFiles: File[]) => { - const file = acceptedFiles[0]; - if (!file) return; - - if (!file.name.match(/\.(xlsx|xls)$/i)) { - toast.error('仅支持 .xlsx 和 .xls 格式的 Excel 文件'); - return; - } - - setUploadedFile(file); - setLoading(true); - setParseResult(null); - setAiAnalysis(null); - setAnalysisCharts(null); - setExpandedSheet(null); - - try { - const result = await backendApi.uploadExcel(file, parseOptions); - - if (result.success) { - toast.success(`解析成功: ${file.name}`); - setParseResult(result); - // 自动展开第一个工作表 - if (result.metadata?.sheet_count === 1) { - setExpandedSheet(null); - } - } else { - toast.error(result.error || '解析失败'); - } - } catch (error: any) { - toast.error(error.message || '上传失败'); - } finally { - setLoading(false); - } - }; - - const handleAnalyze = async () => { - if (!uploadedFile || !parseResult?.success) { - toast.error('请先上传并解析 Excel 文件'); - return; - } - - setAnalyzing(true); - setAiAnalysis(null); - setAnalysisCharts(null); - - try { - const result = await aiApi.analyzeExcel(uploadedFile, { - userPrompt: aiOptions.userPrompt, - analysisType: aiOptions.analysisType, - parseAllSheets: aiOptions.parseAllSheetsForAI - }); - - if (result.success) { - toast.success('AI 分析完成'); - setAiAnalysis(result); - } else { - toast.error(result.error || 'AI 分析失败'); - } - } catch (error: any) { - toast.error(error.message || 'AI 分析失败'); - } finally { - setAnalyzing(false); - } - }; - - const handleGenerateChartsFromAnalysis = async () => { - if (!aiAnalysis || !aiAnalysis.success) { - toast.error('请先进行 AI 分析'); - return; - } - - // 提取 AI 分析文本 - let analysisText = ''; - - if (aiAnalysis.analysis?.analysis) { - analysisText = aiAnalysis.analysis.analysis; - } else if (aiAnalysis.analysis?.sheets) { - // 多工作表模式,合并所有工作表的分析结果 - const sheetAnalyses = aiAnalysis.analysis.sheets; - if (sheetAnalyses && Object.keys(sheetAnalyses).length > 0) { - const firstSheet = Object.keys(sheetAnalyses)[0]; - analysisText = sheetAnalyses[firstSheet]?.analysis || ''; - } - } - - if (!analysisText || !analysisText.trim()) { - toast.error('无法获取 AI 分析结果'); - return; - } - - setAnalyzingForCharts(true); - setAnalysisCharts(null); - - try { - const result = await aiApi.extractAndGenerateCharts({ - analysis_text: analysisText, - original_filename: uploadedFile?.name || 'unknown', - file_type: 'excel' - }); - - if (result.success) { - toast.success('基于 AI 分析的图表生成完成'); - setAnalysisCharts(result); - } else { - toast.error(result.error || '图表生成失败'); - } - } catch (error: any) { - toast.error(error.message || '图表生成失败'); - } finally { - setAnalyzingForCharts(false); - } - }; - - // 获取工作表数据 - const getSheetData = (sheetName: string) => { - if (!parseResult?.success || !parseResult.data) return null; - - const data = parseResult.data; - - // 多工作表模式 - if (data.sheets && data.sheets[sheetName]) { - return data.sheets[sheetName]; - } - - // 单工作表模式 - if (!data.sheets && data.columns && data.rows) { - return data; - } - - return null; - }; - - // 打开导出对话框 - const openExportDialog = () => { - if (!parseResult?.success || !parseResult.data) { - toast.error('请先上传并解析 Excel 文件'); - return; - } - - const data = parseResult.data; - - // 获取所有工作表 - let sheets: string[] = []; - if (data.sheets) { - sheets = Object.keys(data.sheets); - } else { - sheets = ['默认工作表']; - } - - setSelectedSheet(sheets[0]); - const sheetColumns = getSheetData(sheets[0])?.columns || []; - setSelectedColumns(new Set(sheetColumns)); - setSelectAll(true); - setExportDialogOpen(true); - }; - - // 处理列选择 - const toggleColumn = (column: string) => { - const newSelected = new Set(selectedColumns); - if (newSelected.has(column)) { - newSelected.delete(column); - } else { - newSelected.add(column); - } - setSelectedColumns(newSelected); - setSelectAll(newSelected.size === (getSheetData(selectedSheet)?.columns || []).length); - }; - - // 全选/取消全选 - const toggleSelectAll = () => { - const sheetColumns = getSheetData(selectedSheet)?.columns || []; - if (selectAll) { - setSelectedColumns(new Set()); - } else { - setSelectedColumns(new Set(sheetColumns)); - } - setSelectAll(!selectAll); - }; - - // 执行导出 - const handleExport = async () => { - if (selectedColumns.size === 0) { - toast.error('请至少选择一列'); - return; - } - - if (!parseResult?.metadata?.saved_path) { - toast.error('无法获取文件路径'); - return; - } - - setExporting(true); - - try { - const blob = await backendApi.exportExcel( - parseResult.metadata.saved_path, - { - columns: Array.from(selectedColumns), - sheetName: selectedSheet === '默认工作表' ? undefined : selectedSheet - } - ); - - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `export_${selectedSheet}_${uploadedFile?.name || 'data.xlsx'}`; - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - URL.revokeObjectURL(url); - - toast.success('导出成功'); - setExportDialogOpen(false); - } catch (error: any) { - toast.error(error.message || '导出失败'); - } finally { - setExporting(false); - } - }; - - const { getRootProps, getInputProps, isDragActive } = useDropzone({ - onDrop, - accept: { - 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': ['.xlsx'], - 'application/vnd.ms-excel': ['.xls'] - }, - maxFiles: 1 - }); - - const handleDeleteFile = () => { - setUploadedFile(null); - setParseResult(null); - setAiAnalysis(null); - setAnalysisCharts(null); - setExpandedSheet(null); - toast.success('文件已清除'); - }; - - const formatFileSize = (bytes: number): string => { - if (bytes === 0) return '0 B'; - const k = 1024; - const sizes = ['B', 'KB', 'MB', 'GB']; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / Math.pow(k, i)).toFixed(2)} ${sizes[i]}`; - }; - - const getAnalysisIcon = (type: string) => { - switch (type) { - case 'general': - return ; - case 'summary': - return ; - case 'statistics': - return ; - case 'insights': - return ; - default: - return ; - } - }; - - const downloadAnalysis = () => { - if (!aiAnalysis?.analysis?.analysis) return; - - const content = aiAnalysis.analysis.analysis; - const blob = new Blob([content], { type: 'text/plain;charset=utf-8' }); - const url = URL.createObjectURL(blob); - const link = document.createElement('a'); - link.href = url; - link.download = `AI分析结果_${uploadedFile?.name || 'excel'}.txt`; - link.click(); - URL.revokeObjectURL(url); - toast.success('分析结果已下载'); - }; - - return ( -
-
-
-

- - Excel 智能分析工具 -

-

上传 Excel 文件,使用 AI 进行深度数据分析。

-
-
- -
- {/* 左侧:上传区域 */} -
- {/* 上传卡片 */} - - - - - 文件上传 - - - 拖拽或点击上传 Excel 文件 - - - - {!uploadedFile ? ( -
- -
- {loading ? : } -
-

- {isDragActive ? '释放以开始上传' : '点击或拖拽文件到这里'} -

-

支持 .xlsx 和 .xls 格式

-
- ) : ( -
-
-
- -
-
-

{uploadedFile.name}

-

{formatFileSize(uploadedFile.size)}

-
- -
- -
- )} -
-
- - {/* 解析选项卡片 */} - - - - - 解析选项 - - - 配置 Excel 文件的解析方式 - - - -
- - setParseOptions({ ...parseOptions, parseAllSheets: checked })} - /> -
-
- - setParseOptions({ ...parseOptions, headerRow: parseInt(e.target.value) || 0 })} - className="bg-background" - /> -

- 从 0 开始,0 表示第一行 -

-
-
-
- - {/* AI 分析选项卡片 */} - - - - - AI 分析选项 - - - 配置 AI 分析的方式 - - - -
- - -
-
- -