feat: v1.3.0 - 添加完整的日志系统和错误处理

- 新增日志记录系统,自动记录用户核心操作和错误
- 新增日志查看器,支持筛选、搜索和导出
- 增强错误处理,显示具体错误信息
- 修复所有LLM adapter的URL双斜杠问题
- 优化审计失败提示,引导用户查看日志
- 更新版本号到v1.3.0
This commit is contained in:
lintsinghua 2025-10-29 19:25:38 +08:00
parent 094677028a
commit 950325850c
31 changed files with 1948 additions and 56 deletions

View File

@ -347,6 +347,38 @@ VITE_QWEN_API_KEY=key3
```
</details>
<details>
<summary><b>如何查看系统日志和调试信息?</b></summary>
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)
</details>
### 🔑 获取 API Key
#### 支持的 LLM 平台

View File

@ -13,7 +13,7 @@
<div align="center">
[![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
```
</details>
<details>
<summary><b>How to view system logs and debug information?</b></summary>
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)
</details>
### 🔑 Getting API Keys

View File

@ -1,6 +1,6 @@
{
"name": "xcode-reviewer",
"version": "1.2.0",
"version": "1.3.0",
"type": "module",
"scripts": {
"dev": "vite",

View File

@ -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(
<StrictMode>
<AppWrapper>
<App />
</AppWrapper>
<ErrorBoundary>
<AppWrapper>
<App />
</AppWrapper>
</ErrorBoundary>
</StrictMode>
);

View File

@ -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: <RecycleBin />,
visible: true,
},
{
name: "系统日志",
path: "/logs",
element: <LogsPage />,
visible: true,
},
];
export default routes;

View File

@ -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);
}

View File

