add logger vireer
This commit is contained in:
@@ -20,7 +20,7 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fastify/static": "^8.2.0",
|
"@fastify/static": "^8.2.0",
|
||||||
"@musistudio/llms": "^1.0.31",
|
"@musistudio/llms": "file:../../llms",
|
||||||
"dotenv": "^16.4.7",
|
"dotenv": "^16.4.7",
|
||||||
"json5": "^2.2.3",
|
"json5": "^2.2.3",
|
||||||
"openurl": "^1.1.1",
|
"openurl": "^1.1.1",
|
||||||
|
|||||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -12,8 +12,8 @@ importers:
|
|||||||
specifier: ^8.2.0
|
specifier: ^8.2.0
|
||||||
version: 8.2.0
|
version: 8.2.0
|
||||||
'@musistudio/llms':
|
'@musistudio/llms':
|
||||||
specifier: ^1.0.31
|
specifier: file:../../llms
|
||||||
version: 1.0.31(ws@8.18.3)
|
version: file:../../llms(ws@8.18.3)
|
||||||
dotenv:
|
dotenv:
|
||||||
specifier: ^16.4.7
|
specifier: ^16.4.7
|
||||||
version: 16.6.1
|
version: 16.6.1
|
||||||
@@ -266,8 +266,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
'@musistudio/llms@1.0.31':
|
'@musistudio/llms@file:../../llms':
|
||||||
resolution: {integrity: sha512-zPzGAnpB60g6iGldfxzkzohTbUtrg7y1VnTNORRESnC2Fd/4XiSdIHoaURzp7RJ4hnTYkolDLMfvlmHUmdr9AA==}
|
resolution: {directory: ../../llms, type: directory}
|
||||||
|
|
||||||
'@nodelib/fs.scandir@2.1.5':
|
'@nodelib/fs.scandir@2.1.5':
|
||||||
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==}
|
||||||
@@ -1108,7 +1108,7 @@ snapshots:
|
|||||||
|
|
||||||
'@lukeed/ms@2.0.2': {}
|
'@lukeed/ms@2.0.2': {}
|
||||||
|
|
||||||
'@musistudio/llms@1.0.31(ws@8.18.3)':
|
'@musistudio/llms@file:../../llms(ws@8.18.3)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@anthropic-ai/sdk': 0.54.0
|
'@anthropic-ai/sdk': 0.54.0
|
||||||
'@fastify/cors': 11.1.0
|
'@fastify/cors': 11.1.0
|
||||||
|
|||||||
@@ -118,7 +118,8 @@ async function run(options: RunOptions = {}) {
|
|||||||
path: HOME_DIR,
|
path: HOME_DIR,
|
||||||
maxFiles: 3,
|
maxFiles: 3,
|
||||||
interval: "1d",
|
interval: "1d",
|
||||||
compress: 'gzip'
|
compress: 'gzip',
|
||||||
|
maxSize: "50M"
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
: false;
|
: false;
|
||||||
|
|||||||
146
src/server.ts
146
src/server.ts
@@ -3,6 +3,8 @@ import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils";
|
|||||||
import { checkForUpdates, performUpdate } from "./utils";
|
import { checkForUpdates, performUpdate } from "./utils";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import fastifyStatic from "@fastify/static";
|
import fastifyStatic from "@fastify/static";
|
||||||
|
import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "fs";
|
||||||
|
import { homedir } from "os";
|
||||||
|
|
||||||
export const createServer = (config: any): Server => {
|
export const createServer = (config: any): Server => {
|
||||||
const server = new Server(config);
|
const server = new Server(config);
|
||||||
@@ -102,5 +104,149 @@ export const createServer = (config: any): Server => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 获取日志文件列表端点
|
||||||
|
server.app.get("/api/logs/files", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const logDir = join(homedir(), ".claude-code-router", "logs");
|
||||||
|
const logFiles: Array<{ name: string; path: string; size: number; lastModified: string }> = [];
|
||||||
|
|
||||||
|
if (existsSync(logDir)) {
|
||||||
|
const files = readdirSync(logDir);
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
if (file.endsWith('.log')) {
|
||||||
|
const filePath = join(logDir, file);
|
||||||
|
const stats = statSync(filePath);
|
||||||
|
|
||||||
|
logFiles.push({
|
||||||
|
name: file,
|
||||||
|
path: filePath,
|
||||||
|
size: stats.size,
|
||||||
|
lastModified: stats.mtime.toISOString()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按修改时间倒序排列
|
||||||
|
logFiles.sort((a, b) => new Date(b.lastModified).getTime() - new Date(a.lastModified).getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
return logFiles;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get log files:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to get log files" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 获取日志内容端点
|
||||||
|
server.app.get("/api/logs", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const filePath = (req.query as any).file as string;
|
||||||
|
const groupByReqId = (req.query as any).groupByReqId === 'true';
|
||||||
|
let logFilePath: string;
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
// 如果指定了文件路径,使用指定的路径
|
||||||
|
logFilePath = filePath;
|
||||||
|
} else {
|
||||||
|
// 如果没有指定文件路径,使用默认的日志文件路径
|
||||||
|
logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!existsSync(logFilePath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
const logContent = readFileSync(logFilePath, 'utf8');
|
||||||
|
const logLines = logContent.split('\n').filter(line => line.trim());
|
||||||
|
|
||||||
|
const logs = logLines.map(line => {
|
||||||
|
try {
|
||||||
|
// 尝试解析JSON格式的日志
|
||||||
|
const logEntry = JSON.parse(line);
|
||||||
|
return {
|
||||||
|
timestamp: logEntry.timestamp || logEntry.time || new Date(logEntry.time).toISOString() || new Date().toISOString(),
|
||||||
|
level: logEntry.level || 'info',
|
||||||
|
message: logEntry.message || logEntry.msg || line,
|
||||||
|
source: logEntry.source || undefined,
|
||||||
|
reqId: logEntry.reqId || undefined
|
||||||
|
};
|
||||||
|
} catch {
|
||||||
|
// 如果不是JSON格式,创建一个基本的日志条目
|
||||||
|
return {
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
level: 'info',
|
||||||
|
message: line,
|
||||||
|
source: undefined,
|
||||||
|
reqId: undefined
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 如果需要按reqId聚合
|
||||||
|
if (groupByReqId) {
|
||||||
|
const groupedLogs: { [reqId: string]: typeof logs } = {};
|
||||||
|
|
||||||
|
logs.forEach(log => {
|
||||||
|
const reqId = log.reqId || 'no-req-id';
|
||||||
|
if (!groupedLogs[reqId]) {
|
||||||
|
groupedLogs[reqId] = [];
|
||||||
|
}
|
||||||
|
groupedLogs[reqId].push(log);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按时间戳排序每个组的日志
|
||||||
|
Object.keys(groupedLogs).forEach(reqId => {
|
||||||
|
groupedLogs[reqId].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
grouped: true,
|
||||||
|
groups: groupedLogs,
|
||||||
|
summary: {
|
||||||
|
totalRequests: Object.keys(groupedLogs).length,
|
||||||
|
totalLogs: logs.length,
|
||||||
|
requests: Object.keys(groupedLogs).map(reqId => ({
|
||||||
|
reqId,
|
||||||
|
logCount: groupedLogs[reqId].length,
|
||||||
|
firstLog: groupedLogs[reqId][0]?.timestamp,
|
||||||
|
lastLog: groupedLogs[reqId][groupedLogs[reqId].length - 1]?.timestamp
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return logs;
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to get logs:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to get logs" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 清除日志内容端点
|
||||||
|
server.app.delete("/api/logs", async (req, reply) => {
|
||||||
|
try {
|
||||||
|
const filePath = (req.query as any).file as string;
|
||||||
|
let logFilePath: string;
|
||||||
|
|
||||||
|
if (filePath) {
|
||||||
|
// 如果指定了文件路径,使用指定的路径
|
||||||
|
logFilePath = filePath;
|
||||||
|
} else {
|
||||||
|
// 如果没有指定文件路径,使用默认的日志文件路径
|
||||||
|
logFilePath = join(homedir(), ".claude-code-router", "logs", "app.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (existsSync(logFilePath)) {
|
||||||
|
writeFileSync(logFilePath, '', 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
return { success: true, message: "Logs cleared successfully" };
|
||||||
|
} catch (error) {
|
||||||
|
console.error("Failed to clear logs:", error);
|
||||||
|
reply.status(500).send({ error: "Failed to clear logs" });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return server;
|
return server;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,10 +6,11 @@ import { Transformers } from "@/components/Transformers";
|
|||||||
import { Providers } from "@/components/Providers";
|
import { Providers } from "@/components/Providers";
|
||||||
import { Router } from "@/components/Router";
|
import { Router } from "@/components/Router";
|
||||||
import { JsonEditor } from "@/components/JsonEditor";
|
import { JsonEditor } from "@/components/JsonEditor";
|
||||||
|
import { LogViewer } from "@/components/LogViewer";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useConfig } from "@/components/ConfigProvider";
|
import { useConfig } from "@/components/ConfigProvider";
|
||||||
import { api } from "@/lib/api";
|
import { api } from "@/lib/api";
|
||||||
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp } from "lucide-react";
|
import { Settings, Languages, Save, RefreshCw, FileJson, CircleArrowUp, FileText } from "lucide-react";
|
||||||
import {
|
import {
|
||||||
Popover,
|
Popover,
|
||||||
PopoverContent,
|
PopoverContent,
|
||||||
@@ -32,6 +33,7 @@ function App() {
|
|||||||
const { config, error } = useConfig();
|
const { config, error } = useConfig();
|
||||||
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
const [isSettingsOpen, setIsSettingsOpen] = useState(false);
|
||||||
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false);
|
||||||
|
const [isLogViewerOpen, setIsLogViewerOpen] = useState(false);
|
||||||
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
const [isCheckingAuth, setIsCheckingAuth] = useState(true);
|
||||||
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null);
|
||||||
// 版本检查状态
|
// 版本检查状态
|
||||||
@@ -276,6 +278,9 @@ function App() {
|
|||||||
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
<Button variant="ghost" size="icon" onClick={() => setIsJsonEditorOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
<FileJson className="h-5 w-5" />
|
<FileJson className="h-5 w-5" />
|
||||||
</Button>
|
</Button>
|
||||||
|
<Button variant="ghost" size="icon" onClick={() => setIsLogViewerOpen(true)} className="transition-all-ease hover:scale-110">
|
||||||
|
<FileText className="h-5 w-5" />
|
||||||
|
</Button>
|
||||||
<Popover>
|
<Popover>
|
||||||
<PopoverTrigger asChild>
|
<PopoverTrigger asChild>
|
||||||
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
<Button variant="ghost" size="icon" className="transition-all-ease hover:scale-110">
|
||||||
@@ -350,6 +355,11 @@ function App() {
|
|||||||
onOpenChange={setIsJsonEditorOpen}
|
onOpenChange={setIsJsonEditorOpen}
|
||||||
showToast={(message, type) => setToast({ message, type })}
|
showToast={(message, type) => setToast({ message, type })}
|
||||||
/>
|
/>
|
||||||
|
<LogViewer
|
||||||
|
open={isLogViewerOpen}
|
||||||
|
onOpenChange={setIsLogViewerOpen}
|
||||||
|
showToast={(message, type) => setToast({ message, type })}
|
||||||
|
/>
|
||||||
{/* 版本更新对话框 */}
|
{/* 版本更新对话框 */}
|
||||||
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
|
<Dialog open={isUpdateDialogOpen} onOpenChange={setIsUpdateDialogOpen}>
|
||||||
<DialogContent className="max-w-2xl">
|
<DialogContent className="max-w-2xl">
|
||||||
|
|||||||
683
ui/src/components/LogViewer.tsx
Normal file
683
ui/src/components/LogViewer.tsx
Normal file
@@ -0,0 +1,683 @@
|
|||||||
|
import React, { useState, useEffect, useRef } from 'react';
|
||||||
|
import Editor from '@monaco-editor/react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import { api } from '@/lib/api';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { X, RefreshCw, Download, Trash2, ArrowLeft, File, Layers } from 'lucide-react';
|
||||||
|
|
||||||
|
interface LogViewerProps {
|
||||||
|
open: boolean;
|
||||||
|
onOpenChange: (open: boolean) => void;
|
||||||
|
showToast?: (message: string, type: 'success' | 'error' | 'warning') => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogEntry {
|
||||||
|
timestamp: string;
|
||||||
|
level: 'info' | 'warn' | 'error' | 'debug';
|
||||||
|
message: string;
|
||||||
|
source?: string;
|
||||||
|
reqId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogFile {
|
||||||
|
name: string;
|
||||||
|
path: string;
|
||||||
|
size: number;
|
||||||
|
lastModified: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedLogs {
|
||||||
|
[reqId: string]: LogEntry[];
|
||||||
|
}
|
||||||
|
|
||||||
|
interface LogGroupSummary {
|
||||||
|
reqId: string;
|
||||||
|
logCount: number;
|
||||||
|
firstLog: string;
|
||||||
|
lastLog: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GroupedLogsResponse {
|
||||||
|
grouped: boolean;
|
||||||
|
groups: { [reqId: string]: LogEntry[] };
|
||||||
|
summary: {
|
||||||
|
totalRequests: number;
|
||||||
|
totalLogs: number;
|
||||||
|
requests: LogGroupSummary[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function LogViewer({ open, onOpenChange, showToast }: LogViewerProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [logs, setLogs] = useState<LogEntry[]>([]);
|
||||||
|
const [logFiles, setLogFiles] = useState<LogFile[]>([]);
|
||||||
|
const [selectedFile, setSelectedFile] = useState<LogFile | null>(null);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isVisible, setIsVisible] = useState(false);
|
||||||
|
const [isAnimating, setIsAnimating] = useState(false);
|
||||||
|
const [autoRefresh, setAutoRefresh] = useState(false);
|
||||||
|
const [groupByReqId, setGroupByReqId] = useState(false);
|
||||||
|
const [groupedLogs, setGroupedLogs] = useState<GroupedLogsResponse | null>(null);
|
||||||
|
const [selectedReqId, setSelectedReqId] = useState<string | null>(null);
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
const refreshInterval = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
const workerRef = useRef<Worker | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
loadLogFiles();
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
// 创建内联 Web Worker
|
||||||
|
const createInlineWorker = (): Worker => {
|
||||||
|
const workerCode = `
|
||||||
|
// 日志聚合Web Worker
|
||||||
|
self.onmessage = function(event) {
|
||||||
|
const { type, data } = event.data;
|
||||||
|
|
||||||
|
if (type === 'groupLogsByReqId') {
|
||||||
|
try {
|
||||||
|
const { logs } = data;
|
||||||
|
|
||||||
|
// 按reqId聚合日志
|
||||||
|
const groupedLogs = {};
|
||||||
|
|
||||||
|
logs.forEach((log, index) => {
|
||||||
|
let reqId = log.reqId;
|
||||||
|
|
||||||
|
// 如果没有reqId,尝试从message字段中的JSON解析
|
||||||
|
if (!reqId && log.message && log.message.startsWith('{')) {
|
||||||
|
try {
|
||||||
|
const messageObj = JSON.parse(log.message);
|
||||||
|
reqId = messageObj.reqId;
|
||||||
|
} catch (e) {
|
||||||
|
// 解析失败,忽略
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reqId = reqId || 'no-req-id';
|
||||||
|
|
||||||
|
if (!groupedLogs[reqId]) {
|
||||||
|
groupedLogs[reqId] = [];
|
||||||
|
}
|
||||||
|
groupedLogs[reqId].push(log);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 按时间戳排序每个组的日志
|
||||||
|
Object.keys(groupedLogs).forEach(reqId => {
|
||||||
|
groupedLogs[reqId].sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
||||||
|
});
|
||||||
|
|
||||||
|
// 生成摘要信息
|
||||||
|
const summary = {
|
||||||
|
totalRequests: Object.keys(groupedLogs).length,
|
||||||
|
totalLogs: logs.length,
|
||||||
|
requests: Object.keys(groupedLogs).map(reqId => ({
|
||||||
|
reqId,
|
||||||
|
logCount: groupedLogs[reqId].length,
|
||||||
|
firstLog: groupedLogs[reqId][0]?.timestamp,
|
||||||
|
lastLog: groupedLogs[reqId][groupedLogs[reqId].length - 1]?.timestamp
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
|
||||||
|
const response = {
|
||||||
|
grouped: true,
|
||||||
|
groups: groupedLogs,
|
||||||
|
summary
|
||||||
|
};
|
||||||
|
|
||||||
|
// 发送结果回主线程
|
||||||
|
self.postMessage({
|
||||||
|
type: 'groupLogsResult',
|
||||||
|
data: response
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
// 发送错误回主线程
|
||||||
|
self.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
error: error instanceof Error ? error.message : 'Unknown error occurred'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
`;
|
||||||
|
|
||||||
|
const blob = new Blob([workerCode], { type: 'application/javascript' });
|
||||||
|
const workerUrl = URL.createObjectURL(blob);
|
||||||
|
return new Worker(workerUrl);
|
||||||
|
};
|
||||||
|
|
||||||
|
// 初始化Web Worker
|
||||||
|
useEffect(() => {
|
||||||
|
if (typeof Worker !== 'undefined') {
|
||||||
|
try {
|
||||||
|
// 创建内联Web Worker
|
||||||
|
workerRef.current = createInlineWorker();
|
||||||
|
|
||||||
|
// 监听Worker消息
|
||||||
|
workerRef.current.onmessage = (event) => {
|
||||||
|
const { type, data, error } = event.data;
|
||||||
|
|
||||||
|
if (type === 'groupLogsResult') {
|
||||||
|
setGroupedLogs(data);
|
||||||
|
} else if (type === 'error') {
|
||||||
|
console.error('Worker error:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.worker_error') + ': ' + error, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 监听Worker错误
|
||||||
|
workerRef.current.onerror = (error) => {
|
||||||
|
console.error('Worker error:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.worker_init_failed'), 'error');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to create worker:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.worker_init_failed'), 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 清理Worker
|
||||||
|
return () => {
|
||||||
|
if (workerRef.current) {
|
||||||
|
workerRef.current.terminate();
|
||||||
|
workerRef.current = null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [showToast, t]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (autoRefresh && open && selectedFile) {
|
||||||
|
refreshInterval.current = setInterval(() => {
|
||||||
|
loadLogs();
|
||||||
|
}, 5000); // Refresh every 5 seconds
|
||||||
|
} else if (refreshInterval.current) {
|
||||||
|
clearInterval(refreshInterval.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (refreshInterval.current) {
|
||||||
|
clearInterval(refreshInterval.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [autoRefresh, open, selectedFile]);
|
||||||
|
|
||||||
|
// Load logs when selected file changes
|
||||||
|
useEffect(() => {
|
||||||
|
if (selectedFile && open) {
|
||||||
|
setLogs([]); // Clear existing logs
|
||||||
|
loadLogs();
|
||||||
|
}
|
||||||
|
}, [selectedFile, open]);
|
||||||
|
|
||||||
|
// Handle open/close animations
|
||||||
|
useEffect(() => {
|
||||||
|
if (open) {
|
||||||
|
setIsVisible(true);
|
||||||
|
// Trigger the animation after a small delay to ensure the element is rendered
|
||||||
|
requestAnimationFrame(() => {
|
||||||
|
setIsAnimating(true);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setIsAnimating(false);
|
||||||
|
// Wait for the animation to complete before hiding
|
||||||
|
const timer = setTimeout(() => {
|
||||||
|
setIsVisible(false);
|
||||||
|
}, 300);
|
||||||
|
return () => clearTimeout(timer);
|
||||||
|
}
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const loadLogFiles = async () => {
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
const response = await api.getLogFiles();
|
||||||
|
|
||||||
|
if (response && Array.isArray(response)) {
|
||||||
|
setLogFiles(response);
|
||||||
|
setSelectedFile(null);
|
||||||
|
setLogs([]);
|
||||||
|
} else {
|
||||||
|
setLogFiles([]);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.no_log_files_available'), 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load log files:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.load_files_failed') + ': ' + (error as Error).message, 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadLogs = async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
setIsLoading(true);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
|
||||||
|
// 始终加载原始日志数据
|
||||||
|
const response = await api.getLogs(selectedFile.path);
|
||||||
|
|
||||||
|
if (response && Array.isArray(response)) {
|
||||||
|
const typedLogs: LogEntry[] = response.map(log => ({
|
||||||
|
...log,
|
||||||
|
level: (log.level as 'info' | 'warn' | 'error' | 'debug') || 'info'
|
||||||
|
}));
|
||||||
|
|
||||||
|
setLogs(typedLogs);
|
||||||
|
|
||||||
|
// 如果启用了分组,使用Web Worker进行聚合
|
||||||
|
if (groupByReqId && workerRef.current) {
|
||||||
|
workerRef.current.postMessage({
|
||||||
|
type: 'groupLogsByReqId',
|
||||||
|
data: { logs: typedLogs }
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
setGroupedLogs(null);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
setLogs([]);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.no_logs_available'), 'warning');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to load logs:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.load_failed') + ': ' + (error as Error).message, 'error');
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearLogs = async () => {
|
||||||
|
if (!selectedFile) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
await api.clearLogs(selectedFile.path);
|
||||||
|
setLogs([]);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.logs_cleared'), 'success');
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to clear logs:', error);
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.clear_failed') + ': ' + (error as Error).message, 'error');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectFile = (file: LogFile) => {
|
||||||
|
setSelectedFile(file);
|
||||||
|
setAutoRefresh(false); // Reset auto refresh when changing files
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const toggleGroupByReqId = () => {
|
||||||
|
const newValue = !groupByReqId;
|
||||||
|
setGroupByReqId(newValue);
|
||||||
|
|
||||||
|
if (newValue && selectedFile && logs.length > 0) {
|
||||||
|
// 启用聚合时,如果已有日志,则使用Worker进行聚合
|
||||||
|
if (workerRef.current) {
|
||||||
|
workerRef.current.postMessage({
|
||||||
|
type: 'groupLogsByReqId',
|
||||||
|
data: { logs }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (!newValue) {
|
||||||
|
// 禁用聚合时,清除聚合结果
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectReqId = (reqId: string) => {
|
||||||
|
setSelectedReqId(reqId);
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
const getDisplayLogs = () => {
|
||||||
|
if (groupByReqId && groupedLogs) {
|
||||||
|
if (selectedReqId && groupedLogs.groups[selectedReqId]) {
|
||||||
|
return groupedLogs.groups[selectedReqId];
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
}
|
||||||
|
return logs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const downloadLogs = () => {
|
||||||
|
if (!selectedFile || logs.length === 0) return;
|
||||||
|
|
||||||
|
const logText = logs.map(log =>
|
||||||
|
`[${log.timestamp}] [${log.level.toUpperCase()}] ${log.source ? `[${log.source}] ` : ''}${log.message}`
|
||||||
|
).join('\n');
|
||||||
|
|
||||||
|
const blob = new Blob([logText], { type: 'text/plain' });
|
||||||
|
const url = URL.createObjectURL(blob);
|
||||||
|
const a = document.createElement('a');
|
||||||
|
a.href = url;
|
||||||
|
a.download = `${selectedFile.name}-${new Date().toISOString().split('T')[0]}.txt`;
|
||||||
|
document.body.appendChild(a);
|
||||||
|
a.click();
|
||||||
|
document.body.removeChild(a);
|
||||||
|
URL.revokeObjectURL(url);
|
||||||
|
|
||||||
|
if (showToast) {
|
||||||
|
showToast(t('log_viewer.logs_downloaded'), 'success');
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatFileSize = (bytes: number) => {
|
||||||
|
if (bytes === 0) return '0 Bytes';
|
||||||
|
const k = 1024;
|
||||||
|
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
|
||||||
|
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||||
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatDate = (dateString: string) => {
|
||||||
|
return new Date(dateString).toLocaleString();
|
||||||
|
};
|
||||||
|
|
||||||
|
// 面包屑导航项类型
|
||||||
|
interface BreadcrumbItem {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
onClick: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取面包屑导航项
|
||||||
|
const getBreadcrumbs = (): BreadcrumbItem[] => {
|
||||||
|
const breadcrumbs: BreadcrumbItem[] = [
|
||||||
|
{
|
||||||
|
id: 'root',
|
||||||
|
label: t('log_viewer.title'),
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setAutoRefresh(false);
|
||||||
|
setLogs([]);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
setGroupByReqId(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
||||||
|
if (selectedFile) {
|
||||||
|
breadcrumbs.push({
|
||||||
|
id: 'file',
|
||||||
|
label: selectedFile.name,
|
||||||
|
onClick: () => {
|
||||||
|
setSelectedReqId(null);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setGroupByReqId(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectedReqId) {
|
||||||
|
breadcrumbs.push({
|
||||||
|
id: 'req',
|
||||||
|
label: `${t('log_viewer.request')} ${selectedReqId}`,
|
||||||
|
onClick: () => {
|
||||||
|
// 点击当前层级时不做任何操作
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return breadcrumbs;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 获取返回按钮的处理函数
|
||||||
|
const getBackAction = (): (() => void) | null => {
|
||||||
|
if (selectedReqId) {
|
||||||
|
return () => {
|
||||||
|
setSelectedReqId(null);
|
||||||
|
};
|
||||||
|
} else if (selectedFile) {
|
||||||
|
return () => {
|
||||||
|
setSelectedFile(null);
|
||||||
|
setAutoRefresh(false);
|
||||||
|
setLogs([]);
|
||||||
|
setGroupedLogs(null);
|
||||||
|
setSelectedReqId(null);
|
||||||
|
setGroupByReqId(false);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const formatLogsForEditor = () => {
|
||||||
|
const displayLogs = getDisplayLogs();
|
||||||
|
return JSON.stringify(displayLogs, null, 2);
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isVisible && !open) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{(isVisible || open) && (
|
||||||
|
<div
|
||||||
|
className={`fixed inset-0 z-50 transition-all duration-300 ease-out ${
|
||||||
|
isAnimating && open ? 'bg-black/50 opacity-100' : 'bg-black/0 opacity-0 pointer-events-none'
|
||||||
|
}`}
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div
|
||||||
|
ref={containerRef}
|
||||||
|
className={`fixed bottom-0 left-0 right-0 z-50 flex flex-col bg-white shadow-2xl transition-all duration-300 ease-out transform ${
|
||||||
|
isAnimating && open ? 'translate-y-0' : 'translate-y-full'
|
||||||
|
}`}
|
||||||
|
style={{
|
||||||
|
height: '100vh',
|
||||||
|
maxHeight: '100vh'
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between border-b p-4">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
{getBackAction() && (
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={getBackAction()!}
|
||||||
|
>
|
||||||
|
<ArrowLeft className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.back')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* 面包屑导航 */}
|
||||||
|
<nav className="flex items-center space-x-1 text-sm">
|
||||||
|
{getBreadcrumbs().map((breadcrumb, index) => (
|
||||||
|
<React.Fragment key={breadcrumb.id}>
|
||||||
|
{index > 0 && (
|
||||||
|
<span className="text-gray-400 mx-1">/</span>
|
||||||
|
)}
|
||||||
|
{index === getBreadcrumbs().length - 1 ? (
|
||||||
|
<span className="text-gray-900 font-medium">
|
||||||
|
{breadcrumb.label}
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
onClick={breadcrumb.onClick}
|
||||||
|
className="text-blue-600 hover:text-blue-800 transition-colors"
|
||||||
|
>
|
||||||
|
{breadcrumb.label}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</React.Fragment>
|
||||||
|
))}
|
||||||
|
</nav>
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-2">
|
||||||
|
{selectedFile && (
|
||||||
|
<>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={toggleGroupByReqId}
|
||||||
|
className={groupByReqId ? 'bg-blue-100 text-blue-700' : ''}
|
||||||
|
>
|
||||||
|
<Layers className="h-4 w-4 mr-2" />
|
||||||
|
{groupByReqId ? t('log_viewer.grouped_on') : t('log_viewer.group_by_req_id')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="ghost"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => setAutoRefresh(!autoRefresh)}
|
||||||
|
className={autoRefresh ? 'bg-blue-100 text-blue-700' : ''}
|
||||||
|
>
|
||||||
|
<RefreshCw className={`h-4 w-4 mr-2 ${autoRefresh ? 'animate-spin' : ''}`} />
|
||||||
|
{autoRefresh ? t('log_viewer.auto_refresh_on') : t('log_viewer.auto_refresh_off')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={downloadLogs}
|
||||||
|
disabled={logs.length === 0}
|
||||||
|
>
|
||||||
|
<Download className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.download')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={clearLogs}
|
||||||
|
disabled={logs.length === 0}
|
||||||
|
>
|
||||||
|
<Trash2 className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.clear')}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
size="sm"
|
||||||
|
onClick={() => onOpenChange(false)}
|
||||||
|
>
|
||||||
|
<X className="h-4 w-4 mr-2" />
|
||||||
|
{t('log_viewer.close')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex-1 min-h-0 bg-gray-50">
|
||||||
|
{isLoading ? (
|
||||||
|
<div className="flex items-center justify-center h-full">
|
||||||
|
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||||
|
</div>
|
||||||
|
) : selectedFile ? (
|
||||||
|
<>
|
||||||
|
{groupByReqId && groupedLogs && !selectedReqId ? (
|
||||||
|
// 显示日志组列表
|
||||||
|
<div className="flex flex-col h-full p-6">
|
||||||
|
<div className="mb-4 flex-shrink-0">
|
||||||
|
<h3 className="text-lg font-medium mb-2">{t('log_viewer.request_groups')}</h3>
|
||||||
|
<p className="text-sm text-gray-600">
|
||||||
|
{t('log_viewer.total_requests')}: {groupedLogs.summary.totalRequests} |
|
||||||
|
{t('log_viewer.total_logs')}: {groupedLogs.summary.totalLogs}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1 min-h-0 overflow-y-auto space-y-3">
|
||||||
|
{groupedLogs.summary.requests.map((request) => (
|
||||||
|
<div
|
||||||
|
key={request.reqId}
|
||||||
|
className="border rounded-lg p-4 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
|
onClick={() => selectReqId(request.reqId)}
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<File className="h-5 w-5 text-blue-600" />
|
||||||
|
<span className="font-medium text-sm">{request.reqId}</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
|
||||||
|
{request.logCount} {t('log_viewer.logs')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
|
<div>{t('log_viewer.first_log')}: {formatDate(request.firstLog)}</div>
|
||||||
|
<div>{t('log_viewer.last_log')}: {formatDate(request.lastLog)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
// 显示日志内容
|
||||||
|
<Editor
|
||||||
|
height="100%"
|
||||||
|
defaultLanguage="json"
|
||||||
|
value={formatLogsForEditor()}
|
||||||
|
theme="vs"
|
||||||
|
options={{
|
||||||
|
minimap: { enabled: true },
|
||||||
|
fontSize: 14,
|
||||||
|
scrollBeyondLastLine: false,
|
||||||
|
automaticLayout: true,
|
||||||
|
wordWrap: 'on',
|
||||||
|
readOnly: true,
|
||||||
|
lineNumbers: 'on',
|
||||||
|
folding: true,
|
||||||
|
renderWhitespace: 'all',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<div className="p-6">
|
||||||
|
<h3 className="text-lg font-medium mb-4">{t('log_viewer.select_file')}</h3>
|
||||||
|
{logFiles.length === 0 ? (
|
||||||
|
<div className="text-gray-500 text-center py-8">
|
||||||
|
<File className="h-12 w-12 mx-auto mb-4 text-gray-400" />
|
||||||
|
<p>{t('log_viewer.no_log_files_available')}</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||||
|
{logFiles.map((file) => (
|
||||||
|
<div
|
||||||
|
key={file.path}
|
||||||
|
className="border rounded-lg p-4 hover:bg-gray-50 cursor-pointer transition-colors"
|
||||||
|
onClick={() => selectFile(file)}
|
||||||
|
>
|
||||||
|
<div className="flex items-start justify-between mb-2">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<File className="h-5 w-5 text-blue-600" />
|
||||||
|
<span className="font-medium text-sm">{file.name}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-gray-500 space-y-1">
|
||||||
|
<div>{formatFileSize(file.size)}</div>
|
||||||
|
<div>{formatDate(file.lastModified)}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
import type { Config, Provider, Transformer } from '@/types';
|
import type { Config, Provider, Transformer } from '@/types';
|
||||||
|
|
||||||
|
// 日志聚合响应类型
|
||||||
|
interface GroupedLogsResponse {
|
||||||
|
grouped: boolean;
|
||||||
|
groups: { [reqId: string]: Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }> };
|
||||||
|
summary: {
|
||||||
|
totalRequests: number;
|
||||||
|
totalLogs: number;
|
||||||
|
requests: Array<{
|
||||||
|
reqId: string;
|
||||||
|
logCount: number;
|
||||||
|
firstLog: string;
|
||||||
|
lastLog: string;
|
||||||
|
}>;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// API Client Class for handling requests with baseUrl and apikey authentication
|
// API Client Class for handling requests with baseUrl and apikey authentication
|
||||||
class ApiClient {
|
class ApiClient {
|
||||||
private baseUrl: string;
|
private baseUrl: string;
|
||||||
@@ -204,6 +220,21 @@ class ApiClient {
|
|||||||
async performUpdate(): Promise<{ success: boolean; message: string }> {
|
async performUpdate(): Promise<{ success: boolean; message: string }> {
|
||||||
return this.post<{ success: boolean; message: string }>('/api/update/perform', {});
|
return this.post<{ success: boolean; message: string }>('/api/update/perform', {});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Get log files list
|
||||||
|
async getLogFiles(): Promise<Array<{ name: string; path: string; size: number; lastModified: string }>> {
|
||||||
|
return this.get<Array<{ name: string; path: string; size: number; lastModified: string }>>('/logs/files');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get logs from specific file
|
||||||
|
async getLogs(filePath: string): Promise<Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }>> {
|
||||||
|
return this.get<Array<{ timestamp: string; level: string; message: string; source?: string; reqId?: string }>>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clear logs from specific file
|
||||||
|
async clearLogs(filePath: string): Promise<void> {
|
||||||
|
return this.delete<void>(`/logs?file=${encodeURIComponent(filePath)}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a default instance of the API client
|
// Create a default instance of the API client
|
||||||
|
|||||||
@@ -193,5 +193,36 @@
|
|||||||
"template_download_success": "Template downloaded successfully",
|
"template_download_success": "Template downloaded successfully",
|
||||||
"template_download_success_desc": "Configuration template has been downloaded to your device",
|
"template_download_success_desc": "Configuration template has been downloaded to your device",
|
||||||
"template_download_failed": "Failed to download template"
|
"template_download_failed": "Failed to download template"
|
||||||
|
},
|
||||||
|
"log_viewer": {
|
||||||
|
"title": "Log Viewer",
|
||||||
|
"close": "Close",
|
||||||
|
"download": "Download",
|
||||||
|
"clear": "Clear",
|
||||||
|
"auto_refresh_on": "Auto Refresh On",
|
||||||
|
"auto_refresh_off": "Auto Refresh Off",
|
||||||
|
"load_failed": "Failed to load logs",
|
||||||
|
"no_logs_available": "No logs available",
|
||||||
|
"logs_cleared": "Logs cleared successfully",
|
||||||
|
"clear_failed": "Failed to clear logs",
|
||||||
|
"logs_downloaded": "Logs downloaded successfully",
|
||||||
|
"back_to_files": "Back to Files",
|
||||||
|
"select_file": "Select a log file to view",
|
||||||
|
"no_log_files_available": "No log files available",
|
||||||
|
"load_files_failed": "Failed to load log files",
|
||||||
|
"group_by_req_id": "Group by Request ID",
|
||||||
|
"grouped_on": "Grouped",
|
||||||
|
"request_groups": "Request Groups",
|
||||||
|
"total_requests": "Total Requests",
|
||||||
|
"total_logs": "Total Logs",
|
||||||
|
"request": "Request",
|
||||||
|
"logs": "logs",
|
||||||
|
"first_log": "First Log",
|
||||||
|
"last_log": "Last Log",
|
||||||
|
"back_to_all_logs": "Back to All Logs",
|
||||||
|
"worker_error": "Worker error",
|
||||||
|
"worker_init_failed": "Failed to initialize worker",
|
||||||
|
"grouping_not_supported": "Log grouping not supported by server",
|
||||||
|
"back": "Back"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -193,5 +193,36 @@
|
|||||||
"template_download_success": "模板下载成功",
|
"template_download_success": "模板下载成功",
|
||||||
"template_download_success_desc": "配置模板已下载到您的设备",
|
"template_download_success_desc": "配置模板已下载到您的设备",
|
||||||
"template_download_failed": "模板下载失败"
|
"template_download_failed": "模板下载失败"
|
||||||
|
},
|
||||||
|
"log_viewer": {
|
||||||
|
"title": "日志查看器",
|
||||||
|
"close": "关闭",
|
||||||
|
"download": "下载",
|
||||||
|
"clear": "清除",
|
||||||
|
"auto_refresh_on": "自动刷新开启",
|
||||||
|
"auto_refresh_off": "自动刷新关闭",
|
||||||
|
"load_failed": "加载日志失败",
|
||||||
|
"no_logs_available": "暂无日志",
|
||||||
|
"logs_cleared": "日志清除成功",
|
||||||
|
"clear_failed": "清除日志失败",
|
||||||
|
"logs_downloaded": "日志下载成功",
|
||||||
|
"back_to_files": "返回文件列表",
|
||||||
|
"select_file": "选择要查看的日志文件",
|
||||||
|
"no_log_files_available": "暂无日志文件",
|
||||||
|
"load_files_failed": "加载日志文件失败",
|
||||||
|
"group_by_req_id": "按请求ID分组",
|
||||||
|
"grouped_on": "已分组",
|
||||||
|
"request_groups": "请求组",
|
||||||
|
"total_requests": "总请求数",
|
||||||
|
"total_logs": "总日志数",
|
||||||
|
"request": "请求",
|
||||||
|
"logs": "条日志",
|
||||||
|
"first_log": "首条日志",
|
||||||
|
"last_log": "末条日志",
|
||||||
|
"back_to_all_logs": "返回所有日志",
|
||||||
|
"worker_error": "Worker错误",
|
||||||
|
"worker_init_failed": "Worker初始化失败",
|
||||||
|
"grouping_not_supported": "服务器不支持日志分组",
|
||||||
|
"back": "返回"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
{"root":["./src/app.tsx","./src/i18n.ts","./src/main.tsx","./src/routes.tsx","./src/types.ts","./src/vite-env.d.ts","./src/components/configprovider.tsx","./src/components/jsoneditor.tsx","./src/components/logviewer.tsx","./src/components/login.tsx","./src/components/protectedroute.tsx","./src/components/providerlist.tsx","./src/components/providers.tsx","./src/components/publicroute.tsx","./src/components/router.tsx","./src/components/settingsdialog.tsx","./src/components/statuslineconfigdialog.tsx","./src/components/statuslineimportexport.tsx","./src/components/transformerlist.tsx","./src/components/transformers.tsx","./src/components/ui/badge.tsx","./src/components/ui/button.tsx","./src/components/ui/card.tsx","./src/components/ui/color-picker.tsx","./src/components/ui/combo-input.tsx","./src/components/ui/combobox.tsx","./src/components/ui/command.tsx","./src/components/ui/dialog.tsx","./src/components/ui/input.tsx","./src/components/ui/label.tsx","./src/components/ui/multi-combobox.tsx","./src/components/ui/popover.tsx","./src/components/ui/switch.tsx","./src/components/ui/toast.tsx","./src/lib/api.ts","./src/lib/utils.ts","./src/utils/statusline.ts"],"version":"5.8.3"}
|
||||||
Reference in New Issue
Block a user