diff --git a/README.md b/README.md index 2f0b613..c99b69b 100644 --- a/README.md +++ b/README.md @@ -347,6 +347,38 @@ VITE_QWEN_API_KEY=key3 ``` +
+如何查看系统日志和调试信息? + +XCodeReviewer 内置了日志系统,记录核心操作和错误: + +**查看日志**: +- 导航栏 -> 系统日志 +- 或访问:`http://localhost:5173/logs` (开发) / `http://localhost:8888/logs` (生产) + +**记录内容**: +- ✅ 用户核心操作(创建项目、审计任务、修改配置等) +- ✅ API 请求失败和错误 +- ✅ 控制台错误(自动捕获) +- ✅ 未处理的异常 + +**功能特性**: +- 日志筛选、搜索 +- 导出日志(JSON/CSV) +- 错误详情查看 + +**手动记录用户操作**: +```typescript +import { logger, LogCategory } from '@/shared/utils/logger'; + +// 记录用户操作 +logger.logUserAction('创建项目', { projectName, projectType }); +logger.logUserAction('开始审计', { taskId, fileCount }); +``` + +详细说明请参考:[LOGGING_README.md](LOGGING_README.md) +
+ ### 🔑 获取 API Key #### 支持的 LLM 平台 diff --git a/README_EN.md b/README_EN.md index 6d0933a..c4fe75b 100644 --- a/README_EN.md +++ b/README_EN.md @@ -13,7 +13,7 @@
-[![Version](https://img.shields.io/badge/version-1.2.0-blue.svg)](https://github.com/lintsinghua/XCodeReviewer/releases) +[![Version](https://img.shields.io/badge/version-1.3.0-blue.svg)](https://github.com/lintsinghua/XCodeReviewer/releases) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT) [![React](https://img.shields.io/badge/React-18-61dafb.svg)](https://reactjs.org/) [![TypeScript](https://img.shields.io/badge/TypeScript-5.7-3178c6.svg)](https://www.typescriptlang.org/) @@ -347,6 +347,37 @@ VITE_QWEN_API_KEY=key3 ``` +
+How to view system logs and debug information? + +XCodeReviewer has a built-in logging system that records core operations and errors: + +**View Logs**: +- Navigation bar -> System Logs +- Or visit: `http://localhost:5173/logs` (dev) / `http://localhost:8888/logs` (prod) + +**Recorded Content**: +- ✅ Core user operations (create project, audit tasks, config changes, etc.) +- ✅ Failed API requests and errors +- ✅ Console errors (auto-captured) +- ✅ Unhandled exceptions + +**Features**: +- Log filtering and search +- Export logs (JSON/CSV) +- View error details + +**Manual Logging**: +```typescript +import { logger, LogCategory } from '@/shared/utils/logger'; + +// Log user actions +logger.logUserAction('Create Project', { projectName, projectType }); +logger.logUserAction('Start Audit', { taskId, fileCount }); +``` + +For details, see: [LOGGING_README.md](LOGGING_README.md) +
### 🔑 Getting API Keys diff --git a/package.json b/package.json index 42a04ff..0209266 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "xcode-reviewer", - "version": "1.2.0", + "version": "1.3.0", "type": "module", "scripts": { "dev": "vite", diff --git a/src/app/main.tsx b/src/app/main.tsx index 9b256b0..952a964 100644 --- a/src/app/main.tsx +++ b/src/app/main.tsx @@ -5,6 +5,8 @@ import App from "./App.tsx"; import { AppWrapper } from "@/components/layout/PageMeta"; import { isLocalMode } from "@/shared/config/database"; import { initLocalDatabase } from "@/shared/utils/initLocalDB"; +import { ErrorBoundary } from "@/components/common/ErrorBoundary"; +import "@/shared/utils/fetchWrapper"; // 初始化fetch拦截器 // 初始化本地数据库 if (isLocalMode) { @@ -13,8 +15,10 @@ if (isLocalMode) { createRoot(document.getElementById("root")!).render( - - - + + + + + ); diff --git a/src/app/routes.tsx b/src/app/routes.tsx index 77a72b9..e982f67 100644 --- a/src/app/routes.tsx +++ b/src/app/routes.tsx @@ -6,6 +6,7 @@ import InstantAnalysis from "@/pages/InstantAnalysis"; import AuditTasks from "@/pages/AuditTasks"; import TaskDetail from "@/pages/TaskDetail"; import AdminDashboard from "@/pages/AdminDashboard"; +import LogsPage from "@/pages/LogsPage"; import type { ReactNode } from 'react'; export interface RouteConfig { @@ -64,6 +65,12 @@ const routes: RouteConfig[] = [ element: , visible: true, }, + { + name: "系统日志", + path: "/logs", + element: , + visible: true, + }, ]; export default routes; \ No newline at end of file diff --git a/src/components/audit/CreateTaskDialog.tsx b/src/components/audit/CreateTaskDialog.tsx index ffa061e..7922853 100644 --- a/src/components/audit/CreateTaskDialog.tsx +++ b/src/components/audit/CreateTaskDialog.tsx @@ -195,6 +195,18 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr console.log('✅ 任务创建成功:', taskId); + // 记录用户操作 + import('@/shared/utils/logger').then(({ logger, LogCategory }) => { + logger.logUserAction('创建审计任务', { + taskId, + projectId: project.id, + projectName: project.name, + taskType: taskForm.task_type, + branch: taskForm.branch_name, + hasZipFile: !!zipFile, + }); + }); + // 关闭创建对话框 onOpenChange(false); resetForm(); @@ -207,7 +219,14 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr toast.success("审计任务已创建并启动"); } catch (error) { console.error('❌ 创建任务失败:', error); - toast.error("创建任务失败: " + (error as Error).message); + + // 记录错误并显示详细信息 + import('@/shared/utils/errorHandler').then(({ handleError }) => { + handleError(error, '创建审计任务失败'); + }); + + const errorMessage = error instanceof Error ? error.message : '未知错误'; + toast.error(`创建任务失败: ${errorMessage}`); } finally { setCreating(false); } diff --git a/src/components/audit/TerminalProgressDialog.tsx b/src/components/audit/TerminalProgressDialog.tsx index ef84c13..99f95b1 100644 --- a/src/components/audit/TerminalProgressDialog.tsx +++ b/src/components/audit/TerminalProgressDialog.tsx @@ -49,16 +49,16 @@ export default function TerminalProgressDialog({ // 取消任务处理 const handleCancel = async () => { if (!taskId) return; - + if (!confirm('确定要取消此任务吗?已分析的结果将被保留。')) { return; } - + // 1. 标记任务为取消状态 taskControl.cancelTask(taskId); setIsCancelled(true); addLog("🛑 用户取消任务,正在停止...", "error"); - + // 2. 立即更新数据库状态 try { const { api } = await import("@/shared/config/database"); @@ -301,13 +301,55 @@ export default function TerminalProgressDialog({ addLog("", "info"); // 空行分隔 addLog("❌ 审计任务执行失败", "error"); addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); - addLog("可能的原因:", "error"); - addLog(" • 网络连接问题", "error"); - addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error"); - addLog(" • GitHub/GitLab API 限流", "error"); - addLog(" • 代码文件格式错误", "error"); + + // 尝试从日志系统获取具体错误信息 + try { + const { logger } = await import("@/shared/utils/logger"); + const recentLogs = logger.getLogs({ + startTime: Date.now() - 60000, // 最近1分钟 + }); + + // 查找与当前任务相关的错误 + const taskErrors = recentLogs + .filter(log => + log.level === 'ERROR' && + (log.message.includes(taskId) || + log.message.includes('审计') || + log.message.includes('API')) + ) + .slice(-3); // 最近3条错误 + + if (taskErrors.length > 0) { + addLog("具体错误信息:", "error"); + taskErrors.forEach(log => { + addLog(` • ${log.message}`, "error"); + if (log.data?.error) { + const errorMsg = typeof log.data.error === 'string' + ? log.data.error + : log.data.error.message || JSON.stringify(log.data.error); + addLog(` ${errorMsg}`, "error"); + } + }); + } else { + // 如果没有找到具体错误,显示常见原因 + addLog("可能的原因:", "error"); + addLog(" • 网络连接问题", "error"); + addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error"); + addLog(" • GitHub/GitLab API 限流", "error"); + addLog(" • LLM API 配置错误或额度不足", "error"); + } + } catch (e) { + // 如果获取日志失败,显示常见原因 + addLog("可能的原因:", "error"); + addLog(" • 网络连接问题", "error"); + addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error"); + addLog(" • GitHub/GitLab API 限流", "error"); + addLog(" • LLM API 配置错误或额度不足", "error"); + } + addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error"); - addLog("💡 建议: 检查网络连接、仓库配置和 Token 设置后重试", "warning"); + addLog("💡 建议: 检查系统配置和网络连接后重试", "warning"); + addLog("📋 查看完整日志: 导航栏 -> 系统日志", "warning"); setIsFailed(true); if (pollIntervalRef.current) { @@ -485,10 +527,10 @@ export default function TerminalProgressDialog({ {isCancelled ? "🛑 任务已取消,已分析的结果已保存" : isCompleted ? "✅ 任务已完成,可以关闭此窗口" : - isFailed ? "❌ 任务失败,请检查配置后重试" : - "⏳ 审计进行中,请勿关闭窗口,过程可能较慢,请耐心等待......"} + isFailed ? "❌ 任务失败,请检查配置后重试" : + "⏳ 审计进行中,请勿关闭窗口,过程可能较慢,请耐心等待......"} - +
{/* 运行中显示取消按钮 */} {!isCompleted && !isFailed && !isCancelled && ( @@ -502,7 +544,19 @@ export default function TerminalProgressDialog({ 取消任务 )} - + + {/* 失败时显示查看日志按钮 */} + {isFailed && ( + + )} + {/* 已完成/失败/取消显示关闭按钮 */} {(isCompleted || isFailed || isCancelled) && ( + + +
+ +

+ 错误已被记录,我们会尽快修复 +

+
+ + ); + } + + return this.props.children; + } +} + +/** + * 高阶组件:为组件添加错误边界 + */ +export function withErrorBoundary

( + Component: React.ComponentType

, + fallback?: ReactNode +) { + return function WithErrorBoundaryComponent(props: P) { + return ( + + + + ); + }; +} diff --git a/src/components/debug/LogViewer.tsx b/src/components/debug/LogViewer.tsx new file mode 100644 index 0000000..4596495 --- /dev/null +++ b/src/components/debug/LogViewer.tsx @@ -0,0 +1,269 @@ +/** + * 日志查看器组件 + */ + +import { useState, useMemo } from 'react'; +import { logger, LogLevel, LogCategory, LogEntry } from '@/shared/utils/logger'; +import { useLogs, useLogStats } from '@/shared/hooks/useLogger'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; +import { Badge } from '@/components/ui/badge'; +import { Trash2, Search, RefreshCw, FileJson, FileSpreadsheet } from 'lucide-react'; +import { toast } from 'sonner'; + +export function LogViewer() { + const [levelFilter, setLevelFilter] = useState('ALL'); + const [categoryFilter, setCategoryFilter] = useState('ALL'); + const [searchQuery, setSearchQuery] = useState(''); + const [selectedLog, setSelectedLog] = useState(null); + + const filter = useMemo(() => ({ + level: levelFilter !== 'ALL' ? levelFilter : undefined, + category: categoryFilter !== 'ALL' ? categoryFilter : undefined, + search: searchQuery || undefined, + }), [levelFilter, categoryFilter, searchQuery]); + + const rawLogs = useLogs(filter); + // 反转日志顺序,最新的在最上面 + const logs = useMemo(() => [...rawLogs].reverse(), [rawLogs]); + const stats = useLogStats(); + + const handleClearLogs = () => { + if (confirm('确定要清空所有日志吗?')) { + logger.clearLogs(); + toast.success('日志已清空'); + } + }; + + const handleDownloadJson = () => { + logger.downloadLogs('json'); + toast.success('日志已导出为JSON'); + }; + + const handleDownloadCsv = () => { + logger.downloadLogs('csv'); + toast.success('日志已导出为CSV'); + }; + + const getLevelColor = (level: LogLevel) => { + const colors = { + [LogLevel.DEBUG]: 'bg-gray-500', + [LogLevel.INFO]: 'bg-blue-500', + [LogLevel.WARN]: 'bg-yellow-500', + [LogLevel.ERROR]: 'bg-red-500', + [LogLevel.FATAL]: 'bg-red-900', + }; + return colors[level]; + }; + + const getCategoryColor = (category: LogCategory) => { + const colors = { + [LogCategory.USER_ACTION]: 'bg-green-500', + [LogCategory.API_CALL]: 'bg-purple-500', + [LogCategory.SYSTEM]: 'bg-blue-500', + [LogCategory.CONSOLE_ERROR]: 'bg-red-500', + }; + return colors[category]; + }; + + return ( +

+ {/* 统计信息 */} +
+
+
总日志数
+
{stats.total}
+
+
+
错误数
+
{stats.errors}
+
+
+
当前显示
+
{logs.length}
+
+
+
最新日志
+
+ {logs.length > 0 ? new Date(logs[logs.length - 1].timestamp).toLocaleTimeString() : '-'} +
+
+
+ + {/* 工具栏 */} +
+
+ + setSearchQuery(e.target.value)} + className="pl-8" + /> +
+ + + + + + + + + + + + +
+ + {/* 日志列表 */} +
+
+
+
+ {logs.length === 0 ? ( +
+ 没有找到日志 +
+ ) : ( + logs.map((log) => ( +
+
+
+ + {log.level} + + + {log.category} + + + {new Date(log.timestamp).toLocaleString()} + +
+ +
+
{log.message}
+
+ )) + )} +
+
+
+ + {/* 日志详情 */} + {selectedLog && ( +
+
+

日志详情

+ +
+
+
+
+
ID
+
{selectedLog.id}
+
+
+
时间
+
{new Date(selectedLog.timestamp).toLocaleString()}
+
+
+
级别
+ + {selectedLog.level} + +
+
+
分类
+ + {selectedLog.category} + +
+
+
消息
+
+ {selectedLog.message} +
+
+ {selectedLog.data && ( +
+
数据
+
+                                            {JSON.stringify(selectedLog.data, null, 2)}
+                                        
+
+ )} + {selectedLog.stack && ( +
+
堆栈跟踪
+
+                                            {selectedLog.stack}
+                                        
+
+ )} + {selectedLog.url && ( +
+
URL
+
{selectedLog.url}
+
+ )} + {selectedLog.userAgent && ( +
+
User Agent
+
{selectedLog.userAgent}
+
+ )} +
+
+
+ )} +
+
+ ); +} diff --git a/src/components/system/SystemConfig.tsx b/src/components/system/SystemConfig.tsx index 9671352..1c467e9 100644 --- a/src/components/system/SystemConfig.tsx +++ b/src/components/system/SystemConfig.tsx @@ -63,7 +63,7 @@ interface SystemConfigData { llmTemperature: number; llmMaxTokens: number; llmCustomHeaders: string; - + // 平台专用配置 geminiApiKey: string; openaiApiKey: string; @@ -76,13 +76,13 @@ interface SystemConfigData { minimaxApiKey: string; doubaoApiKey: string; ollamaBaseUrl: string; - + // GitHub 配置 githubToken: string; - + // GitLab 配置 gitlabToken: string; - + // 分析配置 maxAnalyzeFiles: number; llmConcurrency: number; @@ -134,7 +134,7 @@ export function SystemConfig() { try { // 尝试从 localStorage 加载运行时配置 const savedConfig = localStorage.getItem(STORAGE_KEY); - + if (savedConfig) { const parsedConfig = JSON.parse(savedConfig); setConfig(parsedConfig); @@ -188,8 +188,20 @@ export function SystemConfig() { localStorage.setItem(STORAGE_KEY, JSON.stringify(config)); setHasChanges(false); setConfigSource('runtime'); + + // 记录用户操作 + import('@/shared/utils/logger').then(({ logger, LogCategory }) => { + logger.logUserAction('保存系统配置', { + provider: config.llmProvider, + hasApiKey: !!config.llmApiKey, + maxFiles: config.maxAnalyzeFiles, + concurrency: config.llmConcurrency, + language: config.outputLanguage, + }); + }); + toast.success("配置已保存!刷新页面后生效"); - + // 提示用户刷新页面 setTimeout(() => { if (window.confirm("配置已保存。是否立即刷新页面使配置生效?")) { @@ -198,17 +210,42 @@ export function SystemConfig() { }, 1000); } catch (error) { console.error('Failed to save config:', error); - toast.error("保存配置失败"); + + // 记录错误并显示详细信息 + import('@/shared/utils/errorHandler').then(({ handleError }) => { + handleError(error, '保存系统配置失败'); + }); + + const errorMessage = error instanceof Error ? error.message : '未知错误'; + toast.error(`保存配置失败: ${errorMessage}`); } }; const resetConfig = () => { if (window.confirm("确定要重置为构建时配置吗?这将清除所有运行时配置。")) { - localStorage.removeItem(STORAGE_KEY); - loadFromEnv(); - setHasChanges(false); - setConfigSource('build'); - toast.success("已重置为构建时配置"); + try { + localStorage.removeItem(STORAGE_KEY); + loadFromEnv(); + setHasChanges(false); + setConfigSource('build'); + + // 记录用户操作 + import('@/shared/utils/logger').then(({ logger, LogCategory }) => { + logger.logUserAction('重置系统配置', { action: 'reset_to_build_config' }); + }); + + toast.success("已重置为构建时配置"); + } catch (error) { + console.error('Failed to reset config:', error); + + // 记录错误并显示详细信息 + import('@/shared/utils/errorHandler').then(({ handleError }) => { + handleError(error, '重置系统配置失败'); + }); + + const errorMessage = error instanceof Error ? error.message : '未知错误'; + toast.error(`重置配置失败: ${errorMessage}`); + } } }; @@ -236,7 +273,7 @@ export function SystemConfig() { doubao: config.doubaoApiKey, ollama: 'ollama', }; - + return config.llmApiKey || keyMap[provider] || ''; }; @@ -386,7 +423,7 @@ export function SystemConfig() { placeholder="例如:https://api.example.com/v1" />
-

💡 使用 API 中转站?在这里填入中转站地址

+

💡 使用 API 中转站?在这里填入中转站地址。配置保存后会在实际使用时自动验证。

查看常见 API 中转示例
diff --git a/src/features/projects/services/repoScan.ts b/src/features/projects/services/repoScan.ts index 5dde9fc..9a12858 100644 --- a/src/features/projects/services/repoScan.ts +++ b/src/features/projects/services/repoScan.ts @@ -75,6 +75,17 @@ export async function runRepositoryAudit(params: { console.log(`🚀 ${repoType}任务已创建: ${taskId},准备启动后台扫描...`); + // 记录审计任务开始 + import('@/shared/utils/logger').then(({ logger, LogCategory }) => { + logger.info(LogCategory.SYSTEM, `开始审计任务: ${taskId}`, { + taskId, + projectId: params.projectId, + repoUrl: params.repoUrl, + branch, + repoType, + }); + }); + // 启动后台审计任务,不阻塞返回 (async () => { console.log(`🎬 后台扫描任务开始执行: ${taskId}`); @@ -283,10 +294,29 @@ export async function runRepositoryAudit(params: { completed_at: new Date().toISOString() } as any); + // 记录审计完成 + import('@/shared/utils/logger').then(({ logger, LogCategory }) => { + logger.info(LogCategory.SYSTEM, `审计任务完成: ${taskId}`, { + taskId, + totalFiles: files.length, + scannedFiles: totalFiles, + totalLines, + issuesCount: createdIssues, + qualityScore, + failedCount, + }); + }); + taskControl.cleanupTask(taskId); } catch (e) { console.error('❌ GitHub审计任务执行失败:', e); console.error('错误详情:', e); + + // 记录审计失败 + import('@/shared/utils/errorHandler').then(({ handleError }) => { + handleError(e, `审计任务失败: ${taskId}`); + }); + try { await api.updateAuditTask(taskId, { status: "failed" } as any); } catch (updateError) { diff --git a/src/features/projects/services/repoZipScan.ts b/src/features/projects/services/repoZipScan.ts index baf8dbd..9710ef9 100644 --- a/src/features/projects/services/repoZipScan.ts +++ b/src/features/projects/services/repoZipScan.ts @@ -139,6 +139,16 @@ export async function scanZipFile(params: { console.log(`🚀 ZIP任务已创建: ${taskId},准备启动后台扫描...`); + // 记录审计任务开始 + import('@/shared/utils/logger').then(({ logger, LogCategory }) => { + logger.info(LogCategory.SYSTEM, `开始ZIP文件审计: ${taskId}`, { + taskId, + projectId, + fileName: zipFile.name, + fileSize: zipFile.size, + }); + }); + // 启动后台扫描任务,不阻塞返回 (async () => { console.log(`🎬 后台扫描任务开始执行: ${taskId}`); @@ -344,9 +354,30 @@ export async function scanZipFile(params: { completed_at: new Date().toISOString() } as any); + // 记录审计完成 + import('@/shared/utils/logger').then(({ logger, LogCategory }) => { + logger.info(LogCategory.SYSTEM, `ZIP审计任务完成: ${taskId}`, { + taskId, + status: taskStatus, + totalFiles, + scannedFiles, + failedFiles, + totalLines, + issuesCount: totalIssues, + qualityScore: avgQualityScore, + successRate: successRate.toFixed(1) + '%', + }); + }); + resolve(); } catch (processingError) { await api.updateAuditTask(taskId, { status: "failed" } as any); + + // 记录处理错误 + import('@/shared/utils/errorHandler').then(({ handleError }) => { + handleError(processingError, `ZIP审计任务处理失败: ${taskId}`); + }); + reject(processingError); } }); diff --git a/src/pages/InstantAnalysis.tsx b/src/pages/InstantAnalysis.tsx index 9b4618e..0810cc4 100644 --- a/src/pages/InstantAnalysis.tsx +++ b/src/pages/InstantAnalysis.tsx @@ -281,7 +281,7 @@ public class Example { // 构造临时任务和问题数据用于导出 const getTempTaskAndIssues = () => { if (!result) return null; - + const tempTask: AuditTask = { id: 'instant-' + Date.now(), project_id: 'instant-analysis', @@ -313,7 +313,7 @@ public class Example { updated_at: new Date().toISOString() } }; - + const tempIssues: AuditIssue[] = result.issues.map((issue, index) => ({ id: `instant-issue-${index}`, task_id: tempTask.id, @@ -332,7 +332,7 @@ public class Example { resolved_at: undefined, created_at: new Date().toISOString() })); - + return { task: tempTask, issues: tempIssues }; }; @@ -618,10 +618,10 @@ public class Example { {language.charAt(0).toUpperCase() + language.slice(1)} - + {/* 导出按钮 */} -