feat: v1.3.0 - 添加完整的日志系统和错误处理
- 新增日志记录系统,自动记录用户核心操作和错误 - 新增日志查看器,支持筛选、搜索和导出 - 增强错误处理,显示具体错误信息 - 修复所有LLM adapter的URL双斜杠问题 - 优化审计失败提示,引导用户查看日志 - 更新版本号到v1.3.0
This commit is contained in:
parent
094677028a
commit
950325850c
32
README.md
32
README.md
|
|
@ -347,6 +347,38 @@ VITE_QWEN_API_KEY=key3
|
||||||
```
|
```
|
||||||
</details>
|
</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
|
### 🔑 获取 API Key
|
||||||
|
|
||||||
#### 支持的 LLM 平台
|
#### 支持的 LLM 平台
|
||||||
|
|
|
||||||
33
README_EN.md
33
README_EN.md
|
|
@ -13,7 +13,7 @@
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
[](https://github.com/lintsinghua/XCodeReviewer/releases)
|
[](https://github.com/lintsinghua/XCodeReviewer/releases)
|
||||||
[](https://opensource.org/licenses/MIT)
|
[](https://opensource.org/licenses/MIT)
|
||||||
[](https://reactjs.org/)
|
[](https://reactjs.org/)
|
||||||
[](https://www.typescriptlang.org/)
|
[](https://www.typescriptlang.org/)
|
||||||
|
|
@ -347,6 +347,37 @@ VITE_QWEN_API_KEY=key3
|
||||||
```
|
```
|
||||||
</details>
|
</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
|
### 🔑 Getting API Keys
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "xcode-reviewer",
|
"name": "xcode-reviewer",
|
||||||
"version": "1.2.0",
|
"version": "1.3.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,8 @@ import App from "./App.tsx";
|
||||||
import { AppWrapper } from "@/components/layout/PageMeta";
|
import { AppWrapper } from "@/components/layout/PageMeta";
|
||||||
import { isLocalMode } from "@/shared/config/database";
|
import { isLocalMode } from "@/shared/config/database";
|
||||||
import { initLocalDatabase } from "@/shared/utils/initLocalDB";
|
import { initLocalDatabase } from "@/shared/utils/initLocalDB";
|
||||||
|
import { ErrorBoundary } from "@/components/common/ErrorBoundary";
|
||||||
|
import "@/shared/utils/fetchWrapper"; // 初始化fetch拦截器
|
||||||
|
|
||||||
// 初始化本地数据库
|
// 初始化本地数据库
|
||||||
if (isLocalMode) {
|
if (isLocalMode) {
|
||||||
|
|
@ -13,8 +15,10 @@ if (isLocalMode) {
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(
|
createRoot(document.getElementById("root")!).render(
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
|
<ErrorBoundary>
|
||||||
<AppWrapper>
|
<AppWrapper>
|
||||||
<App />
|
<App />
|
||||||
</AppWrapper>
|
</AppWrapper>
|
||||||
|
</ErrorBoundary>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ import InstantAnalysis from "@/pages/InstantAnalysis";
|
||||||
import AuditTasks from "@/pages/AuditTasks";
|
import AuditTasks from "@/pages/AuditTasks";
|
||||||
import TaskDetail from "@/pages/TaskDetail";
|
import TaskDetail from "@/pages/TaskDetail";
|
||||||
import AdminDashboard from "@/pages/AdminDashboard";
|
import AdminDashboard from "@/pages/AdminDashboard";
|
||||||
|
import LogsPage from "@/pages/LogsPage";
|
||||||
import type { ReactNode } from 'react';
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface RouteConfig {
|
export interface RouteConfig {
|
||||||
|
|
@ -64,6 +65,12 @@ const routes: RouteConfig[] = [
|
||||||
element: <RecycleBin />,
|
element: <RecycleBin />,
|
||||||
visible: true,
|
visible: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "系统日志",
|
||||||
|
path: "/logs",
|
||||||
|
element: <LogsPage />,
|
||||||
|
visible: true,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default routes;
|
export default routes;
|
||||||
|
|
@ -195,6 +195,18 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
|
|
||||||
console.log('✅ 任务创建成功:', taskId);
|
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);
|
onOpenChange(false);
|
||||||
resetForm();
|
resetForm();
|
||||||
|
|
@ -207,7 +219,14 @@ export default function CreateTaskDialog({ open, onOpenChange, onTaskCreated, pr
|
||||||
toast.success("审计任务已创建并启动");
|
toast.success("审计任务已创建并启动");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('❌ 创建任务失败:', 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 {
|
} finally {
|
||||||
setCreating(false);
|
setCreating(false);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -301,13 +301,55 @@ export default function TerminalProgressDialog({
|
||||||
addLog("", "info"); // 空行分隔
|
addLog("", "info"); // 空行分隔
|
||||||
addLog("❌ 审计任务执行失败", "error");
|
addLog("❌ 审计任务执行失败", "error");
|
||||||
addLog("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "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(" • 网络连接问题", "error");
|
addLog(" • 网络连接问题", "error");
|
||||||
addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error");
|
addLog(" • 仓库访问权限不足(私有仓库需配置 Token)", "error");
|
||||||
addLog(" • GitHub/GitLab API 限流", "error");
|
addLog(" • GitHub/GitLab API 限流", "error");
|
||||||
addLog(" • 代码文件格式错误", "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("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━", "error");
|
||||||
addLog("💡 建议: 检查网络连接、仓库配置和 Token 设置后重试", "warning");
|
addLog("💡 建议: 检查系统配置和网络连接后重试", "warning");
|
||||||
|
addLog("📋 查看完整日志: 导航栏 -> 系统日志", "warning");
|
||||||
|
|
||||||
setIsFailed(true);
|
setIsFailed(true);
|
||||||
if (pollIntervalRef.current) {
|
if (pollIntervalRef.current) {
|
||||||
|
|
@ -503,6 +545,18 @@ export default function TerminalProgressDialog({
|
||||||
</Button>
|
</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) && (
|
{(isCompleted || isFailed || isCancelled) && (
|
||||||
<button
|
<button
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -188,6 +188,18 @@ export function SystemConfig() {
|
||||||
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(config));
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
setConfigSource('runtime');
|
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("配置已保存!刷新页面后生效");
|
toast.success("配置已保存!刷新页面后生效");
|
||||||
|
|
||||||
// 提示用户刷新页面
|
// 提示用户刷新页面
|
||||||
|
|
@ -198,17 +210,42 @@ export function SystemConfig() {
|
||||||
}, 1000);
|
}, 1000);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to save config:', 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 = () => {
|
const resetConfig = () => {
|
||||||
if (window.confirm("确定要重置为构建时配置吗?这将清除所有运行时配置。")) {
|
if (window.confirm("确定要重置为构建时配置吗?这将清除所有运行时配置。")) {
|
||||||
|
try {
|
||||||
localStorage.removeItem(STORAGE_KEY);
|
localStorage.removeItem(STORAGE_KEY);
|
||||||
loadFromEnv();
|
loadFromEnv();
|
||||||
setHasChanges(false);
|
setHasChanges(false);
|
||||||
setConfigSource('build');
|
setConfigSource('build');
|
||||||
|
|
||||||
|
// 记录用户操作
|
||||||
|
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
|
||||||
|
logger.logUserAction('重置系统配置', { action: 'reset_to_build_config' });
|
||||||
|
});
|
||||||
|
|
||||||
toast.success("已重置为构建时配置");
|
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}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -386,7 +423,7 @@ export function SystemConfig() {
|
||||||
placeholder="例如:https://api.example.com/v1"
|
placeholder="例如:https://api.example.com/v1"
|
||||||
/>
|
/>
|
||||||
<div className="text-xs text-muted-foreground space-y-1">
|
<div className="text-xs text-muted-foreground space-y-1">
|
||||||
<p>💡 <strong>使用 API 中转站?</strong>在这里填入中转站地址</p>
|
<p>💡 <strong>使用 API 中转站?</strong>在这里填入中转站地址。配置保存后会在实际使用时自动验证。</p>
|
||||||
<details className="cursor-pointer">
|
<details className="cursor-pointer">
|
||||||
<summary className="text-primary hover:underline">查看常见 API 中转示例</summary>
|
<summary className="text-primary hover:underline">查看常见 API 中转示例</summary>
|
||||||
<div className="mt-2 p-3 bg-muted rounded space-y-1 text-xs">
|
<div className="mt-2 p-3 bg-muted rounded space-y-1 text-xs">
|
||||||
|
|
|
||||||
|
|
@ -75,6 +75,17 @@ export async function runRepositoryAudit(params: {
|
||||||
|
|
||||||
console.log(`🚀 ${repoType}任务已创建: ${taskId},准备启动后台扫描...`);
|
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 () => {
|
(async () => {
|
||||||
console.log(`🎬 后台扫描任务开始执行: ${taskId}`);
|
console.log(`🎬 后台扫描任务开始执行: ${taskId}`);
|
||||||
|
|
@ -283,10 +294,29 @@ export async function runRepositoryAudit(params: {
|
||||||
completed_at: new Date().toISOString()
|
completed_at: new Date().toISOString()
|
||||||
} as any);
|
} 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);
|
taskControl.cleanupTask(taskId);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('❌ GitHub审计任务执行失败:', e);
|
console.error('❌ GitHub审计任务执行失败:', e);
|
||||||
console.error('错误详情:', e);
|
console.error('错误详情:', e);
|
||||||
|
|
||||||
|
// 记录审计失败
|
||||||
|
import('@/shared/utils/errorHandler').then(({ handleError }) => {
|
||||||
|
handleError(e, `审计任务失败: ${taskId}`);
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
||||||
} catch (updateError) {
|
} catch (updateError) {
|
||||||
|
|
|
||||||
|
|
@ -139,6 +139,16 @@ export async function scanZipFile(params: {
|
||||||
|
|
||||||
console.log(`🚀 ZIP任务已创建: ${taskId},准备启动后台扫描...`);
|
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 () => {
|
(async () => {
|
||||||
console.log(`🎬 后台扫描任务开始执行: ${taskId}`);
|
console.log(`🎬 后台扫描任务开始执行: ${taskId}`);
|
||||||
|
|
@ -344,9 +354,30 @@ export async function scanZipFile(params: {
|
||||||
completed_at: new Date().toISOString()
|
completed_at: new Date().toISOString()
|
||||||
} as any);
|
} 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();
|
resolve();
|
||||||
} catch (processingError) {
|
} catch (processingError) {
|
||||||
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
await api.updateAuditTask(taskId, { status: "failed" } as any);
|
||||||
|
|
||||||
|
// 记录处理错误
|
||||||
|
import('@/shared/utils/errorHandler').then(({ handleError }) => {
|
||||||
|
handleError(processingError, `ZIP审计任务处理失败: ${taskId}`);
|
||||||
|
});
|
||||||
|
|
||||||
reject(processingError);
|
reject(processingError);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -782,9 +782,9 @@ public class Example {
|
||||||
|
|
||||||
{/* 分析进行中状态 */}
|
{/* 分析进行中状态 */}
|
||||||
{analyzing && (
|
{analyzing && (
|
||||||
<Card ref={loadingCardRef} className="card-modern">
|
<Card className="card-modern">
|
||||||
<CardContent className="py-16">
|
<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="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 className="animate-spin rounded-full h-12 w-12 border-4 border-primary border-t-transparent"></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
/**
|
||||||
|
* 日志查看页面
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { LogViewer } from '@/components/debug/LogViewer';
|
||||||
|
|
||||||
|
export default function LogsPage() {
|
||||||
|
return (
|
||||||
|
<div className="h-screen">
|
||||||
|
<LogViewer />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
@ -100,13 +100,29 @@ export default function Projects() {
|
||||||
// 无登录场景下不传 owner_id,由后端置为 null
|
// 无登录场景下不传 owner_id,由后端置为 null
|
||||||
} as any);
|
} as any);
|
||||||
|
|
||||||
|
// 记录用户操作
|
||||||
|
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
|
||||||
|
logger.logUserAction('创建项目', {
|
||||||
|
projectName: createForm.name,
|
||||||
|
repositoryType: createForm.repository_type,
|
||||||
|
languages: createForm.programming_languages,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
toast.success("项目创建成功");
|
toast.success("项目创建成功");
|
||||||
setShowCreateDialog(false);
|
setShowCreateDialog(false);
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
loadProjects();
|
loadProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to create project:', 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);
|
clearInterval(progressInterval);
|
||||||
setUploadProgress(100);
|
setUploadProgress(100);
|
||||||
|
|
||||||
|
// 记录用户操作
|
||||||
|
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
|
||||||
|
logger.logUserAction('上传ZIP文件创建项目', {
|
||||||
|
projectName: project.name,
|
||||||
|
fileName: file.name,
|
||||||
|
fileSize: file.size,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// 关闭创建对话框
|
// 关闭创建对话框
|
||||||
setShowCreateDialog(false);
|
setShowCreateDialog(false);
|
||||||
resetCreateForm();
|
resetCreateForm();
|
||||||
|
|
@ -181,7 +206,14 @@ export default function Projects() {
|
||||||
|
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Upload failed:', error);
|
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 {
|
} finally {
|
||||||
setUploading(false);
|
setUploading(false);
|
||||||
setUploadProgress(0);
|
setUploadProgress(0);
|
||||||
|
|
@ -265,6 +297,15 @@ export default function Projects() {
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await api.deleteProject(projectToDelete.id);
|
await api.deleteProject(projectToDelete.id);
|
||||||
|
|
||||||
|
// 记录用户操作
|
||||||
|
import('@/shared/utils/logger').then(({ logger, LogCategory }) => {
|
||||||
|
logger.logUserAction('删除项目', {
|
||||||
|
projectId: projectToDelete.id,
|
||||||
|
projectName: projectToDelete.name,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
toast.success(`项目 "${projectToDelete.name}" 已移到回收站`, {
|
toast.success(`项目 "${projectToDelete.name}" 已移到回收站`, {
|
||||||
description: '您可以在回收站中恢复此项目',
|
description: '您可以在回收站中恢复此项目',
|
||||||
duration: 4000
|
duration: 4000
|
||||||
|
|
@ -274,7 +315,14 @@ export default function Projects() {
|
||||||
loadProjects();
|
loadProjects();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to delete project:', 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}`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -58,7 +58,7 @@ export class ClaudeAdapter extends BaseLLMAdapter {
|
||||||
Object.assign(headers, this.config.customHeaders);
|
Object.assign(headers, this.config.customHeaders);
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetch(`${this.baseUrl}/messages`, {
|
const response = await fetch(`${this.baseUrl.replace(/\/$/, '')}/messages`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
|
|
|
||||||
|
|
@ -37,7 +37,7 @@ export class DeepSeekAdapter extends BaseLLMAdapter {
|
||||||
Object.assign(headers, this.config.customHeaders);
|
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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class DoubaoAdapter extends BaseLLMAdapter {
|
||||||
};
|
};
|
||||||
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
|
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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class MinimaxAdapter extends BaseLLMAdapter {
|
||||||
};
|
};
|
||||||
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
|
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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class MoonshotAdapter extends BaseLLMAdapter {
|
||||||
};
|
};
|
||||||
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
|
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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ export class OllamaAdapter extends BaseLLMAdapter {
|
||||||
Object.assign(headers, 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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -58,7 +58,7 @@ export class OpenAIAdapter extends BaseLLMAdapter {
|
||||||
requestBody.max_tokens = request.maxTokens ?? this.config.maxTokens;
|
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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify(requestBody),
|
body: JSON.stringify(requestBody),
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class QwenAdapter extends BaseLLMAdapter {
|
||||||
};
|
};
|
||||||
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
|
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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -32,7 +32,7 @@ export class ZhipuAdapter extends BaseLLMAdapter {
|
||||||
};
|
};
|
||||||
if (this.config.customHeaders) Object.assign(headers, this.config.customHeaders);
|
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',
|
method: 'POST',
|
||||||
headers: this.buildHeaders(headers),
|
headers: this.buildHeaders(headers),
|
||||||
body: JSON.stringify({
|
body: JSON.stringify({
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
@ -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 };
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
/**
|
||||||
|
* 工具函数统一导出
|
||||||
|
*/
|
||||||
|
|
||||||
|
export * from './logger';
|
||||||
|
export * from './errorHandler';
|
||||||
|
export * from './performanceMonitor';
|
||||||
|
|
@ -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();
|
||||||
|
|
@ -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();
|
||||||
Loading…
Reference in New Issue