@ -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({
<span className="text-gray-300">
{isCancelled ? "🛑 任务已取消,已分析的结果已保存" :
isCompleted ? "✅ 任务已完成,可以关闭此窗口" :
isFailed ? "❌ 任务失败,请检查配置后重试" :
"⏳ 审计进行中,请勿关闭窗口,过程可能较慢,请耐心等待......"}
isFailed ? "❌ 任务失败,请检查配置后重试" :
"⏳ 审计进行中,请勿关闭窗口,过程可能较慢,请耐心等待......"}
</span>
<div className="flex items-center space-x-2">
{/* 运行中显示取消按钮 */}
{!isCompleted && !isFailed && !isCancelled && (
@ -502,7 +544,19 @@ export default function TerminalProgressDialog({
</Button>
)}
{/* 失败时显示查看日志按钮 */}
{isFailed && (
<button
onClick={() => {
window.open('/logs', '_blank');
}}
className="px-4 py-1.5 bg-gradient-to-r from-yellow-600 to-orange-600 hover:from-yellow-500 hover:to-orange-500 text-white rounded text-xs transition-all shadow-lg shadow-yellow-900/50 font-medium"
>
📋
</button>
)}
{/* 已完成/失败/取消显示关闭按钮 */}
{(isCompleted || isFailed || isCancelled) && (
<button

View File

@ -0,0 +1,169 @@
/**
* React错误边界组件
* JavaScript错误并记录
*/
import React, { Component, ReactNode } from 'react';
import { logger, LogCategory } from '@/shared/utils/logger';
import { Button } from '@/components/ui/button';
import { AlertTriangle, RefreshCw, Home } from 'lucide-react';
interface Props {
children: ReactNode;
fallback?: ReactNode;
onError?: (error: Error, errorInfo: React.ErrorInfo) => void;
}
interface State {
hasError: boolean;
error: Error | null;
errorInfo: React.ErrorInfo | null;
}
export class ErrorBoundary extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasError: false,
error: null,
errorInfo: null,
};
}
static getDerivedStateFromError(error: Error): Partial<State> {
return {
hasError: true,
error,
};
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// 记录错误到日志系统
logger.error(
LogCategory.CONSOLE_ERROR,
`React组件错误: ${error.message}`,
{
error: error.toString(),
componentStack: errorInfo.componentStack,
},
error.stack
);
this.setState({
errorInfo,
});
// 调用自定义错误处理
this.props.onError?.(error, errorInfo);
}
handleReset = () => {
this.setState({
hasError: false,
error: null,
errorInfo: null,
});
};
handleReload = () => {
window.location.reload();
};
handleGoHome = () => {
window.location.href = '/';
};
render() {
if (this.state.hasError) {
// 如果提供了自定义fallback使用它
if (this.props.fallback) {
return this.props.fallback;
}
// 默认错误UI
return (
<div className="flex min-h-screen items-center justify-center bg-background p-4">
<div className="w-full max-w-md space-y-6 rounded-lg border bg-card p-6 shadow-lg">
<div className="flex items-center gap-3">
<div className="rounded-full bg-destructive/10 p-3">
<AlertTriangle className="h-6 w-6 text-destructive" />
</div>
<div>
<h2 className="text-xl font-semibold"></h2>
<p className="text-sm text-muted-foreground"></p>
</div>
</div>
{this.state.error && (
<div className="space-y-2">
<div className="rounded-md bg-destructive/10 p-3">
<p className="text-sm font-medium text-destructive">
{this.state.error.message}
</p>
</div>
{import.meta.env.DEV && this.state.error.stack && (
<details className="text-xs">
<summary className="cursor-pointer font-medium text-muted-foreground">
</summary>
<pre className="mt-2 overflow-auto rounded bg-muted p-2 text-xs">
{this.state.error.stack}
</pre>
</details>
)}
{import.meta.env.DEV && this.state.errorInfo?.componentStack && (
<details className="text-xs">
<summary className="cursor-pointer font-medium text-muted-foreground">
</summary>
<pre className="mt-2 overflow-auto rounded bg-muted p-2 text-xs">
{this.state.errorInfo.componentStack}
</pre>
</details>
)}
</div>
)}
<div className="flex gap-2">
<Button onClick={this.handleReset} variant="outline" className="flex-1">
<RefreshCw className="mr-2 h-4 w-4" />
</Button>
<Button onClick={this.handleGoHome} variant="outline" className="flex-1">
<Home className="mr-2 h-4 w-4" />
</Button>
<Button onClick={this.handleReload} className="flex-1">
</Button>
</div>
<p className="text-center text-xs text-muted-foreground">
</p>
</div>
</div>
);
}
return this.props.children;
}
}
/**
*
*/
export function withErrorBoundary<P extends object>(
Component: React.ComponentType<P>,
fallback?: ReactNode
) {
return function WithErrorBoundaryComponent(props: P) {
return (
<ErrorBoundary fallback={fallback}>
<Component {...props} />
</ErrorBoundary>
);
};
}

View File

@ -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<LogLevel | 'ALL'>('ALL');
const [categoryFilter, setCategoryFilter] = useState<LogCategory | 'ALL'>('ALL');
const [searchQuery, setSearchQuery] = useState('');
const [selectedLog, setSelectedLog] = useState<LogEntry | null>(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 (
<div className="flex h-full flex-col gap-4 p-4">
{/* 统计信息 */}
<div className="grid grid-cols-2 gap-4 md:grid-cols-4">
<div className="rounded-lg border bg-card p-4">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold">{stats.total}</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold text-red-500">{stats.errors}</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="text-sm text-muted-foreground"></div>
<div className="text-2xl font-bold">{logs.length}</div>
</div>
<div className="rounded-lg border bg-card p-4">
<div className="text-sm text-muted-foreground"></div>
<div className="text-sm">
{logs.length > 0 ? new Date(logs[logs.length - 1].timestamp).toLocaleTimeString() : '-'}
</div>
</div>
</div>
{/* 工具栏 */}
<div className="flex flex-wrap items-center gap-2">
<div className="relative flex-1 min-w-[200px]">
<Search className="absolute left-2 top-2.5 h-4 w-4 text-muted-foreground" />
<Input
placeholder="搜索日志..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="pl-8"
/>
</div>
<Select value={levelFilter} onValueChange={(v) => setLevelFilter(v as any)}>
<SelectTrigger className="w-32">
<SelectValue placeholder="所有级别" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL"></SelectItem>
{Object.values(LogLevel).map(level => (
<SelectItem key={level} value={level}>{level}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={categoryFilter} onValueChange={(v) => setCategoryFilter(v as any)}>
<SelectTrigger className="w-40">
<SelectValue placeholder="所有分类" />
</SelectTrigger>
<SelectContent>
<SelectItem value="ALL"></SelectItem>
{Object.values(LogCategory).map(category => (
<SelectItem key={category} value={category}>{category}</SelectItem>
))}
</SelectContent>
</Select>
<Button variant="outline" size="icon" onClick={() => window.location.reload()} title="刷新页面">
<RefreshCw className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleDownloadJson} title="导出为JSON格式">
<FileJson className="h-4 w-4" />
</Button>
<Button variant="outline" size="icon" onClick={handleDownloadCsv} title="导出为CSV格式">
<FileSpreadsheet className="h-4 w-4" />
</Button>
<Button variant="destructive" size="icon" onClick={handleClearLogs} title="清空日志">
<Trash2 className="h-4 w-4" />
</Button>
</div>
{/* 日志列表 */}
<div className="flex flex-1 flex-col gap-4 overflow-hidden lg:flex-row">
<div className="flex-1 flex flex-col rounded-lg border bg-card overflow-hidden">
<div className="flex-1 overflow-auto">
<div className="p-2">
{logs.length === 0 ? (
<div className="flex h-40 items-center justify-center text-muted-foreground">
</div>
) : (
logs.map((log) => (
<div
key={log.id}
className={`mb-2 rounded-lg border p-3 transition-colors ${selectedLog?.id === log.id ? 'bg-accent' : ''
}`}
>
<div className="flex flex-wrap items-start justify-between gap-2">
<div className="flex flex-wrap items-start gap-2">
<Badge className={`${getLevelColor(log.level)} text-white`}>
{log.level}
</Badge>
<Badge className={`${getCategoryColor(log.category)} text-white`}>
{log.category}
</Badge>
<span className="text-xs text-muted-foreground">
{new Date(log.timestamp).toLocaleString()}
</span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => setSelectedLog(log)}
className="h-7 text-xs"
>
</Button>
</div>
<div className="mt-2 text-sm line-clamp-2">{log.message}</div>
</div>
))
)}
</div>
</div>
</div>
{/* 日志详情 */}
{selectedLog && (
<div className="flex w-full flex-col rounded-lg border bg-card lg:w-[600px] overflow-hidden">
<div className="flex items-center justify-between border-b p-4 flex-shrink-0">
<h3 className="text-lg font-semibold"></h3>
<Button
variant="ghost"
size="sm"
onClick={() => setSelectedLog(null)}
className="h-8 w-8 p-0"
>
</Button>
</div>
<div className="flex-1 overflow-auto">
<div className="p-4 space-y-4 text-sm">
<div>
<div className="mb-1 font-medium text-muted-foreground">ID</div>
<div className="rounded bg-muted p-2 font-mono text-xs break-all">{selectedLog.id}</div>
</div>
<div>
<div className="mb-1 font-medium text-muted-foreground"></div>
<div className="rounded bg-muted p-2">{new Date(selectedLog.timestamp).toLocaleString()}</div>
</div>
<div>
<div className="mb-1 font-medium text-muted-foreground"></div>
<Badge className={`${getLevelColor(selectedLog.level)} text-white`}>
{selectedLog.level}
</Badge>
</div>
<div>
<div className="mb-1 font-medium text-muted-foreground"></div>
<Badge className={`${getCategoryColor(selectedLog.category)} text-white`}>
{selectedLog.category}
</Badge>
</div>
<div>
<div className="mb-1 font-medium text-muted-foreground"></div>
<div className="rounded bg-muted p-3 whitespace-pre-wrap break-words overflow-auto max-h-96">
{selectedLog.message}
</div>
</div>
{selectedLog.data && (
<div>
<div className="mb-1 font-medium text-muted-foreground"></div>
<pre className="overflow-auto rounded bg-muted p-3 text-xs max-h-96 whitespace-pre-wrap break-words">
{JSON.stringify(selectedLog.data, null, 2)}
</pre>
</div>
)}
{selectedLog.stack && (
<div>
<div className="mb-1 font-medium text-muted-foreground"></div>
<pre className="overflow-auto rounded bg-muted p-3 text-xs max-h-96 whitespace-pre-wrap break-words">
{selectedLog.stack}
</pre>
</div>
)}
{selectedLog.url && (
<div>
<div className="mb-1 font-medium text-muted-foreground">URL</div>
<div className="rounded bg-muted p-3 break-all text-xs">{selectedLog.url}</div>
</div>
)}
{selectedLog.userAgent && (
<div>
<div className="mb-1 font-medium text-muted-foreground">User Agent</div>
<div className="rounded bg-muted p-3 break-all text-xs">{selectedLog.userAgent}</div>
</div>
)}
</div>
</div>
</div>
)}
</div>
</div>
);
}

View File

@ -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"
/>
<div className="text-xs text-muted-foreground space-y-1">
<p>💡 <strong>使 API </strong></p>
<p>💡 <strong>使 API </strong>使</p>
<details className="cursor-pointer">
<summary className="text-primary hover:underline"> API </summary>
<div className="mt-2 p-3 bg-muted rounded space-y-1 text-xs">

View File

@ -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) {

View File

@ -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);
}
});

