🎨 优化网页控制台实时日志模块的显示效果

This commit is contained in:
KimigaiiWuyi 2025-06-13 03:34:18 +08:00
parent f55dc2d8df
commit b9232bf1d7
7 changed files with 1347 additions and 1121 deletions

View File

@ -1,5 +1,6 @@
import re import re
import sys import sys
import json
import asyncio import asyncio
import logging import logging
import datetime import datetime
@ -131,16 +132,16 @@ def std_format_event(record):
try: try:
data = format_event(record) data = format_event(record)
_data = ( _data = (
data.replace('<g>', '\033[37m') data.replace("<g>", "<span class=\"log-keyword-success\">")
.replace('</g>', '\033[0m') .replace("</g>", "</span>")
.replace('<c><u>', '\033[34m') .replace("<c><u>", "<span class=\"log-keyword-id\">")
.replace('</u></c>', '\033[0m') .replace("</u></c>", "</span>")
.replace('<m><b>', '\033[35m') .replace("<m><b>", "<span> class=\"log-keyword-blue\">")
.replace('</b></m>', '\033[0m') .replace("</b></m>", "</span>")
.replace('<c><b>', '\033[32m') .replace("<c><b>", "<span log-keyword-error>")
.replace('</b></c>', '\033[0m') .replace("</b></c>", "</span>")
.replace('<lvl>', '') .replace("<lvl>", "")
.replace('</lvl>', '') .replace("</lvl>", "")
) )
log = _data.format_map(record) log = _data.format_map(record)
log_history.append(log[:-5]) log_history.append(log[:-5])
@ -186,7 +187,13 @@ async def read_log():
while True: while True:
if index <= len(log_history) - 1: if index <= len(log_history) - 1:
if log_history[index]: if log_history[index]:
yield log_history[index] log_data = {
"level": "INFO",
"message": log_history[index],
"message_type": "html",
"timestamp": datetime.datetime.now().isoformat(),
}
yield f"data: {json.dumps(log_data)}\n\n"
index += 1 index += 1
else: else:
await asyncio.sleep(1) await asyncio.sleep(1)

View File

@ -315,7 +315,7 @@ def install_dependencies(dependencies: Dict, need_update: bool = False):
installed_dependencies, dependencies installed_dependencies, dependencies
) )
if not to_update: if not to_update:
logger.debug('[安装/更新依赖] 无需更新依赖!') logger.debug('🚀 [安装/更新依赖] 无需更新依赖!')
return return
logger.debug(f'[安装/更新依赖] 需更新依赖列表如下:\n{to_update}') logger.debug(f'[安装/更新依赖] 需更新依赖列表如下:\n{to_update}')

View File

@ -510,8 +510,9 @@ async def get_image(image_id: str, background_tasks: BackgroundTasks):
@app.get("/corelogs") @app.get("/corelogs")
async def core_log(): @site.auth.requires('root')
return StreamingResponse(read_log(), media_type='text/plain') async def core_log(request: Request):
return StreamingResponse(read_log(), media_type='text/event-stream')
@app.post('/genshinuid/api/loadData/{bot_id}/{bot_self_id}') @app.post('/genshinuid/api/loadData/{bot_id}/{bot_self_id}')

View File

