mirror of
https://github.com/Genshin-bots/gsuid_core.git
synced 2025-06-18 21:35:08 +08:00
🎨 优化网页控制台
中实时日志
模块的显示效果
This commit is contained in:
parent
f55dc2d8df
commit
b9232bf1d7
@ -1,5 +1,6 @@
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import asyncio
|
||||
import logging
|
||||
import datetime
|
||||
@ -131,16 +132,16 @@ def std_format_event(record):
|
||||
try:
|
||||
data = format_event(record)
|
||||
_data = (
|
||||
data.replace('<g>', '\033[37m')
|
||||
.replace('</g>', '\033[0m')
|
||||
.replace('<c><u>', '\033[34m')
|
||||
.replace('</u></c>', '\033[0m')
|
||||
.replace('<m><b>', '\033[35m')
|
||||
.replace('</b></m>', '\033[0m')
|
||||
.replace('<c><b>', '\033[32m')
|
||||
.replace('</b></c>', '\033[0m')
|
||||
.replace('<lvl>', '')
|
||||
.replace('</lvl>', '')
|
||||
data.replace("<g>", "<span class=\"log-keyword-success\">")
|
||||
.replace("</g>", "</span>")
|
||||
.replace("<c><u>", "<span class=\"log-keyword-id\">")
|
||||
.replace("</u></c>", "</span>")
|
||||
.replace("<m><b>", "<span> class=\"log-keyword-blue\">")
|
||||
.replace("</b></m>", "</span>")
|
||||
.replace("<c><b>", "<span log-keyword-error>")
|
||||
.replace("</b></c>", "</span>")
|
||||
.replace("<lvl>", "")
|
||||
.replace("</lvl>", "")
|
||||
)
|
||||
log = _data.format_map(record)
|
||||
log_history.append(log[:-5])
|
||||
@ -186,7 +187,13 @@ async def read_log():
|
||||
while True:
|
||||
if index <= len(log_history) - 1:
|
||||
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
|
||||
else:
|
||||
await asyncio.sleep(1)
|
||||
|
@ -315,7 +315,7 @@ def install_dependencies(dependencies: Dict, need_update: bool = False):
|
||||
installed_dependencies, dependencies
|
||||
)
|
||||
if not to_update:
|
||||
logger.debug('[安装/更新依赖] 无需更新依赖!')
|
||||
logger.debug('🚀 [安装/更新依赖] 无需更新依赖!')
|
||||
return
|
||||
|
||||
logger.debug(f'[安装/更新依赖] 需更新依赖列表如下:\n{to_update}')
|
||||
|
@ -510,8 +510,9 @@ async def get_image(image_id: str, background_tasks: BackgroundTasks):
|
||||
|
||||
|
||||
@app.get("/corelogs")
|
||||
async def core_log():
|
||||
return StreamingResponse(read_log(), media_type='text/plain')
|
||||
@site.auth.requires('root')
|
||||
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}')
|
||||
|
216
gsuid_core/webconsole/log.py
Normal file
216
gsuid_core/webconsole/log.py
Normal 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, "<").replace(/>/g, ">");
|
||||
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
|
@ -43,13 +43,15 @@ from fastapi_amis_admin.amis.components import (
|
||||
ButtonToolbar,
|
||||
)
|
||||
|
||||
from gsuid_core.webconsole.log import render_html
|
||||
from gsuid_core.logger import logger, handle_exceptions
|
||||
from gsuid_core.utils.cookie_manager.add_ck import _deal_ck
|
||||
from gsuid_core.version import __version__ as gscore_version
|
||||
from gsuid_core.webconsole.html import gsuid_webconsole_help
|
||||
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_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_config_panel import get_config_page
|
||||
from gsuid_core.utils.plugins_config.gs_config import core_plugins_config
|
||||
@ -493,7 +495,7 @@ class LogsPage(GsAdminPage):
|
||||
|
||||
@handle_exceptions
|
||||
async def get_page(self, request: Request) -> Page:
|
||||
return Page.parse_obj(create_log_page())
|
||||
return Page.parse_obj(render_html())
|
||||
|
||||
|
||||
class HistoryLogsPage(GsAdminPage):
|
||||
|
Loading…
x
Reference in New Issue
Block a user