From e5741ae4703d4ddb6c90fd3c75a85c2d38800f9e Mon Sep 17 00:00:00 2001 From: musistudio Date: Fri, 5 Sep 2025 21:36:21 +0800 Subject: [PATCH] add logger vireer --- package.json | 2 +- pnpm-lock.yaml | 10 +- src/index.ts | 3 +- src/server.ts | 146 +++++++ ui/src/App.tsx | 12 +- ui/src/components/LogViewer.tsx | 683 ++++++++++++++++++++++++++++++++ ui/src/lib/api.ts | 31 ++ ui/src/locales/en.json | 31 ++ ui/src/locales/zh.json | 31 ++ ui/tsconfig.tsbuildinfo | 2 +- 10 files changed, 942 insertions(+), 9 deletions(-) create mode 100644 ui/src/components/LogViewer.tsx diff --git a/package.json b/package.json index ff2e4ca..2981faf 100644 --- a/package.json +++ b/package.json @@ -20,7 +20,7 @@ "license": "MIT", "dependencies": { "@fastify/static": "^8.2.0", - "@musistudio/llms": "^1.0.31", + "@musistudio/llms": "file:../../llms", "dotenv": "^16.4.7", "json5": "^2.2.3", "openurl": "^1.1.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 81ab13d..4f6922e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -12,8 +12,8 @@ importers: specifier: ^8.2.0 version: 8.2.0 '@musistudio/llms': - specifier: ^1.0.31 - version: 1.0.31(ws@8.18.3) + specifier: file:../../llms + version: file:../../llms(ws@8.18.3) dotenv: specifier: ^16.4.7 version: 16.6.1 @@ -266,8 +266,8 @@ packages: resolution: {integrity: sha512-9I2Zn6+NJLfaGoz9jN3lpwDgAYvfGeNYdbAIjJOqzs4Tpc+VU3Jqq4IofSUBKajiDS8k9fZIg18/z13mpk1bsA==} engines: {node: '>=8'} - '@musistudio/llms@1.0.31': - resolution: {integrity: sha512-zPzGAnpB60g6iGldfxzkzohTbUtrg7y1VnTNORRESnC2Fd/4XiSdIHoaURzp7RJ4hnTYkolDLMfvlmHUmdr9AA==} + '@musistudio/llms@file:../../llms': + resolution: {directory: ../../llms, type: directory} '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} @@ -1108,7 +1108,7 @@ snapshots: '@lukeed/ms@2.0.2': {} - '@musistudio/llms@1.0.31(ws@8.18.3)': + '@musistudio/llms@file:../../llms(ws@8.18.3)': dependencies: '@anthropic-ai/sdk': 0.54.0 '@fastify/cors': 11.1.0 diff --git a/src/index.ts b/src/index.ts index c6273db..28fe238 100644 --- a/src/index.ts +++ b/src/index.ts @@ -118,7 +118,8 @@ async function run(options: RunOptions = {}) { path: HOME_DIR, maxFiles: 3, interval: "1d", - compress: 'gzip' + compress: 'gzip', + maxSize: "50M" }), } : false; diff --git a/src/server.ts b/src/server.ts index dad7bba..1547569 100644 --- a/src/server.ts +++ b/src/server.ts @@ -3,6 +3,8 @@ import { readConfigFile, writeConfigFile, backupConfigFile } from "./utils"; import { checkForUpdates, performUpdate } from "./utils"; import { join } from "path"; import fastifyStatic from "@fastify/static"; +import { readdirSync, statSync, readFileSync, writeFileSync, existsSync } from "fs"; +import { homedir } from "os"; export const createServer = (config: any): Server => { 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; }; diff --git a/ui/src/App.tsx b/ui/src/App.tsx index bce45f8..b641680 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -6,10 +6,11 @@ import { Transformers } from "@/components/Transformers"; import { Providers } from "@/components/Providers"; import { Router } from "@/components/Router"; import { JsonEditor } from "@/components/JsonEditor"; +import { LogViewer } from "@/components/LogViewer"; import { Button } from "@/components/ui/button"; import { useConfig } from "@/components/ConfigProvider"; 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 { Popover, PopoverContent, @@ -32,6 +33,7 @@ function App() { const { config, error } = useConfig(); const [isSettingsOpen, setIsSettingsOpen] = useState(false); const [isJsonEditorOpen, setIsJsonEditorOpen] = useState(false); + const [isLogViewerOpen, setIsLogViewerOpen] = useState(false); const [isCheckingAuth, setIsCheckingAuth] = useState(true); const [toast, setToast] = useState<{ message: string; type: 'success' | 'error' | 'warning' } | null>(null); // 版本检查状态 @@ -276,6 +278,9 @@ function App() { + + )} + + {/* 面包屑导航 */} + + +
+ {selectedFile && ( + <> + + + + + + )} + +
+ + +
+ {isLoading ? ( +
+
+
+ ) : selectedFile ? ( + <> + {groupByReqId && groupedLogs && !selectedReqId ? ( + // 显示日志组列表 +
+
+

{t('log_viewer.request_groups')}

+

+ {t('log_viewer.total_requests')}: {groupedLogs.summary.totalRequests} | + {t('log_viewer.total_logs')}: {groupedLogs.summary.totalLogs} +

+
+
+ {groupedLogs.summary.requests.map((request) => ( +
selectReqId(request.reqId)} + > +
+
+ + {request.reqId} +
+ + {request.logCount} {t('log_viewer.logs')} + +
+
+
{t('log_viewer.first_log')}: {formatDate(request.firstLog)}
+
{t('log_viewer.last_log')}: {formatDate(request.lastLog)}
+
+
+ ))} +
+
+ ) : ( + // 显示日志内容 + + )} + + ) : ( +
+

{t('log_viewer.select_file')}

+ {logFiles.length === 0 ? ( +
+ +

{t('log_viewer.no_log_files_available')}

+
+ ) : ( +
+ {logFiles.map((file) => ( +
selectFile(file)} + > +
+
+ + {file.name} +
+
+
+
{formatFileSize(file.size)}
+
{formatDate(file.lastModified)}
+
+
+ ))} +
+ )} +
+ )} +
+ + + ); +} \ No newline at end of file diff --git a/ui/src/lib/api.ts b/ui/src/lib/api.ts index 70f3314..9090741 100644 --- a/ui/src/lib/api.ts +++ b/ui/src/lib/api.ts @@ -1,5 +1,21 @@ 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 class ApiClient { private baseUrl: string; @@ -204,6 +220,21 @@ class ApiClient { async performUpdate(): Promise<{ success: boolean; message: string }> { return this.post<{ success: boolean; message: string }>('/api/update/perform', {}); } + + // Get log files list + async getLogFiles(): Promise> { + return this.get>('/logs/files'); + } + + // Get logs from specific file + async getLogs(filePath: string): Promise> { + return this.get>(`/logs?file=${encodeURIComponent(filePath)}`); + } + + // Clear logs from specific file + async clearLogs(filePath: string): Promise { + return this.delete(`/logs?file=${encodeURIComponent(filePath)}`); + } } // Create a default instance of the API client diff --git a/ui/src/locales/en.json b/ui/src/locales/en.json index ca3316a..201cb56 100644 --- a/ui/src/locales/en.json +++ b/ui/src/locales/en.json @@ -193,5 +193,36 @@ "template_download_success": "Template downloaded successfully", "template_download_success_desc": "Configuration template has been downloaded to your device", "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" } } diff --git a/ui/src/locales/zh.json b/ui/src/locales/zh.json index e28346c..0f994bb 100644 --- a/ui/src/locales/zh.json +++ b/ui/src/locales/zh.json @@ -193,5 +193,36 @@ "template_download_success": "模板下载成功", "template_download_success_desc": "配置模板已下载到您的设备", "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": "返回" } } diff --git a/ui/tsconfig.tsbuildinfo b/ui/tsconfig.tsbuildinfo index 862370a..78adc38 100644 --- a/ui/tsconfig.tsbuildinfo +++ b/ui/tsconfig.tsbuildinfo @@ -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"} \ No newline at end of file +{"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"} \ No newline at end of file