@ -0,0 +1,216 @@
HIGHLIGHT = 'https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0'
def render_html():
return {
"type": "page",
"title": "",
"css": f"{HIGHLIGHT}/styles/atom-one-dark.min.css",
"js": [
f"{HIGHLIGHT}/highlight.min.js",
f"{HIGHLIGHT}/languages/json.min.js",
],
"body": [
{
"type": "custom",
"id": "log_viewer",
"html": HTML,
"onMount": ON_MOUNT_SSE,
"onUnmount": ON_UNMOUNT_SSE,
}
],
}
HTML = """
<style>
:root {
--theme-color: #ce5050;
--background-color: #1e1e1e;
--text-color: #d4d4d4;
--log-bg-color: #252526;
--border-color: #444;
}
#log-container-inner {
height: 75vh; /* 可以适当增加高度 */
background-color: var(--log-bg-color);
border: 1px solid var(--border-color);
border-radius: 8px;
padding: 8px 12px; /* 大幅减小内边距 */
overflow-y: auto;
font-family: 'Consolas', 'Monaco', 'Menlo', monospace;
/* 减小字体和行高以容纳更多行 */
font-size: 13px;
line-height: 1.4;
}
/* --- 核心改动日志条目的新样式 --- */
.log-entry {
padding: 3px 0; /* 减小每个条目的垂直内边距 */
margin: 0;
border-bottom: 1px solid var(--border-color);
white-space: normal; /* 允许自动换行 */
}
.log-entry:last-child {
border-bottom: none;
}
/* 将元数据时间级别变为行内元素 */
.log-time {
margin-right: 12px;
color: #888;
display: inline; /* 变为行内元素 */
}
.log-level {
font-weight: bold;
padding: 1px 5px; /* 减小内边距 */
border-radius: 3px;
color: #fff;
margin-right: 10px;
display: inline-block; /* 允许设置padding */
font-size: 12px; /* 可以比正文小一点 */
}
.log-level.INFO { background-color: #2196F3; }
.log-level.WARN { background-color: #FFC107; color: #333; }
.log-level.ERROR { background-color: #F44336; }
/* 日志内容样式 */
.log-content {
display: inline; /* 核心让内容跟在级别后面 */
word-break: break-all; /* 强制长单词换行 */
}
/* 对于普通文本的code标签 */
.log-content > code {
white-space: pre-wrap; /* 允许普通文本内容自动换行 */
}
/* 对于JSON的pre标签它会自然成为块级元素单独占行 */
.log-content > pre {
margin: 4px 0 0 0;
padding: 5px 8px;
background-color: rgba(0,0,0,0.2);
border-radius: 4px;
}
.log-content > pre > code.hljs {
padding: 0;
background: transparent;
}
/* 关键词染色样式保持不变 */
.log-keyword-error { color: #ff8a80; font-weight: bold; }
.log-keyword-success { color: #b9f6ca; font-weight: bold; }
.log-keyword-blue { color: #407dff; font-weight: bold; }
.log-keyword-id { color: #82aaff; background-color: #333a4f; padding: 1px 4px; border-radius: 3px; }
/* --- 页面头部样式可以稍微紧凑一点 --- */
.log-header { display: flex; justify-content: space-between; align-items: center; border-bottom: 2px solid var(--theme-color); padding-bottom: 8px; margin-bottom: 12px; }
.log-header h1 { color: var(--theme-color); margin: 0; font-size: 20px; font-family: 'Microsoft YaHei', sans-serif; }
.log-status { font-size: 14px; font-family: Arial, sans-serif;}
.log-status-indicator { width: 10px; height: 10px; margin-right: 6px; }
</style>
<div class="log-header">
<h1>实时日志</h1>
<div class="log-status">
<span id="status-indicator-inner" class="log-status-indicator disconnected"></span>
<span id="status-text-inner">未连接</span>
</div>
</div>
<div id="log-container-inner"></div>
""" # noqa: E501
ON_UNMOUNT_SSE = """
// 关闭 EventSource 连接
if (window.logEventSource) {
window.logEventSource.close();
}
// 清理模拟服务器的定时器
if (window.mockServerInterval) {
clearInterval(window.mockServerInterval);
}
"""
ON_MOUNT_SSE = """
const logContainer = dom.querySelector('#log-container-inner');
const statusIndicator = dom.querySelector('#status-indicator-inner');
const statusText = dom.querySelector('#status-text-inner');
const SSE_URL = '/corelogs';
window.logEventSource = null;
function appendLog(data) {
const { timestamp, level, message, message_type } = data; // 从data中解构出新字段 message_type
const logEntry = document.createElement('div');
logEntry.className = 'log-entry';
const time = new Date(timestamp).toLocaleTimeString();
let messageHtml;
let isJson = false;
try {
JSON.parse(message);
isJson = true;
} catch (e) {
// isJson 保持 false
}
// ======================= 核心修改点在这里 =======================
if (isJson) {
// 1. 如果是JSON优先高亮显示 (逻辑不变)
const jsonObj = JSON.parse(message);
const formattedJson = JSON.stringify(jsonObj, null, 2);
messageHtml = `<pre><code class="language-json hljs">${hljs.highlight(formattedJson, {language: 'json'}).value}</code></pre>`;
} else if (message_type === 'html') {
// 2. 如果后端标记为HTML我们信任它直接使用 message
// 不再进行HTML转义这样<span>标签就能生效
messageHtml = `<code>${message}</code>`;
} else {
// 3. 否则就是未知来源的纯文本为了安全必须进行转义
const safeMessage = message.replace(/</g, "&lt;").replace(/>/g, "&gt;");
messageHtml = `<code>${safeMessage}</code>`;
}
// ======================= 修改结束 =======================
logEntry.innerHTML = `
<span class="log-time">${time}</span>
<span class="log-level ${level}">${level}</span>
<span class="log-content">${messageHtml}</span>
`;
logContainer.appendChild(logEntry);
const isScrolledToBottom = logContainer.scrollHeight - logContainer.clientHeight <= logContainer.scrollTop + 5;
if (isScrolledToBottom) {
logContainer.scrollTop = logContainer.scrollHeight;
}
}
function connectSSE() {
// ... 此函数内容保持不变
console.log(`尝试连接到 SSE 端点: ${SSE_URL}`);
const evtSource = new EventSource(SSE_URL);
window.logEventSource = evtSource;
evtSource.onopen = () => {
console.log('SSE 连接已建立。');
statusIndicator.className = 'log-status-indicator connected';
statusText.textContent = '已连接';
};
evtSource.onmessage = (event) => {
try {
const logData = JSON.parse(event.data);
appendLog(logData);
} catch (e) {
console.error("解析SSE数据失败:", e);
appendLog({ level: 'INFO', message: event.data, timestamp: new Date().toISOString() });
}
};
evtSource.onerror = (err) => {
console.error("EventSource 发生错误: ", err);
statusIndicator.className = 'log-status-indicator disconnected';
statusText.textContent = '连接中断,自动重连中...';
};
}
connectSSE();
""" # noqa:E501