View File

@ -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 {
<Badge variant="outline" className="text-xs">
{language.charAt(0).toUpperCase() + language.slice(1)}
</Badge>
{/* 导出按钮 */}
<Button
size="sm"
<Button
size="sm"
onClick={() => setExportDialogOpen(true)}
className="btn-primary"
>
@ -782,9 +782,9 @@ public class Example {
{/* 分析进行中状态 */}
{analyzing && (
<Card ref={loadingCardRef} className="card-modern">
<Card className="card-modern">
<CardContent className="py-16">
<div className="text-center">
<div ref={loadingCardRef} className="text-center">
<div className="w-20 h-20 bg-red-50 rounded-full flex items-center justify-center mx-auto mb-6">
<div className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
</div>
@ -801,7 +801,7 @@ public class Example {
</CardContent>
</Card>
)}
{/* 导出报告对话框 */}
{result && (() => {
const data = getTempTaskAndIssues();

13
src/pages/LogsPage.tsx Normal file
View File

@ -0,0 +1,13 @@
/**
*
*/
import { LogViewer } from '@/components/debug/LogViewer';
export default function LogsPage() {
return (
<div className="h-screen">
<LogViewer />
</div>
);
}

View File

@ -100,13 +100,29 @@ export default function Projects() {
// 无登录场景下不传 owner_id由后端置为 null
} as any);
// 记录用户操作
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
logger.logUserAction('创建项目', {
projectName: createForm.name,
repositoryType: createForm.repository_type,
languages: createForm.programming_languages,
});
});
toast.success("项目创建成功");
setShowCreateDialog(false);
resetCreateForm();
loadProjects();
} catch (error) {
console.error('Failed to create project:', error);
toast.error("创建项目失败");
// 记录错误并显示详细信息
import('@/shared/utils/errorHandler').then(({ handleError }) => {
handleError(error, '创建项目失败');
});
const errorMessage = error instanceof Error ? error.message : '未知错误';
toast.error(`创建项目失败: ${errorMessage}`);
}
};
@ -169,6 +185,15 @@ export default function Projects() {
clearInterval(progressInterval);
setUploadProgress(100);
// 记录用户操作
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
logger.logUserAction('上传ZIP文件创建项目', {
projectName: project.name,
fileName: file.name,
fileSize: file.size,
});
});
// 关闭创建对话框
setShowCreateDialog(false);
resetCreateForm();
@ -181,7 +206,14 @@ export default function Projects() {
} catch (error: any) {
console.error('Upload failed:', error);
toast.error(error.message || "上传失败");
// 记录错误并显示详细信息
import('@/shared/utils/errorHandler').then(({ handleError }) => {
handleError(error, '上传ZIP文件失败');
});
const errorMessage = error?.message || '未知错误';
toast.error(`上传失败: ${errorMessage}`);
} finally {
setUploading(false);
setUploadProgress(0);
@ -265,6 +297,15 @@ export default function Projects() {
try {
await api.deleteProject(projectToDelete.id);
// 记录用户操作
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
logger.logUserAction('删除项目', {
projectId: projectToDelete.id,
projectName: projectToDelete.name,
});
});
toast.success(`项目 "${projectToDelete.name}" 已移到回收站`, {
description: '您可以在回收站中恢复此项目',
duration: 4000
@ -274,7 +315,14 @@ export default function Projects() {
loadProjects();
} catch (error) {
console.error('Failed to delete project:', error);
toast.error("删除项目失败");
// 记录错误并显示详细信息
import('@/shared/utils/errorHandler').then(({ handleError }) => {
handleError(error, '删除项目失败');
});
const errorMessage = error instanceof Error ? error.message : '未知错误';
toast.error(`删除项目失败: ${errorMessage}`);
}
};

View File

@ -0,0 +1,92 @@
/**
* Hook
*/
import { useEffect, useCallback, useState } from 'react';
import { logger, LogEntry, LogLevel, LogCategory } from '../utils/logger';
export function useLogger() {
const logUserAction = useCallback((action: string, details?: any) => {
logger.logUserAction(action, details);
}, []);
const logApiCall = useCallback((
method: string,
url: string,
status?: number,
duration?: number,
error?: any
) => {
logger.logApiCall(method, url, status, duration, error);
}, []);
const logPerformance = useCallback((metric: string, value: number, unit?: string) => {
logger.logPerformance(metric, value, unit);
}, []);
return {
debug: logger.debug.bind(logger),
info: logger.info.bind(logger),
warn: logger.warn.bind(logger),
error: logger.error.bind(logger),
fatal: logger.fatal.bind(logger),
logUserAction,
logApiCall,
logPerformance,
};
}
export function useLogListener(callback: (log: LogEntry) => void) {
useEffect(() => {
const unsubscribe = logger.addListener(callback);
return () => {
unsubscribe();
};
}, [callback]);
}
export function useLogs(filter?: {
level?: LogLevel;
category?: LogCategory;
startTime?: number;
endTime?: number;
search?: string;
}) {
const [logs, setLogs] = useState<LogEntry[]>(() => logger.getLogs(filter));
// 将filter转换为字符串作为依赖避免对象引用问题
const filterKey = JSON.stringify(filter);
useEffect(() => {
// 立即更新一次
setLogs(logger.getLogs(filter));
const updateLogs = () => {
setLogs(logger.getLogs(filter));
};
const unsubscribe = logger.addListener(updateLogs);
return () => {
unsubscribe();
};
}, [filterKey]); // 使用filterKey而不是filter对象
return logs;
}
export function useLogStats() {
const [stats, setStats] = useState(() => logger.getStats());
useEffect(() => {
const updateStats = () => {
setStats(logger.getStats());
};
const unsubscribe = logger.addListener(updateStats);
return () => {
unsubscribe();
};
}, []);
return stats;
}

View File

@ -58,7 +58,7 @@ export class ClaudeAdapter extends BaseLLMAdapter {
Object.assign(headers, this.config.customHeaders);
}
const response = await fetch(`${this.baseUrl}/messages`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/messages`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify(requestBody),

View File

@ -37,7 +37,7 @@ export class DeepSeekAdapter extends BaseLLMAdapter {
Object.assign(headers, this.config.customHeaders);
}
const response = await fetch(`${this.baseUrl}/v1/chat/completions`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/v1/chat/completions`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify({

View File

@ -32,7 +32,7 @@ export class DoubaoAdapter extends BaseLLMAdapter {
};
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
const response = await fetch(`${this.baseUrl}/chat/completions`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/chat/completions`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify({

View File

@ -32,7 +32,7 @@ export class MinimaxAdapter extends BaseLLMAdapter {
};
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
const response = await fetch(`${this.baseUrl}/text/chatcompletion_v2`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/text/chatcompletion_v2`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify({

View File

@ -32,7 +32,7 @@ export class MoonshotAdapter extends BaseLLMAdapter {
};
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
const response = await fetch(`${this.baseUrl}/chat/completions`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/chat/completions`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify({
@ -73,11 +73,11 @@ export class MoonshotAdapter extends BaseLLMAdapter {
async validateConfig(): Promise<boolean> {
await super.validateConfig();
if (!this.config.model.startsWith('moonshot-')) {
throw new Error(`无效的Moonshot模型: ${this.config.model}`);
}
return true;
}
}

View File

@ -42,7 +42,7 @@ export class OllamaAdapter extends BaseLLMAdapter {
Object.assign(headers, this.config.customHeaders);
}
const response = await fetch(`${this.baseUrl}/chat/completions`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/chat/completions`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify({

View File

@ -58,7 +58,7 @@ export class OpenAIAdapter extends BaseLLMAdapter {
requestBody.max_tokens = request.maxTokens ?? this.config.maxTokens;
}
const response = await fetch(`${this.baseUrl}/chat/completions`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/chat/completions`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify(requestBody),

View File

@ -32,7 +32,7 @@ export class QwenAdapter extends BaseLLMAdapter {
};
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
const response = await fetch(`${this.baseUrl}/services/aigc/text-generation/generation`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/services/aigc/text-generation/generation`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify({

View File

@ -32,7 +32,7 @@ export class ZhipuAdapter extends BaseLLMAdapter {
};
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
const response = await fetch(`${this.baseUrl}/chat/completions`, {
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/chat/completions`, {
method: 'POST',
headers: this.buildHeaders(headers),
body: JSON.stringify({

View File

@ -0,0 +1,189 @@
/**
* API拦截器
* API调用和响应
*/
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse, AxiosError } from 'axios';
import { logger, LogCategory } from './logger';
import { errorHandler } from './errorHandler';
interface RequestMetadata {
startTime: number;
url: string;
method: string;
}
const requestMetadataMap = new WeakMap<any, RequestMetadata>();
/**
* Axios拦截器
*/
export function setupAxiosInterceptors(instance: AxiosInstance) {
// 请求拦截器
instance.interceptors.request.use(
(config) => {
const metadata: RequestMetadata = {
startTime: Date.now(),
url: config.url || '',
method: (config.method || 'GET').toUpperCase(),
};
requestMetadataMap.set(config, metadata);
logger.debug(
LogCategory.API_CALL,
`${metadata.method} ${metadata.url}`,
{
method: metadata.method,
url: metadata.url,
params: config.params,
data: config.data,
}
);
return config;
},
(error) => {
logger.error(LogCategory.API_CALL, 'Request interceptor error', { error });
return Promise.reject(error);
}
);
// 响应拦截器
instance.interceptors.response.use(
(response) => {
const metadata = requestMetadataMap.get(response.config);
if (metadata) {
const duration = Date.now() - metadata.startTime;
logger.info(
LogCategory.API_CALL,
`${metadata.method} ${metadata.url} (${response.status}) - ${duration}ms`,
{
method: metadata.method,
url: metadata.url,
status: response.status,
duration,
data: response.data,
}
);
// 记录性能
if (duration > 1000) {
logger.warn(
LogCategory.PERFORMANCE,
`Slow API call: ${metadata.method} ${metadata.url} took ${duration}ms`,
{ method: metadata.method, url: metadata.url, duration }
);
}
}
return response;
},
(error: AxiosError) => {
const metadata = requestMetadataMap.get(error.config);
if (metadata) {
const duration = Date.now() - metadata.startTime;
logger.error(
LogCategory.API_CALL,
`${metadata.method} ${metadata.url} - ${duration}ms`,
{
method: metadata.method,
url: metadata.url,
status: error.response?.status,
duration,
error: error.response?.data || error.message,
},
error.stack
);
}
return Promise.reject(error);
}
);
return instance;
}
/**
* Axios实例
*/
export function createApiClient(config?: AxiosRequestConfig): AxiosInstance {
const instance = axios.create(config);
return setupAxiosInterceptors(instance);
}
/**
* Fetch API包装器
*/
export async function fetchWithLogging(
url: string,
options?: RequestInit
): Promise<Response> {
const method = options?.method || 'GET';
const startTime = Date.now();
logger.debug(LogCategory.API_CALL, `${method} ${url}`, {
method,
url,
options,
});
try {
const response = await fetch(url, options);
const duration = Date.now() - startTime;
logger.info(
LogCategory.API_CALL,
`${method} ${url} (${response.status}) - ${duration}ms`,
{
method,
url,
status: response.status,
duration,
}
);
if (duration > 1000) {
logger.warn(
LogCategory.PERFORMANCE,
`Slow fetch call: ${method} ${url} took ${duration}ms`,
{ method, url, duration }
);
}
return response;
} catch (error) {
const duration = Date.now() - startTime;
logger.error(
LogCategory.API_CALL,
`${method} ${url} - ${duration}ms`,
{
method,
url,
duration,
error,
}
);
throw error;
}
}
/**
*
*/
export function withErrorLogging<T extends (...args: any[]) => Promise<any>>(
fn: T,
context?: string
): T {
return (async (...args: any[]) => {
try {
return await fn(...args);
} catch (error) {
errorHandler.handle(error, context);
throw error;
}
}) as T;
}

View File

@ -0,0 +1,308 @@
/**
*
*
*/
import { toast } from 'sonner';
import { logger, LogCategory } from './logger';
export enum ErrorType {
NETWORK = 'NETWORK',
API = 'API',
VALIDATION = 'VALIDATION',
AUTHENTICATION = 'AUTHENTICATION',
AUTHORIZATION = 'AUTHORIZATION',
NOT_FOUND = 'NOT_FOUND',
TIMEOUT = 'TIMEOUT',
UNKNOWN = 'UNKNOWN',
}
export interface AppError {
type: ErrorType;
message: string;
originalError?: any;
code?: string | number;
details?: any;
timestamp: number;
}
class ErrorHandler {
/**
*
*/
handle(error: any, context?: string): AppError {
const appError = this.parseError(error);
// 记录错误日志
logger.error(
LogCategory.SYSTEM,
`${context ? `[${context}] ` : ''}${appError.message}`,
{
type: appError.type,
code: appError.code,
details: appError.details,
originalError: error,
},
error?.stack
);
// 显示用户提示
this.showErrorToast(appError, context);
return appError;
}
/**
*
*/
private parseError(error: any): AppError {
const timestamp = Date.now();
// Axios错误
if (error?.isAxiosError || error?.response) {
return this.parseAxiosError(error, timestamp);
}
// Fetch错误
if (error instanceof TypeError && error.message.includes('fetch')) {
return {
type: ErrorType.NETWORK,
message: '网络连接失败,请检查网络设置',
originalError: error,
timestamp,
};
}
// 自定义AppError
if (error?.type && Object.values(ErrorType).includes(error.type)) {
return { ...error, timestamp };
}
// 标准Error对象
if (error instanceof Error) {
return {
type: ErrorType.UNKNOWN,
message: error.message || '发生未知错误',
originalError: error,
timestamp,
};
}
// 其他类型
return {
type: ErrorType.UNKNOWN,
message: typeof error === 'string' ? error : '发生未知错误',
originalError: error,
timestamp,
};
}
/**
* Axios错误
*/
private parseAxiosError(error: any, timestamp: number): AppError {
const response = error.response;
const status = response?.status;
// 网络错误
if (!response) {
return {
type: ErrorType.NETWORK,
message: '网络请求失败,请检查网络连接',
originalError: error,
timestamp,
};
}
// 根据状态码分类
switch (status) {
case 400:
return {
type: ErrorType.VALIDATION,
message: response.data?.message || '请求参数错误',
code: status,
details: response.data,
originalError: error,
timestamp,
};
case 401:
return {
type: ErrorType.AUTHENTICATION,
message: '未登录或登录已过期,请重新登录',
code: status,
originalError: error,
timestamp,
};
case 403:
return {
type: ErrorType.AUTHORIZATION,
message: '没有权限执行此操作',
code: status,
originalError: error,
timestamp,
};
case 404:
return {
type: ErrorType.NOT_FOUND,
message: '请求的资源不存在',
code: status,
originalError: error,
timestamp,
};
case 408:
case 504:
return {
type: ErrorType.TIMEOUT,
message: '请求超时,请稍后重试',
code: status,
originalError: error,
timestamp,
};
case 500:
case 502:
case 503:
return {
type: ErrorType.API,
message: '服务器错误,请稍后重试',
code: status,
details: response.data,
originalError: error,
timestamp,
};
default:
return {
type: ErrorType.API,
message: response.data?.message || `请求失败 (${status})`,
code: status,
details: response.data,
originalError: error,
timestamp,
};
}
}
/**
*
*/
private showErrorToast(error: AppError, context?: string) {
const title = context || this.getErrorTitle(error.type);
toast.error(title, {
description: error.message,
duration: 5000,
action: error.code ? {
label: '查看详情',
onClick: () => this.showErrorDetails(error),
} : undefined,
});
}
/**
*
*/
private getErrorTitle(type: ErrorType): string {
const titles = {
[ErrorType.NETWORK]: '网络错误',
[ErrorType.API]: 'API错误',
[ErrorType.VALIDATION]: '验证错误',
[ErrorType.AUTHENTICATION]: '认证错误',
[ErrorType.AUTHORIZATION]: '权限错误',
[ErrorType.NOT_FOUND]: '资源不存在',
[ErrorType.TIMEOUT]: '请求超时',
[ErrorType.UNKNOWN]: '未知错误',
};
return titles[type];
}
/**
*
*/
private showErrorDetails(error: AppError) {
console.group('错误详情');
console.log('类型:', error.type);
console.log('消息:', error.message);
console.log('代码:', error.code);
console.log('时间:', new Date(error.timestamp).toLocaleString());
if (error.details) {
console.log('详情:', error.details);
}
if (error.originalError) {
console.log('原始错误:', error.originalError);
}
console.groupEnd();
}
/**
*
*/
createError(type: ErrorType, message: string, details?: any): AppError {
return {
type,
message,
details,
timestamp: Date.now(),
};
}
/**
*
*/
async wrap<T>(
fn: () => Promise<T>,
context?: string,
options?: {
silent?: boolean;
fallback?: T;
}
): Promise<T | undefined> {
try {
return await fn();
} catch (error) {
if (!options?.silent) {
this.handle(error, context);
} else {
logger.error(LogCategory.SYSTEM, `${context || 'Error'}: ${error}`, { error });
}
return options?.fallback;
}
}
/**
*
*/
wrapSync<T>(
fn: () => T,
context?: string,
options?: {
silent?: boolean;
fallback?: T;
}
): T | undefined {
try {
return fn();
} catch (error) {
if (!options?.silent) {
this.handle(error, context);
} else {
logger.error(LogCategory.SYSTEM, `${context || 'Error'}: ${error}`, { error });
}
return options?.fallback;
}
}
}
// 导出单例
export const errorHandler = new ErrorHandler();
// 便捷函数
export const handleError = (error: any, context?: string) => errorHandler.handle(error, context);
export const wrapAsync = <T>(fn: () => Promise<T>, context?: string, options?: any) =>
errorHandler.wrap(fn, context, options);
export const wrapSync = <T>(fn: () => T, context?: string, options?: any) =>
errorHandler.wrapSync(fn, context, options);

View File

@ -0,0 +1,73 @@
/**
* Fetch包装器 - API调用
*/
import { logger, LogCategory } from './logger';
const originalFetch = window.fetch;
/**
* URL
*/
function shouldLogUrl(url: string): boolean {
// 过滤掉静态资源和某些不需要记录的请求
const skipPatterns = [
/\.(png|jpg|jpeg|gif|svg|ico|woff|woff2|ttf|eot)$/i,
/\/assets\//,
/chrome-extension:/,
/localhost:.*\/node_modules/,
];
return !skipPatterns.some(pattern => pattern.test(url));
}
/**
* fetch -
*/
window.fetch = async function (...args: Parameters<typeof fetch>): Promise<Response> {
const [url, options] = args;
const method = options?.method || 'GET';
const urlString = typeof url === 'string' ? url : url.toString();
// 跳过不需要记录的URL
if (!shouldLogUrl(urlString)) {
return originalFetch(...args);
}
const startTime = Date.now();
try {
const response = await originalFetch(...args);
const duration = Date.now() - startTime;
// 只记录失败的请求
if (!response.ok) {
logger.error(
LogCategory.API_CALL,
`API请求失败: ${method} ${urlString} (${response.status})`,
{ method, url: urlString, status: response.status, statusText: response.statusText, duration }
);
}
return response;
} catch (error) {
const duration = Date.now() - startTime;
// 记录网络错误
logger.error(
LogCategory.API_CALL,
`API请求异常: ${method} ${urlString}`,
{
method,
url: urlString,
duration,
error: error instanceof Error ? error.message : String(error),
},
error instanceof Error ? error.stack : undefined
);
throw error;
}
};
export { originalFetch };

View File

@ -0,0 +1,7 @@
/**
*
*/
export * from './logger';
export * from './errorHandler';
export * from './performanceMonitor';

382
src/shared/utils/logger.ts Normal file
View File

@ -0,0 +1,382 @@
/**
*
*
*/
export enum LogLevel {
DEBUG = 'DEBUG',
INFO = 'INFO',
WARN = 'WARN',
ERROR = 'ERROR',
FATAL = 'FATAL',
}
export enum LogCategory {
USER_ACTION = 'USER_ACTION', // 用户操作
API_CALL = 'API_CALL', // API调用
SYSTEM = 'SYSTEM', // 系统事件
CONSOLE_ERROR = 'CONSOLE_ERROR', // 控制台错误
}
export interface LogEntry {
id: string;
timestamp: number;
level: LogLevel;
category: LogCategory;
message: string;
data?: any;
stack?: string;
userAgent?: string;
url?: string;
userId?: string;
}
class Logger {
private logs: LogEntry[] = [];
private maxLogs = 1000; // 最多保存1000条日志
private storageKey = 'app_logs';
private listeners: Set<(log: LogEntry) => void> = new Set();
private isEnabled = true;
constructor() {
this.loadLogsFromStorage();
this.setupConsoleInterceptor();
this.setupErrorHandler();
this.setupUnhandledRejectionHandler();
}
/**
* localStorage加载历史日志
*/
private loadLogsFromStorage() {
try {
const stored = localStorage.getItem(this.storageKey);
if (stored) {
this.logs = JSON.parse(stored);
}
} catch (error) {
console.error('Failed to load logs from storage:', error);
}
}
/**
* localStorage
*/
private saveLogsToStorage() {
try {
// 只保存最近的日志
const logsToSave = this.logs.slice(-this.maxLogs);
localStorage.setItem(this.storageKey, JSON.stringify(logsToSave));
} catch (error) {
console.error('Failed to save logs to storage:', error);
}
}
/**
* console方法
*/
private setupConsoleInterceptor() {
const originalError = console.error;
// 只拦截错误,不拦截警告
console.error = (...args: any[]) => {
originalError.apply(console, args);
// 过滤掉一些常见的无关错误
const message = args.join(' ');
if (!message.includes('ResizeObserver') &&
!message.includes('favicon') &&
!message.includes('Download the React DevTools')) {
this.log(LogLevel.ERROR, LogCategory.CONSOLE_ERROR, message, { args });
}
};
}
/**
*
*/
private setupErrorHandler() {
window.addEventListener('error', (event) => {
this.log(
LogLevel.ERROR,
LogCategory.CONSOLE_ERROR,
event.message,
{
filename: event.filename,
lineno: event.lineno,
colno: event.colno,
error: event.error,
},
event.error?.stack
);
});
}
/**
* Promise拒绝处理
*/
private setupUnhandledRejectionHandler() {
window.addEventListener('unhandledrejection', (event) => {
this.log(
LogLevel.ERROR,
LogCategory.CONSOLE_ERROR,
`Unhandled Promise Rejection: ${event.reason}`,
{ reason: event.reason },
event.reason?.stack
);
});
}
/**
*
*/
log(
level: LogLevel,
category: LogCategory,
message: string,
data?: any,
stack?: string
) {
if (!this.isEnabled) return;
const entry: LogEntry = {
id: `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
timestamp: Date.now(),
level,
category,
message,
data,
stack,
userAgent: navigator.userAgent,
url: window.location.href,
};
this.logs.push(entry);
// 限制日志数量
if (this.logs.length > this.maxLogs) {
this.logs = this.logs.slice(-this.maxLogs);
}
// 保存到localStorage
this.saveLogsToStorage();
// 通知监听器
this.listeners.forEach(listener => listener(entry));
// 在开发环境输出到控制台
if (import.meta.env.DEV) {
this.logToConsole(entry);
}
}
/**
*
*/
private logToConsole(entry: LogEntry) {
// 生产环境只输出ERROR和FATAL级别
if (!import.meta.env.DEV && entry.level !== LogLevel.ERROR && entry.level !== LogLevel.FATAL) {
return;
}
const style = this.getConsoleStyle(entry.level);
console.log(
`%c[${entry.level}] [${entry.category}] ${new Date(entry.timestamp).toISOString()}`,
style,
entry.message,
entry.data || ''
);
}
/**
*
*/
private getConsoleStyle(level: LogLevel): string {
const styles = {
[LogLevel.DEBUG]: 'color: #888',
[LogLevel.INFO]: 'color: #0066cc',
[LogLevel.WARN]: 'color: #ff9900',
[LogLevel.ERROR]: 'color: #cc0000; font-weight: bold',
[LogLevel.FATAL]: 'color: #fff; background: #cc0000; font-weight: bold',
};
return styles[level];
}
/**
* 便
*/
debug(category: LogCategory, message: string, data?: any) {
this.log(LogLevel.DEBUG, category, message, data);
}
info(category: LogCategory, message: string, data?: any) {
this.log(LogLevel.INFO, category, message, data);
}
warn(category: LogCategory, message: string, data?: any) {
this.log(LogLevel.WARN, category, message, data);
}
error(category: LogCategory, message: string, data?: any, stack?: string) {
this.log(LogLevel.ERROR, category, message, data, stack);
}
fatal(category: LogCategory, message: string, data?: any, stack?: string) {
this.log(LogLevel.FATAL, category, message, data, stack);
}
/**
*
*/
logUserAction(action: string, details?: any) {
this.info(LogCategory.USER_ACTION, action, details);
}
/**
* API调用
*/
logApiCall(method: string, url: string, status?: number, duration?: number, error?: any) {
const level = error ? LogLevel.ERROR : LogLevel.INFO;
this.log(level, LogCategory.API_CALL, `${method} ${url}`, {
method,
url,
status,
duration,
error,
});
}
/**
*
*/
logPerformance(metric: string, value: number, unit = 'ms') {
this.info(LogCategory.SYSTEM, `${metric}: ${value}${unit}`, { metric, value, unit });
}
/**
*
*/
getLogs(filter?: {
level?: LogLevel;
category?: LogCategory;
startTime?: number;
endTime?: number;
search?: string;
}): LogEntry[] {
let filtered = [...this.logs];
if (filter) {
if (filter.level) {
filtered = filtered.filter(log => log.level === filter.level);
}
if (filter.category) {
filtered = filtered.filter(log => log.category === filter.category);
}
if (filter.startTime) {
filtered = filtered.filter(log => log.timestamp >= filter.startTime!);
}
if (filter.endTime) {
filtered = filtered.filter(log => log.timestamp <= filter.endTime!);
}
if (filter.search) {
const search = filter.search.toLowerCase();
filtered = filtered.filter(log =>
log.message.toLowerCase().includes(search) ||
JSON.stringify(log.data).toLowerCase().includes(search)
);
}
}
return filtered;
}
/**
*
*/
clearLogs() {
this.logs = [];
localStorage.removeItem(this.storageKey);
}
/**
* JSON
*/
exportLogsAsJson(): string {
return JSON.stringify(this.logs, null, 2);
}
/**
* CSV
*/
exportLogsAsCsv(): string {
const headers = ['Timestamp', 'Level', 'Category', 'Message', 'Data', 'URL'];
const rows = this.logs.map(log => [
new Date(log.timestamp).toISOString(),
log.level,
log.category,
log.message,
JSON.stringify(log.data || {}),
log.url || '',
]);
return [
headers.join(','),
...rows.map(row => row.map(cell => `"${cell}"`).join(',')),
].join('\n');
}
/**
*
*/
downloadLogs(format: 'json' | 'csv' = 'json') {
const content = format === 'json' ? this.exportLogsAsJson() : this.exportLogsAsCsv();
const blob = new Blob([content], { type: format === 'json' ? 'application/json' : 'text/csv' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `logs-${new Date().toISOString()}.${format}`;
document.body.appendChild(a);
a.click();
document.body.removeChild(a);
URL.revokeObjectURL(url);
}
/**
*
*/
addListener(listener: (log: LogEntry) => void) {
this.listeners.add(listener);
return () => this.listeners.delete(listener);
}
/**
* /
*/
setEnabled(enabled: boolean) {
this.isEnabled = enabled;
}
/**
*
*/
getStats() {
const stats = {
total: this.logs.length,
byLevel: {} as Record<LogLevel, number>,
byCategory: {} as Record<LogCategory, number>,
errors: 0,
};
this.logs.forEach(log => {
stats.byLevel[log.level] = (stats.byLevel[log.level] || 0) + 1;
stats.byCategory[log.category] = (stats.byCategory[log.category] || 0) + 1;
if (log.level === LogLevel.ERROR || log.level === LogLevel.FATAL) {
stats.errors++;
}
});
return stats;
}
}
// 导出单例
export const logger = new Logger();

View File

@ -0,0 +1,97 @@
/**
*
*/
import { logger } from './logger';
class PerformanceMonitor {
private marks = new Map<string, number>();
/**
*
*/
start(label: string) {
this.marks.set(label, performance.now());
}
/**
*
*/
end(label: string, logToConsole = false) {
const startTime = this.marks.get(label);
if (!startTime) {
console.warn(`Performance mark "${label}" not found`);
return 0;
}
const duration = performance.now() - startTime;
this.marks.delete(label);
logger.logPerformance(label, Math.round(duration));
if (logToConsole) {
console.log(`⏱️ ${label}: ${duration.toFixed(2)}ms`);
}
return duration;
}
/**
*
*/
async measure<T>(label: string, fn: () => T | Promise<T>): Promise<T> {
this.start(label);
try {
const result = await fn();
this.end(label);
return result;
} catch (error) {
this.end(label);
throw error;
}
}
/**
* -
*/
monitorPagePerformance() {
// 不自动记录页面性能,只在需要时手动调用
return;
}
/**
* -
*/
monitorResourceLoading() {
// 不记录资源加载
return;
}
/**
* 使 -
*/
monitorMemory() {
// 不记录内存使用
return;
}
/**
* -
*/
monitorLongTasks() {
// 不记录长任务
return;
}
/**
*
*/
initAll() {
this.monitorPagePerformance();
this.monitorResourceLoading();
this.monitorMemory();
this.monitorLongTasks();
}
}
export const performanceMonitor = new PerformanceMonitor();