mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-06-16 21:03:58 +08:00
Merge pull request #303 from router-for-me:home
feat(logs): enhance logging functionality and UI updates
This commit is contained in:
@@ -213,7 +213,6 @@ export function MainLayout() {
|
||||
|
||||
const logout = useAuthStore((state) => state.logout);
|
||||
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const fetchConfig = useConfigStore((state) => state.fetchConfig);
|
||||
const clearCache = useConfigStore((state) => state.clearCache);
|
||||
|
||||
@@ -431,16 +430,12 @@ export function MainLayout() {
|
||||
metaKey: 'nav_meta.quota_management',
|
||||
icon: sidebarIcons.quota,
|
||||
},
|
||||
...(config?.loggingToFile
|
||||
? [
|
||||
{
|
||||
path: '/logs',
|
||||
labelKey: 'nav.logs',
|
||||
metaKey: 'nav_meta.logs',
|
||||
icon: sidebarIcons.logs,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
path: '/logs',
|
||||
labelKey: 'nav.logs',
|
||||
metaKey: 'nav_meta.logs',
|
||||
icon: sidebarIcons.logs,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
|
||||
@@ -730,12 +730,16 @@
|
||||
},
|
||||
"logs": {
|
||||
"title": "Logs Viewer",
|
||||
"runtime_unknown": "Current runtime: detecting",
|
||||
"runtime_cpa": "Current runtime: CPA",
|
||||
"runtime_home": "Current runtime: Home",
|
||||
"refresh_button": "Refresh Logs",
|
||||
"clear_button": "Clear Logs",
|
||||
"download_button": "Download Logs",
|
||||
"error_log_button": "Select Error Log",
|
||||
"error_logs_modal_title": "Error Request Logs",
|
||||
"error_logs_description": "Pick an error request log file to download (only generated when request logging is off).",
|
||||
"error_logs_home_unavailable": "In Home mode, use Log Content for database logs. Error request log files only apply to CPA file logs.",
|
||||
"error_logs_request_log_enabled": "Request logging is enabled, so this list will always be empty. Disable request logging and refresh to view error logs.",
|
||||
"error_logs_empty": "No error request log files found",
|
||||
"error_logs_load_error": "Failed to load error log list",
|
||||
@@ -748,6 +752,13 @@
|
||||
"request_log_download_success": "Request log downloaded successfully",
|
||||
"empty_title": "No Logs Available",
|
||||
"empty_desc": "When \"Enable logging to file\" is enabled, logs will be displayed here",
|
||||
"cpa_file_logging_required": "The connected runtime is CPA. Log viewing requires logging to file to be enabled first.",
|
||||
"cpa_file_logging_required_title": "File logging is required",
|
||||
"cpa_file_logging_required_desc": "CPA reads this log view from local log files. Enable logging-to-file in the config, then refresh.",
|
||||
"file_logging_required": "This logs endpoint requires logging to file before file logs can be read.",
|
||||
"file_logging_required_title": "File logging is required",
|
||||
"file_logging_required_desc": "This logs endpoint reads local log files. Enable logging-to-file in the config, then refresh.",
|
||||
"home_clear_unavailable": "Home logs come from database queries and cannot be cleared here.",
|
||||
"log_content": "Log Content",
|
||||
"loading": "Loading logs...",
|
||||
"load_error": "Failed to load logs",
|
||||
|
||||
@@ -727,12 +727,16 @@
|
||||
},
|
||||
"logs": {
|
||||
"title": "Просмотр журналов",
|
||||
"runtime_unknown": "Текущий режим: определяется",
|
||||
"runtime_cpa": "Текущий режим: CPA",
|
||||
"runtime_home": "Текущий режим: Home",
|
||||
"refresh_button": "Обновить журналы",
|
||||
"clear_button": "Очистить журналы",
|
||||
"download_button": "Скачать журналы",
|
||||
"error_log_button": "Выбрать журнал ошибок",
|
||||
"error_logs_modal_title": "Журналы ошибок запросов",
|
||||
"error_logs_description": "Выберите файл журнала ошибок запроса для скачивания (создаётся только при отключённом журналировании запросов).",
|
||||
"error_logs_home_unavailable": "В режиме Home смотрите журналы базы данных на вкладке содержимого. Файлы ошибок запросов относятся только к файловым журналам CPA.",
|
||||
"error_logs_request_log_enabled": "Журналирование запросов включено, поэтому этот список всегда будет пустым. Отключите журналирование запросов и обновите список, чтобы просмотреть журналы ошибок.",
|
||||
"error_logs_empty": "Файлы журнала ошибок запросов не найдены",
|
||||
"error_logs_load_error": "Не удалось загрузить список журналов ошибок",
|
||||
@@ -745,6 +749,13 @@
|
||||
"request_log_download_success": "Журнал запросов успешно скачан",
|
||||
"empty_title": "Журналы недоступны",
|
||||
"empty_desc": "Когда включена опция \"Включить журналирование в файл\", журналы появятся здесь",
|
||||
"cpa_file_logging_required": "Подключён режим CPA. Для просмотра журналов сначала включите запись журналов в файл.",
|
||||
"cpa_file_logging_required_title": "Требуется файловое журналирование",
|
||||
"cpa_file_logging_required_desc": "CPA читает этот журнал из локальных файлов. Включите logging-to-file в конфигурации и обновите страницу.",
|
||||
"file_logging_required": "Этот endpoint журналов требует включить запись журналов в файл перед чтением файловых журналов.",
|
||||
"file_logging_required_title": "Требуется файловое журналирование",
|
||||
"file_logging_required_desc": "Этот endpoint журналов читает локальные файлы. Включите logging-to-file в конфигурации и обновите страницу.",
|
||||
"home_clear_unavailable": "Журналы Home загружаются из базы данных и не могут быть очищены здесь.",
|
||||
"log_content": "Содержимое журнала",
|
||||
"loading": "Загрузка журналов...",
|
||||
"load_error": "Не удалось загрузить журналы",
|
||||
|
||||
@@ -730,12 +730,16 @@
|
||||
},
|
||||
"logs": {
|
||||
"title": "日志查看",
|
||||
"runtime_unknown": "当前运行端:识别中",
|
||||
"runtime_cpa": "当前运行端:CPA",
|
||||
"runtime_home": "当前运行端:Home",
|
||||
"refresh_button": "刷新日志",
|
||||
"clear_button": "清空日志",
|
||||
"download_button": "下载日志",
|
||||
"error_log_button": "选择错误日志",
|
||||
"error_logs_modal_title": "错误请求日志",
|
||||
"error_logs_description": "请选择要下载的错误请求日志文件(仅在关闭请求日志时生成)。",
|
||||
"error_logs_home_unavailable": "Home 模式下请在日志内容页查看数据库日志,错误请求日志列表仅适用于 CPA 文件日志。",
|
||||
"error_logs_request_log_enabled": "当前已开启请求日志,按接口约定错误请求日志列表会始终为空。关闭请求日志后再刷新即可查看。",
|
||||
"error_logs_empty": "暂无错误请求日志文件",
|
||||
"error_logs_load_error": "加载错误日志列表失败",
|
||||
@@ -748,6 +752,13 @@
|
||||
"request_log_download_success": "报文下载成功",
|
||||
"empty_title": "暂无日志记录",
|
||||
"empty_desc": "当启用\"日志记录到文件\"功能后,日志将显示在这里",
|
||||
"cpa_file_logging_required": "当前连接的是 CPA,日志查看需要先开启“日志记录到文件”。",
|
||||
"cpa_file_logging_required_title": "需要开启文件日志",
|
||||
"cpa_file_logging_required_desc": "CPA 的日志接口读取本地日志文件。请在配置中开启 logging-to-file 后再刷新查看。",
|
||||
"file_logging_required": "当前日志接口需要先开启“日志记录到文件”后才能读取文件日志。",
|
||||
"file_logging_required_title": "需要开启文件日志",
|
||||
"file_logging_required_desc": "当前日志接口读取本地日志文件。请在配置中开启 logging-to-file 后再刷新查看。",
|
||||
"home_clear_unavailable": "Home 日志来自数据库查询,当前不支持从这里清空。",
|
||||
"log_content": "日志内容",
|
||||
"loading": "正在加载日志...",
|
||||
"load_error": "加载日志失败",
|
||||
|
||||
@@ -756,12 +756,16 @@
|
||||
},
|
||||
"logs": {
|
||||
"title": "記錄檢視",
|
||||
"runtime_unknown": "目前執行端:識別中",
|
||||
"runtime_cpa": "目前執行端:CPA",
|
||||
"runtime_home": "目前執行端:Home",
|
||||
"refresh_button": "重新整理記錄",
|
||||
"clear_button": "清空記錄",
|
||||
"download_button": "下載記錄",
|
||||
"error_log_button": "選擇錯誤記錄",
|
||||
"error_logs_modal_title": "錯誤請求記錄",
|
||||
"error_logs_description": "請選擇要下載的錯誤請求記錄檔案(僅在關閉請求記錄時產生)。",
|
||||
"error_logs_home_unavailable": "Home 模式下請在記錄內容頁查看資料庫記錄,錯誤請求記錄清單僅適用於 CPA 檔案記錄。",
|
||||
"error_logs_request_log_enabled": "目前已開啟請求記錄,按介面約定錯誤請求記錄清單會始終為空。關閉請求記錄後再重新整理即可查看。",
|
||||
"error_logs_empty": "暫無錯誤請求記錄檔案",
|
||||
"error_logs_load_error": "載入錯誤記錄清單失敗",
|
||||
@@ -774,6 +778,13 @@
|
||||
"request_log_download_success": "封包下載成功",
|
||||
"empty_title": "暫無記錄",
|
||||
"empty_desc": "當啟用「記錄到檔案」功能後,記錄將顯示在這裡",
|
||||
"cpa_file_logging_required": "目前連線的是 CPA,記錄檢視需要先開啟「記錄到檔案」。",
|
||||
"cpa_file_logging_required_title": "需要開啟檔案記錄",
|
||||
"cpa_file_logging_required_desc": "CPA 的記錄介面會讀取本機記錄檔。請在設定中開啟 logging-to-file 後再重新整理查看。",
|
||||
"file_logging_required": "目前記錄介面需要先開啟「記錄到檔案」後才能讀取檔案記錄。",
|
||||
"file_logging_required_title": "需要開啟檔案記錄",
|
||||
"file_logging_required_desc": "目前記錄介面會讀取本機記錄檔。請在設定中開啟 logging-to-file 後再重新整理查看。",
|
||||
"home_clear_unavailable": "Home 記錄來自資料庫查詢,目前不支援從這裡清空。",
|
||||
"log_content": "記錄內容",
|
||||
"loading": "正在載入記錄...",
|
||||
"load_error": "載入記錄失敗",
|
||||
|
||||
@@ -13,11 +13,25 @@
|
||||
}
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: $spacing-md;
|
||||
margin: 0 0 $spacing-lg 0;
|
||||
|
||||
@include mobile {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
gap: $spacing-xs;
|
||||
}
|
||||
}
|
||||
|
||||
.pageTitle {
|
||||
font-size: 28px;
|
||||
font-weight: 700;
|
||||
color: var(--text-primary);
|
||||
margin: 0 0 $spacing-lg 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.tabBar {
|
||||
@@ -84,6 +98,17 @@
|
||||
}
|
||||
}
|
||||
|
||||
.runtimeNotice {
|
||||
flex: 0 0 auto;
|
||||
padding: 4px 10px;
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: $radius-full;
|
||||
color: var(--text-secondary);
|
||||
background: var(--bg-secondary);
|
||||
font-size: 12px;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -622,6 +647,9 @@
|
||||
@media (max-height: 820px) {
|
||||
.pageTitle {
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
margin-bottom: $spacing-md;
|
||||
}
|
||||
|
||||
@@ -671,6 +699,9 @@
|
||||
@media (max-height: 600px) {
|
||||
.pageTitle {
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.pageHeader {
|
||||
margin-bottom: $spacing-sm;
|
||||
}
|
||||
|
||||
|
||||
+159
-19
@@ -23,7 +23,8 @@ import {
|
||||
import { useHeaderRefresh } from '@/hooks/useHeaderRefresh';
|
||||
import { useLocalStorage } from '@/hooks/useLocalStorage';
|
||||
import { useAuthStore, useConfigStore, useNotificationStore } from '@/stores';
|
||||
import { logsApi } from '@/services/api/logs';
|
||||
import { logsApi, type LogsQuery } from '@/services/api/logs';
|
||||
import { versionApi } from '@/services/api/version';
|
||||
import { copyToClipboard } from '@/utils/clipboard';
|
||||
import { downloadBlob } from '@/utils/download';
|
||||
import { MANAGEMENT_API_PREFIX } from '@/utils/constants';
|
||||
@@ -61,14 +62,44 @@ const getErrorMessage = (err: unknown): string => {
|
||||
return typeof message === 'string' ? message : '';
|
||||
};
|
||||
|
||||
const getErrorPayloadText = (err: unknown): string => {
|
||||
if (typeof err !== 'object' || err === null) return '';
|
||||
const payloads = [
|
||||
(err as { data?: unknown }).data,
|
||||
(err as { details?: unknown }).details
|
||||
].filter((payload) => payload !== undefined);
|
||||
return payloads
|
||||
.map((payload) => {
|
||||
if (typeof payload === 'string') return payload;
|
||||
try {
|
||||
return JSON.stringify(payload);
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
})
|
||||
.join(' ');
|
||||
};
|
||||
|
||||
const isLoggingToFileDisabledError = (err: unknown): boolean => {
|
||||
const text = `${getErrorMessage(err)} ${getErrorPayloadText(err)}`.toLowerCase();
|
||||
return text.includes('logging to file disabled');
|
||||
};
|
||||
|
||||
type TabType = 'logs' | 'errors';
|
||||
|
||||
export function LogsPage() {
|
||||
const { t } = useTranslation();
|
||||
const { showNotification, showConfirmation } = useNotificationStore();
|
||||
const connectionStatus = useAuthStore((state) => state.connectionStatus);
|
||||
const serverRuntimeKind = useAuthStore((state) => state.serverRuntimeKind);
|
||||
const updateServerRuntimeKind = useAuthStore((state) => state.updateServerRuntimeKind);
|
||||
const config = useConfigStore((state) => state.config);
|
||||
const requestLogEnabled = config?.requestLog ?? false;
|
||||
const loggingToFileEnabled = config?.loggingToFile ?? false;
|
||||
const cpaNeedsFileLogging = serverRuntimeKind === 'cpa' && !loggingToFileEnabled;
|
||||
const isHomeRuntime = serverRuntimeKind === 'home';
|
||||
const [fileLoggingRequired, setFileLoggingRequired] = useState(false);
|
||||
const showFileLoggingRequired = cpaNeedsFileLogging || fileLoggingRequired;
|
||||
|
||||
const [activeTab, setActiveTab] = useState<TabType>('logs');
|
||||
const [logState, setLogState] = useState<LogState>({ buffer: [], visibleFrom: 0 });
|
||||
@@ -93,6 +124,7 @@ export function LogsPage() {
|
||||
const [requestLogDownloading, setRequestLogDownloading] = useState(false);
|
||||
|
||||
const logScrollerRef = useRef<ReturnType<typeof useLogScroller> | null>(null);
|
||||
const requestLogHomeIpByIdRef = useRef<Record<string, string>>({});
|
||||
const longPressRef = useRef<{
|
||||
timer: number | null;
|
||||
startX: number;
|
||||
@@ -102,10 +134,13 @@ export function LogsPage() {
|
||||
const logRequestInFlightRef = useRef(false);
|
||||
const pendingFullReloadRef = useRef(false);
|
||||
|
||||
// 保存最新时间戳用于增量获取
|
||||
const latestTimestampRef = useRef<number>(0);
|
||||
// 保存最新游标用于增量获取
|
||||
const latestCursorRef = useRef<LogsQuery['after']>(undefined);
|
||||
|
||||
const disableControls = connectionStatus !== 'connected';
|
||||
const refreshDisabled = disableControls || loading || cpaNeedsFileLogging;
|
||||
const autoRefreshDisabled = disableControls || showFileLoggingRequired;
|
||||
const clearDisabled = disableControls || showFileLoggingRequired || isHomeRuntime;
|
||||
|
||||
const loadLogs = async (incremental = false) => {
|
||||
if (connectionStatus !== 'connected') {
|
||||
@@ -113,6 +148,18 @@ export function LogsPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
if (cpaNeedsFileLogging) {
|
||||
if (!incremental) {
|
||||
latestCursorRef.current = undefined;
|
||||
requestLogHomeIpByIdRef.current = {};
|
||||
setFileLoggingRequired(false);
|
||||
setLogState({ buffer: [], visibleFrom: 0 });
|
||||
setError('');
|
||||
setLoading(false);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (logRequestInFlightRef.current) {
|
||||
if (!incremental) {
|
||||
pendingFullReloadRef.current = true;
|
||||
@@ -135,13 +182,25 @@ export function LogsPage() {
|
||||
scrollerInstance?.requestScrollToBottom();
|
||||
}
|
||||
|
||||
const params =
|
||||
incremental && latestTimestampRef.current > 0 ? { after: latestTimestampRef.current } : {};
|
||||
const params: LogsQuery =
|
||||
incremental && latestCursorRef.current
|
||||
? { after: latestCursorRef.current, limit: MAX_BUFFER_LINES }
|
||||
: { limit: MAX_BUFFER_LINES };
|
||||
const data = await logsApi.fetchLogs(params);
|
||||
setFileLoggingRequired(false);
|
||||
|
||||
// 更新时间戳
|
||||
if (data['latest-timestamp']) {
|
||||
latestTimestampRef.current = data['latest-timestamp'];
|
||||
// 更新游标
|
||||
if (data.latestCursor) {
|
||||
latestCursorRef.current = data.latestCursor;
|
||||
} else if (!incremental) {
|
||||
latestCursorRef.current = undefined;
|
||||
}
|
||||
if (data.requestLogHomeIpById) {
|
||||
requestLogHomeIpByIdRef.current = incremental
|
||||
? { ...requestLogHomeIpByIdRef.current, ...data.requestLogHomeIpById }
|
||||
: data.requestLogHomeIpById;
|
||||
} else if (!incremental) {
|
||||
requestLogHomeIpByIdRef.current = {};
|
||||
}
|
||||
|
||||
const newLines = Array.isArray(data.lines) ? data.lines : [];
|
||||
@@ -170,6 +229,16 @@ export function LogsPage() {
|
||||
}
|
||||
} catch (err: unknown) {
|
||||
console.error('Failed to load logs:', err);
|
||||
if (isLoggingToFileDisabledError(err)) {
|
||||
if (!incremental) {
|
||||
latestCursorRef.current = undefined;
|
||||
requestLogHomeIpByIdRef.current = {};
|
||||
setFileLoggingRequired(true);
|
||||
setLogState({ buffer: [], visibleFrom: 0 });
|
||||
setError('');
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (!incremental) {
|
||||
setError(getErrorMessage(err) || t('logs.load_error'));
|
||||
}
|
||||
@@ -188,6 +257,18 @@ export function LogsPage() {
|
||||
useHeaderRefresh(() => loadLogs(false));
|
||||
|
||||
const clearLogs = async () => {
|
||||
if (isHomeRuntime) {
|
||||
showNotification(t('logs.home_clear_unavailable'), 'warning');
|
||||
return;
|
||||
}
|
||||
if (cpaNeedsFileLogging) {
|
||||
showNotification(t('logs.cpa_file_logging_required'), 'warning');
|
||||
return;
|
||||
}
|
||||
if (fileLoggingRequired) {
|
||||
showNotification(t('logs.file_logging_required'), 'warning');
|
||||
return;
|
||||
}
|
||||
showConfirmation({
|
||||
title: t('logs.clear_confirm_title', { defaultValue: 'Clear Logs' }),
|
||||
message: t('logs.clear_confirm'),
|
||||
@@ -197,7 +278,9 @@ export function LogsPage() {
|
||||
try {
|
||||
await logsApi.clearLogs();
|
||||
setLogState({ buffer: [], visibleFrom: 0 });
|
||||
latestTimestampRef.current = 0;
|
||||
latestCursorRef.current = undefined;
|
||||
requestLogHomeIpByIdRef.current = {};
|
||||
setFileLoggingRequired(false);
|
||||
showNotification(t('logs.clear_success'), 'success');
|
||||
} catch (err: unknown) {
|
||||
const message = getErrorMessage(err);
|
||||
@@ -221,6 +304,12 @@ export function LogsPage() {
|
||||
setLoadingErrors(false);
|
||||
return;
|
||||
}
|
||||
if (isHomeRuntime) {
|
||||
setLoadingErrors(false);
|
||||
setErrorLogs([]);
|
||||
setErrorLogsError('');
|
||||
return;
|
||||
}
|
||||
|
||||
setLoadingErrors(true);
|
||||
setErrorLogsError('');
|
||||
@@ -256,11 +345,28 @@ export function LogsPage() {
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionStatus === 'connected') {
|
||||
latestTimestampRef.current = 0;
|
||||
latestCursorRef.current = undefined;
|
||||
requestLogHomeIpByIdRef.current = {};
|
||||
setFileLoggingRequired(false);
|
||||
loadLogs(false);
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [connectionStatus]);
|
||||
}, [connectionStatus, loggingToFileEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionStatus !== 'connected' || serverRuntimeKind !== 'unknown') return;
|
||||
let cancelled = false;
|
||||
const detectRuntime = async () => {
|
||||
const runtimeKind = await versionApi.detectRuntimeKind();
|
||||
if (!cancelled && (runtimeKind === 'cpa' || runtimeKind === 'home')) {
|
||||
updateServerRuntimeKind(runtimeKind);
|
||||
}
|
||||
};
|
||||
void detectRuntime();
|
||||
return () => {
|
||||
cancelled = true;
|
||||
};
|
||||
}, [connectionStatus, serverRuntimeKind, updateServerRuntimeKind]);
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab !== 'errors') return;
|
||||
@@ -270,7 +376,7 @@ export function LogsPage() {
|
||||
}, [activeTab, connectionStatus, requestLogEnabled]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!autoRefresh || connectionStatus !== 'connected') {
|
||||
if (!autoRefresh || connectionStatus !== 'connected' || showFileLoggingRequired) {
|
||||
return;
|
||||
}
|
||||
const id = window.setInterval(() => {
|
||||
@@ -278,7 +384,7 @@ export function LogsPage() {
|
||||
}, 8000);
|
||||
return () => window.clearInterval(id);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [autoRefresh, connectionStatus]);
|
||||
}, [autoRefresh, connectionStatus, showFileLoggingRequired]);
|
||||
|
||||
const visibleLines = useMemo(
|
||||
() => logState.buffer.slice(logState.visibleFrom),
|
||||
@@ -423,7 +529,10 @@ export function LogsPage() {
|
||||
const downloadRequestLog = async (id: string) => {
|
||||
setRequestLogDownloading(true);
|
||||
try {
|
||||
const response = await logsApi.downloadRequestLogById(id);
|
||||
const response = await logsApi.downloadRequestLogById(
|
||||
id,
|
||||
requestLogHomeIpByIdRef.current[id]
|
||||
);
|
||||
downloadBlob({
|
||||
filename: `request-${id}.log`,
|
||||
blob: new Blob([response.data], { type: 'text/plain' })
|
||||
@@ -452,7 +561,12 @@ export function LogsPage() {
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<h1 className={styles.pageTitle}>{t('logs.title')}</h1>
|
||||
<div className={styles.pageHeader}>
|
||||
<h1 className={styles.pageTitle}>{t('logs.title')}</h1>
|
||||
<div className={styles.runtimeNotice}>
|
||||
{t(`logs.runtime_${serverRuntimeKind}`)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.tabBar}>
|
||||
<button
|
||||
@@ -474,6 +588,15 @@ export function LogsPage() {
|
||||
<div className={styles.content}>
|
||||
{activeTab === 'logs' && (
|
||||
<Card className={styles.logCard}>
|
||||
{showFileLoggingRequired && (
|
||||
<div className="status-badge warning">
|
||||
{t(
|
||||
cpaNeedsFileLogging
|
||||
? 'logs.cpa_file_logging_required'
|
||||
: 'logs.file_logging_required'
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
{error && <div className="error-box">{error}</div>}
|
||||
|
||||
<div className={styles.filters}>
|
||||
@@ -647,7 +770,7 @@ export function LogsPage() {
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={() => loadLogs(false)}
|
||||
disabled={disableControls || loading}
|
||||
disabled={refreshDisabled}
|
||||
className={styles.actionButton}
|
||||
>
|
||||
<span className={styles.buttonContent}>
|
||||
@@ -658,7 +781,7 @@ export function LogsPage() {
|
||||
<ToggleSwitch
|
||||
checked={autoRefresh}
|
||||
onChange={(value) => setAutoRefresh(value)}
|
||||
disabled={disableControls}
|
||||
disabled={autoRefreshDisabled}
|
||||
label={
|
||||
<span className={styles.switchLabel}>
|
||||
<IconTimer size={16} />
|
||||
@@ -682,7 +805,7 @@ export function LogsPage() {
|
||||
variant="danger"
|
||||
size="sm"
|
||||
onClick={clearLogs}
|
||||
disabled={disableControls}
|
||||
disabled={clearDisabled}
|
||||
className={styles.actionButton}
|
||||
>
|
||||
<span className={styles.buttonContent}>
|
||||
@@ -828,6 +951,19 @@ export function LogsPage() {
|
||||
title={t('logs.search_empty_title')}
|
||||
description={t('logs.search_empty_desc')}
|
||||
/>
|
||||
) : showFileLoggingRequired ? (
|
||||
<EmptyState
|
||||
title={t(
|
||||
cpaNeedsFileLogging
|
||||
? 'logs.cpa_file_logging_required_title'
|
||||
: 'logs.file_logging_required_title'
|
||||
)}
|
||||
description={t(
|
||||
cpaNeedsFileLogging
|
||||
? 'logs.cpa_file_logging_required_desc'
|
||||
: 'logs.file_logging_required_desc'
|
||||
)}
|
||||
/>
|
||||
) : (
|
||||
<EmptyState title={t('logs.empty_title')} description={t('logs.empty_desc')} />
|
||||
)}
|
||||
@@ -851,7 +987,11 @@ export function LogsPage() {
|
||||
<div className="stack">
|
||||
<div className="hint">{t('logs.error_logs_description')}</div>
|
||||
|
||||
{requestLogEnabled && (
|
||||
{isHomeRuntime && (
|
||||
<div className="status-badge warning">{t('logs.error_logs_home_unavailable')}</div>
|
||||
)}
|
||||
|
||||
{requestLogEnabled && !isHomeRuntime && (
|
||||
<div>
|
||||
<div className="status-badge warning">{t('logs.error_logs_request_log_enabled')}</div>
|
||||
</div>
|
||||
|
||||
@@ -10,6 +10,7 @@ const LOG_LATENCY_REGEX =
|
||||
const LOG_IPV4_REGEX = /\b(?:\d{1,3}\.){3}\d{1,3}\b/;
|
||||
const LOG_IPV6_REGEX = /\b(?:[a-f0-9]{0,4}:){2,7}[a-f0-9]{0,4}\b/i;
|
||||
const LOG_REQUEST_ID_REGEX = /^([a-f0-9]{8}|--------)$/i;
|
||||
const LOG_NAMED_REQUEST_ID_REGEX = /\brequest[_-]?id=([A-Za-z0-9][A-Za-z0-9._:-]{0,127})\b/i;
|
||||
const LOG_TIME_OF_DAY_REGEX = /^\d{1,2}:\d{2}:\d{2}(?:\.\d{1,3})?$/;
|
||||
const GIN_TIMESTAMP_SEGMENT_REGEX =
|
||||
/^\[GIN\]\s+(\d{4})\/(\d{2})\/(\d{2})\s*-\s*(\d{2}:\d{2}:\d{2}(?:\.\d{1,3})?)\s*$/;
|
||||
@@ -87,6 +88,14 @@ const inferLogLevel = (line: string): LogLevel | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
const extractNamedRequestId = (text: string): string | undefined => {
|
||||
const match = text.match(LOG_NAMED_REQUEST_ID_REGEX);
|
||||
if (!match) return undefined;
|
||||
const id = match[1];
|
||||
if (/^-+$/.test(id)) return undefined;
|
||||
return id;
|
||||
};
|
||||
|
||||
const extractHttpMethodAndPath = (text: string): { method?: HttpMethod; path?: string } => {
|
||||
const match = text.match(HTTP_METHOD_REGEX);
|
||||
if (!match) return {};
|
||||
@@ -163,16 +172,24 @@ export const parseLogLine = (raw: string): ParsedLogLine => {
|
||||
}
|
||||
}
|
||||
|
||||
// request id (8-char hex or dashes)
|
||||
const requestIdIndex = segments.findIndex((segment) => LOG_REQUEST_ID_REGEX.test(segment));
|
||||
// request id
|
||||
const requestIdIndex = segments.findIndex(
|
||||
(segment) => LOG_REQUEST_ID_REGEX.test(segment) || Boolean(extractNamedRequestId(segment))
|
||||
);
|
||||
if (requestIdIndex >= 0) {
|
||||
const match = segments[requestIdIndex].match(LOG_REQUEST_ID_REGEX);
|
||||
if (match) {
|
||||
const id = match[1];
|
||||
if (!/^-+$/.test(id)) {
|
||||
requestId = id;
|
||||
}
|
||||
const namedId = extractNamedRequestId(segments[requestIdIndex]);
|
||||
if (namedId) {
|
||||
requestId = namedId;
|
||||
consumed.add(requestIdIndex);
|
||||
} else {
|
||||
const match = segments[requestIdIndex].match(LOG_REQUEST_ID_REGEX);
|
||||
if (match) {
|
||||
const id = match[1];
|
||||
if (!/^-+$/.test(id)) {
|
||||
requestId = id;
|
||||
}
|
||||
consumed.add(requestIdIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -243,6 +260,10 @@ export const parseLogLine = (raw: string): ParsedLogLine => {
|
||||
const parsed = extractHttpMethodAndPath(remaining);
|
||||
method = parsed.method;
|
||||
path = parsed.path;
|
||||
|
||||
if (!requestId) {
|
||||
requestId = extractNamedRequestId(remaining);
|
||||
}
|
||||
}
|
||||
|
||||
if (!level) level = inferLogLevel(raw);
|
||||
@@ -272,4 +293,3 @@ export const parseLogLine = (raw: string): ParsedLogLine => {
|
||||
message,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
@@ -7,10 +7,15 @@ import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
|
||||
import type { ApiClientConfig, ApiError } from '@/types';
|
||||
import {
|
||||
BUILD_DATE_HEADER_KEYS,
|
||||
CPA_BUILD_DATE_HEADER_KEYS,
|
||||
CPA_VERSION_HEADER_KEYS,
|
||||
HOME_BUILD_DATE_HEADER_KEYS,
|
||||
HOME_VERSION_HEADER_KEYS,
|
||||
REQUEST_TIMEOUT_MS,
|
||||
VERSION_HEADER_KEYS
|
||||
} from '@/utils/constants';
|
||||
import { computeApiUrl } from '@/utils/connection';
|
||||
import type { ServerRuntimeKind } from '@/types';
|
||||
|
||||
class ApiClient {
|
||||
private instance: AxiosInstance;
|
||||
@@ -109,14 +114,21 @@ class ApiClient {
|
||||
this.instance.interceptors.response.use(
|
||||
(response) => {
|
||||
const headers = response.headers as Record<string, string | undefined>;
|
||||
const version = this.readHeader(headers, VERSION_HEADER_KEYS);
|
||||
const buildDate = this.readHeader(headers, BUILD_DATE_HEADER_KEYS);
|
||||
const homeVersion = this.readHeader(headers, HOME_VERSION_HEADER_KEYS);
|
||||
const homeBuildDate = this.readHeader(headers, HOME_BUILD_DATE_HEADER_KEYS);
|
||||
const cpaVersion = this.readHeader(headers, CPA_VERSION_HEADER_KEYS);
|
||||
const cpaBuildDate = this.readHeader(headers, CPA_BUILD_DATE_HEADER_KEYS);
|
||||
const version = homeVersion || cpaVersion || this.readHeader(headers, VERSION_HEADER_KEYS);
|
||||
const buildDate =
|
||||
homeBuildDate || cpaBuildDate || this.readHeader(headers, BUILD_DATE_HEADER_KEYS);
|
||||
const runtimeKind: ServerRuntimeKind | null =
|
||||
homeVersion || homeBuildDate ? 'home' : cpaVersion || cpaBuildDate ? 'cpa' : null;
|
||||
|
||||
// 触发版本更新事件(后续通过 store 处理)
|
||||
if (version || buildDate) {
|
||||
if (version || buildDate || runtimeKind) {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('server-version-update', {
|
||||
detail: { version: version || null, buildDate: buildDate || null }
|
||||
detail: { version: version || null, buildDate: buildDate || null, runtimeKind }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
+131
-6
@@ -5,14 +5,48 @@
|
||||
import { apiClient } from './client';
|
||||
import { LOGS_TIMEOUT_MS } from '@/utils/constants';
|
||||
|
||||
export type LogCursor = number | string;
|
||||
export type LogBackendKind = 'unknown' | 'file' | 'home-db';
|
||||
|
||||
export interface LogsQuery {
|
||||
after?: number;
|
||||
after?: LogCursor;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface CPALogsResponse {
|
||||
lines: string[];
|
||||
'line-count': number;
|
||||
'latest-timestamp': number;
|
||||
}
|
||||
|
||||
export interface HomeLogRecord {
|
||||
id?: number;
|
||||
timestamp?: string | number;
|
||||
client_ip?: string;
|
||||
request_id?: string;
|
||||
home_ip?: string;
|
||||
level?: string;
|
||||
line?: string;
|
||||
created_at?: string | number;
|
||||
}
|
||||
|
||||
export interface HomeLogsResponse {
|
||||
logs?: HomeLogRecord[];
|
||||
total?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface LogsResponse {
|
||||
lines: string[];
|
||||
'line-count': number;
|
||||
'latest-timestamp': number;
|
||||
lineCount: number;
|
||||
latestCursor?: LogCursor;
|
||||
logBackendKind: LogBackendKind;
|
||||
requestLogHomeIpById?: Record<string, string>;
|
||||
total?: number;
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
}
|
||||
|
||||
export interface ErrorLogFile {
|
||||
@@ -25,9 +59,99 @@ export interface ErrorLogsResponse {
|
||||
files?: ErrorLogFile[];
|
||||
}
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object';
|
||||
|
||||
const stringValue = (value: unknown): string => (typeof value === 'string' ? value.trim() : '');
|
||||
|
||||
const unixSecondsFromValue = (value: unknown): number => {
|
||||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||||
const text = stringValue(value);
|
||||
if (!text) return 0;
|
||||
const asNumber = Number(text);
|
||||
if (Number.isFinite(asNumber)) return asNumber;
|
||||
const asDate = Date.parse(text);
|
||||
return Number.isFinite(asDate) ? Math.floor(asDate / 1000) : 0;
|
||||
};
|
||||
|
||||
const homeCursorFromRecord = (record: HomeLogRecord): string => {
|
||||
const timestamp = stringValue(record.timestamp);
|
||||
if (timestamp) return timestamp;
|
||||
const createdAt = stringValue(record.created_at);
|
||||
return createdAt;
|
||||
};
|
||||
|
||||
const normalizeCPALogs = (data: Record<string, unknown>): LogsResponse => {
|
||||
const lines = Array.isArray(data.lines)
|
||||
? data.lines.filter((line): line is string => typeof line === 'string')
|
||||
: [];
|
||||
const latestTimestamp = unixSecondsFromValue(data['latest-timestamp']);
|
||||
const lineCount = Number(data['line-count']);
|
||||
|
||||
return {
|
||||
lines,
|
||||
lineCount: Number.isFinite(lineCount) ? lineCount : lines.length,
|
||||
latestCursor: latestTimestamp > 0 ? latestTimestamp : undefined,
|
||||
logBackendKind: 'file'
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeHomeLogs = (data: Record<string, unknown>): LogsResponse => {
|
||||
const rawLogs = Array.isArray(data.logs)
|
||||
? data.logs.filter((entry): entry is HomeLogRecord => isRecord(entry))
|
||||
: [];
|
||||
const orderedLogs = [...rawLogs].reverse();
|
||||
const lines = orderedLogs
|
||||
.map((record) => record.line)
|
||||
.filter((line): line is string => typeof line === 'string' && line.length > 0);
|
||||
const requestLogHomeIpById = orderedLogs.reduce<Record<string, string>>((acc, record) => {
|
||||
const requestId = stringValue(record.request_id);
|
||||
const homeIp = stringValue(record.home_ip);
|
||||
if (requestId && homeIp) {
|
||||
acc[requestId] = homeIp;
|
||||
}
|
||||
return acc;
|
||||
}, {});
|
||||
const latestCursor = rawLogs.reduce<string | undefined>((latest, record) => {
|
||||
const cursor = homeCursorFromRecord(record);
|
||||
if (!cursor) return latest;
|
||||
if (!latest) return cursor;
|
||||
const latestTime = Date.parse(latest);
|
||||
const cursorTime = Date.parse(cursor);
|
||||
if (!Number.isFinite(latestTime) || !Number.isFinite(cursorTime)) return latest;
|
||||
return cursorTime > latestTime ? cursor : latest;
|
||||
}, undefined);
|
||||
|
||||
const total = Number(data.total);
|
||||
const limit = Number(data.limit);
|
||||
const offset = Number(data.offset);
|
||||
|
||||
return {
|
||||
lines,
|
||||
lineCount: Number.isFinite(total) ? total : lines.length,
|
||||
latestCursor,
|
||||
logBackendKind: 'home-db',
|
||||
requestLogHomeIpById,
|
||||
total: Number.isFinite(total) ? total : undefined,
|
||||
limit: Number.isFinite(limit) ? limit : undefined,
|
||||
offset: Number.isFinite(offset) ? offset : undefined
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeLogsResponse = (data: unknown): LogsResponse => {
|
||||
if (!isRecord(data)) {
|
||||
return { lines: [], lineCount: 0, logBackendKind: 'unknown' };
|
||||
}
|
||||
if (Array.isArray(data.logs)) return normalizeHomeLogs(data);
|
||||
if (Array.isArray(data.lines)) return normalizeCPALogs(data);
|
||||
return { lines: [], lineCount: 0, logBackendKind: 'unknown' };
|
||||
};
|
||||
|
||||
export const logsApi = {
|
||||
fetchLogs: (params: LogsQuery = {}): Promise<LogsResponse> =>
|
||||
apiClient.get('/logs', { params, timeout: LOGS_TIMEOUT_MS }),
|
||||
async fetchLogs(params: LogsQuery = {}): Promise<LogsResponse> {
|
||||
const data = await apiClient.get('/logs', { params, timeout: LOGS_TIMEOUT_MS });
|
||||
return normalizeLogsResponse(data);
|
||||
},
|
||||
|
||||
clearLogs: () => apiClient.delete('/logs'),
|
||||
|
||||
@@ -40,8 +164,9 @@ export const logsApi = {
|
||||
timeout: LOGS_TIMEOUT_MS
|
||||
}),
|
||||
|
||||
downloadRequestLogById: (id: string) =>
|
||||
downloadRequestLogById: (id: string, homeIp?: string) =>
|
||||
apiClient.getRaw(`/request-log-by-id/${encodeURIComponent(id)}`, {
|
||||
params: homeIp ? { home_ip: homeIp } : undefined,
|
||||
responseType: 'blob',
|
||||
timeout: LOGS_TIMEOUT_MS
|
||||
}),
|
||||
|
||||
@@ -3,7 +3,24 @@
|
||||
*/
|
||||
|
||||
import { apiClient } from './client';
|
||||
import type { ServerRuntimeKind } from '@/types';
|
||||
|
||||
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
||||
value !== null && typeof value === 'object';
|
||||
|
||||
export const versionApi = {
|
||||
checkLatest: () => apiClient.get<Record<string, unknown>>('/latest-version')
|
||||
checkLatest: () => apiClient.get<Record<string, unknown>>('/latest-version'),
|
||||
|
||||
async detectRuntimeKind(): Promise<ServerRuntimeKind> {
|
||||
try {
|
||||
const data = await apiClient.get('/nodes');
|
||||
return isRecord(data) && Array.isArray(data.nodes) ? 'home' : 'unknown';
|
||||
} catch (error: unknown) {
|
||||
const status = isRecord(error) ? error.status : undefined;
|
||||
if (status === 404 || status === 405) {
|
||||
return 'cpa';
|
||||
}
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -5,10 +5,11 @@
|
||||
|
||||
import { create } from 'zustand';
|
||||
import { persist, createJSONStorage } from 'zustand/middleware';
|
||||
import type { AuthState, LoginCredentials, ConnectionStatus } from '@/types';
|
||||
import type { AuthState, LoginCredentials, ConnectionStatus, ServerRuntimeKind } from '@/types';
|
||||
import { STORAGE_KEY_AUTH } from '@/utils/constants';
|
||||
import { obfuscatedStorage } from '@/services/storage/secureStorage';
|
||||
import { apiClient } from '@/services/api/client';
|
||||
import { versionApi } from '@/services/api/version';
|
||||
import { useConfigStore } from './useConfigStore';
|
||||
import { useModelsStore } from './useModelsStore';
|
||||
import { detectApiBaseFromLocation, normalizeApiBase } from '@/utils/connection';
|
||||
@@ -22,12 +23,26 @@ interface AuthStoreState extends AuthState {
|
||||
logout: () => void;
|
||||
checkAuth: () => Promise<boolean>;
|
||||
restoreSession: () => Promise<boolean>;
|
||||
updateServerVersion: (version: string | null, buildDate?: string | null) => void;
|
||||
updateServerVersion: (
|
||||
version: string | null,
|
||||
buildDate?: string | null,
|
||||
runtimeKind?: ServerRuntimeKind | null
|
||||
) => void;
|
||||
updateServerRuntimeKind: (runtimeKind: ServerRuntimeKind) => void;
|
||||
updateConnectionStatus: (status: ConnectionStatus, error?: string | null) => void;
|
||||
}
|
||||
|
||||
let restoreSessionPromise: Promise<boolean> | null = null;
|
||||
|
||||
const detectRuntimeKind = async (): Promise<ServerRuntimeKind> => {
|
||||
try {
|
||||
return await versionApi.detectRuntimeKind();
|
||||
} catch (error) {
|
||||
console.warn('Runtime kind detection failed:', error);
|
||||
return 'unknown';
|
||||
}
|
||||
};
|
||||
|
||||
export const useAuthStore = create<AuthStoreState>()(
|
||||
persist(
|
||||
(set, get) => ({
|
||||
@@ -38,6 +53,7 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
rememberPassword: false,
|
||||
serverVersion: null,
|
||||
serverBuildDate: null,
|
||||
serverRuntimeKind: 'unknown',
|
||||
connectionStatus: 'disconnected',
|
||||
connectionError: null,
|
||||
|
||||
@@ -93,7 +109,12 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
const rememberPassword = credentials.rememberPassword ?? get().rememberPassword ?? false;
|
||||
|
||||
try {
|
||||
set({ connectionStatus: 'connecting' });
|
||||
set({
|
||||
connectionStatus: 'connecting',
|
||||
serverVersion: null,
|
||||
serverBuildDate: null,
|
||||
serverRuntimeKind: 'unknown'
|
||||
});
|
||||
useModelsStore.getState().clearCache();
|
||||
|
||||
// 配置 API 客户端
|
||||
@@ -104,6 +125,7 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
|
||||
// 测试连接 - 获取配置
|
||||
await useConfigStore.getState().fetchConfig(undefined, true);
|
||||
const runtimeKind = await detectRuntimeKind();
|
||||
|
||||
// 登录成功
|
||||
set({
|
||||
@@ -112,7 +134,8 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
managementKey,
|
||||
rememberPassword,
|
||||
connectionStatus: 'connected',
|
||||
connectionError: null
|
||||
connectionError: null,
|
||||
...(runtimeKind !== 'unknown' ? { serverRuntimeKind: runtimeKind } : {})
|
||||
});
|
||||
if (rememberPassword) {
|
||||
localStorage.setItem('isLoggedIn', 'true');
|
||||
@@ -145,6 +168,7 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
managementKey: '',
|
||||
serverVersion: null,
|
||||
serverBuildDate: null,
|
||||
serverRuntimeKind: 'unknown',
|
||||
connectionStatus: 'disconnected',
|
||||
connectionError: null
|
||||
});
|
||||
@@ -165,10 +189,12 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
|
||||
// 验证连接
|
||||
await useConfigStore.getState().fetchConfig();
|
||||
const runtimeKind = await detectRuntimeKind();
|
||||
|
||||
set({
|
||||
isAuthenticated: true,
|
||||
connectionStatus: 'connected'
|
||||
connectionStatus: 'connected',
|
||||
...(runtimeKind !== 'unknown' ? { serverRuntimeKind: runtimeKind } : {})
|
||||
});
|
||||
|
||||
return true;
|
||||
@@ -182,8 +208,16 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
},
|
||||
|
||||
// 更新服务器版本
|
||||
updateServerVersion: (version, buildDate) => {
|
||||
set({ serverVersion: version || null, serverBuildDate: buildDate || null });
|
||||
updateServerVersion: (version, buildDate, runtimeKind) => {
|
||||
set((state) => ({
|
||||
serverVersion: version || null,
|
||||
serverBuildDate: buildDate || null,
|
||||
serverRuntimeKind: runtimeKind || state.serverRuntimeKind
|
||||
}));
|
||||
},
|
||||
|
||||
updateServerRuntimeKind: (runtimeKind) => {
|
||||
set({ serverRuntimeKind: runtimeKind });
|
||||
},
|
||||
|
||||
// 更新连接状态
|
||||
@@ -213,7 +247,8 @@ export const useAuthStore = create<AuthStoreState>()(
|
||||
...(state.rememberPassword ? { managementKey: state.managementKey } : {}),
|
||||
rememberPassword: state.rememberPassword,
|
||||
serverVersion: state.serverVersion,
|
||||
serverBuildDate: state.serverBuildDate
|
||||
serverBuildDate: state.serverBuildDate,
|
||||
serverRuntimeKind: state.serverRuntimeKind
|
||||
})
|
||||
}
|
||||
)
|
||||
@@ -229,7 +264,13 @@ if (typeof window !== 'undefined') {
|
||||
'server-version-update',
|
||||
((e: CustomEvent) => {
|
||||
const detail = e.detail || {};
|
||||
useAuthStore.getState().updateServerVersion(detail.version || null, detail.buildDate || null);
|
||||
const runtimeKind =
|
||||
detail.runtimeKind === 'cpa' || detail.runtimeKind === 'home'
|
||||
? detail.runtimeKind
|
||||
: null;
|
||||
useAuthStore
|
||||
.getState()
|
||||
.updateServerVersion(detail.version || null, detail.buildDate || null, runtimeKind);
|
||||
}) as EventListener
|
||||
);
|
||||
}
|
||||
|
||||
@@ -18,10 +18,12 @@ export interface AuthState {
|
||||
rememberPassword: boolean;
|
||||
serverVersion: string | null;
|
||||
serverBuildDate: string | null;
|
||||
serverRuntimeKind: ServerRuntimeKind;
|
||||
}
|
||||
|
||||
// 连接状态
|
||||
export type ConnectionStatus = 'connected' | 'disconnected' | 'connecting' | 'error';
|
||||
export type ServerRuntimeKind = 'unknown' | 'cpa' | 'home';
|
||||
|
||||
export interface ConnectionInfo {
|
||||
status: ConnectionStatus;
|
||||
|
||||
+14
-2
@@ -16,8 +16,20 @@ export const CACHE_EXPIRY_MS = 30 * 1000; // 与基线保持一致,减少管
|
||||
export const DEFAULT_API_PORT = 8317;
|
||||
export const MANAGEMENT_API_PREFIX = '/v0/management';
|
||||
export const REQUEST_TIMEOUT_MS = 30 * 1000;
|
||||
export const VERSION_HEADER_KEYS = ['x-cpa-version', 'x-server-version'];
|
||||
export const BUILD_DATE_HEADER_KEYS = ['x-cpa-build-date', 'x-server-build-date'];
|
||||
export const CPA_VERSION_HEADER_KEYS = ['x-cpa-version'];
|
||||
export const CPA_BUILD_DATE_HEADER_KEYS = ['x-cpa-build-date'];
|
||||
export const HOME_VERSION_HEADER_KEYS = ['x-cpa-home-version'];
|
||||
export const HOME_BUILD_DATE_HEADER_KEYS = ['x-cpa-home-build-date'];
|
||||
export const VERSION_HEADER_KEYS = [
|
||||
...HOME_VERSION_HEADER_KEYS,
|
||||
...CPA_VERSION_HEADER_KEYS,
|
||||
'x-server-version'
|
||||
];
|
||||
export const BUILD_DATE_HEADER_KEYS = [
|
||||
...HOME_BUILD_DATE_HEADER_KEYS,
|
||||
...CPA_BUILD_DATE_HEADER_KEYS,
|
||||
'x-server-build-date'
|
||||
];
|
||||
|
||||
// 日志相关
|
||||
export const LOGS_TIMEOUT_MS = 60 * 1000;
|
||||
|
||||
Reference in New Issue
Block a user