View File

@ -43,13 +43,15 @@ from fastapi_amis_admin.amis.components import (
ButtonToolbar, ButtonToolbar,
) )
from gsuid_core.webconsole.log import render_html
from gsuid_core.logger import logger, handle_exceptions from gsuid_core.logger import logger, handle_exceptions
from gsuid_core.utils.cookie_manager.add_ck import _deal_ck from gsuid_core.utils.cookie_manager.add_ck import _deal_ck
from gsuid_core.version import __version__ as gscore_version from gsuid_core.version import __version__ as gscore_version
from gsuid_core.webconsole.html import gsuid_webconsole_help from gsuid_core.webconsole.html import gsuid_webconsole_help
from gsuid_core.utils.database.base_models import finally_url from gsuid_core.utils.database.base_models import finally_url
from gsuid_core.webconsole.create_sv_panel import get_sv_page from gsuid_core.webconsole.create_sv_panel import get_sv_page
from gsuid_core.webconsole.create_log_panel import create_log_page
# from gsuid_core.webconsole.create_log_panel import create_log_page
from gsuid_core.webconsole.create_task_panel import get_tasks_panel from gsuid_core.webconsole.create_task_panel import get_tasks_panel
from gsuid_core.webconsole.create_config_panel import get_config_page from gsuid_core.webconsole.create_config_panel import get_config_page
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
@ -493,7 +495,7 @@ class LogsPage(GsAdminPage):
@handle_exceptions @handle_exceptions
async def get_page(self, request: Request) -> Page: async def get_page(self, request: Request) -> Page:
return Page.parse_obj(create_log_page()) return Page.parse_obj(render_html())
class HistoryLogsPage(GsAdminPage): class HistoryLogsPage(GsAdminPage):

2208
uv.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,2 +1,2 @@
[[index]] [[index]]
url = "https://mirror.nju.edu.cn/pypi/web/simple" url = "https://mirrors.cloud.tencent.com/pypi/simple/"