diff --git a/src/api/__init__.py b/src/api/__init__.py index 32ce6b2..8e90cb1 100644 --- a/src/api/__init__.py +++ b/src/api/__init__.py @@ -5,6 +5,7 @@ from .datasets import datasets_bp from .model_manage import model_manage_bp from .model_chat import model_chat_bp from .dimension import dimension_bp +from .logs import logs_bp # 注册所有蓝图 def register_blueprints(app): @@ -13,3 +14,4 @@ def register_blueprints(app): app.register_blueprint(model_manage_bp) app.register_blueprint(model_chat_bp) app.register_blueprint(dimension_bp) + app.register_blueprint(logs_bp) diff --git a/src/api/logs.py b/src/api/logs.py new file mode 100644 index 0000000..9315991 --- /dev/null +++ b/src/api/logs.py @@ -0,0 +1,171 @@ +""" +日志管理 API 路由 +""" +import os +import sys +import logging +import yaml +from datetime import datetime +from logging.handlers import TimedRotatingFileHandler, RotatingFileHandler +from flask import Blueprint, request, jsonify + +# 获取项目根目录 +PROJECT_ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) +sys.path.insert(0, PROJECT_ROOT) + +# 加载配置 +CONFIG_PATH = os.path.join(PROJECT_ROOT, 'config.yaml') +with open(CONFIG_PATH, 'r', encoding='utf-8') as f: + CONFIG = yaml.safe_load(f) + +# 日志目录 +LOG_BASE_DIR = os.path.join(PROJECT_ROOT, 'logs') + +# 创建蓝图 +logs_bp = Blueprint('logs', __name__, url_prefix='/api') + + +def setup_logs_logger(): + """配置日志系统,按日期分目录存储""" + today = datetime.now().strftime('%Y-%m-%d') + log_dir = os.path.join(LOG_BASE_DIR, today) + os.makedirs(log_dir, exist_ok=True) + + logger = logging.getLogger('logs_api') + logger.setLevel(logging.DEBUG) + logger.handlers.clear() + + # 全部日志处理器 + all_log_path = os.path.join(log_dir, 'all.log') + all_handler = TimedRotatingFileHandler(all_log_path, when='midnight', interval=1, backupCount=30, encoding='utf-8') + all_handler.setLevel(logging.DEBUG) + all_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + )) + + # 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%H:%M:%S' + )) + + logger.addHandler(all_handler) + logger.addHandler(console_handler) + + return logger + + +logs_logger = setup_logs_logger() + + +@logs_bp.route('/web-log', methods=['POST']) +def receive_web_log(): + """接收前端页面发送的日志""" + data = request.json + level = data.get('level', 'info') + message = data.get('message', '') + page = data.get('page', 'unknown') + timestamp = data.get('timestamp', datetime.now().isoformat()) + + log_message = f'[WEB-{page}] {message}' + + if level == 'error': + logs_logger.error(log_message) + elif level == 'warning': + logs_logger.warning(log_message) + elif level == 'debug': + logs_logger.debug(log_message) + else: + logs_logger.info(log_message) + + return jsonify({'code': 0, 'message': '日志接收成功'}) + + +def format_file_size(size_bytes): + """格式化文件大小""" + if size_bytes < 1024: + return f'{size_bytes} B' + elif size_bytes < 1024 * 1024: + return f'{size_bytes / 1024:.1f} KB' + else: + return f'{size_bytes / (1024 * 1024):.1f} MB' + + +@logs_bp.route('/log-files', methods=['GET']) +def get_log_files(): + """获取指定日期的日志文件列表""" + date = request.args.get('date') + if not date: + return jsonify({'code': 1, 'message': '缺少日期参数'}) + + # 验证日期格式 + try: + datetime.strptime(date, '%Y-%m-%d') + except ValueError: + return jsonify({'code': 1, 'message': '日期格式错误,应为 YYYY-MM-DD'}) + + log_dir = os.path.join(LOG_BASE_DIR, date) + + if not os.path.exists(log_dir): + return jsonify({'code': 0, 'data': []}) + + log_files = [] + file_names = ['all.log', 'error.log', 'request.log'] + + for file_name in file_names: + file_path = os.path.join(log_dir, file_name) + if os.path.exists(file_path): + size = os.path.getsize(file_path) + log_files.append({ + 'name': file_name.replace('.log', ''), + 'file': f'{date}/{file_name}', + 'size': format_file_size(size) + }) + + return jsonify({'code': 0, 'data': log_files}) + + +@logs_bp.route('/log-content', methods=['GET']) +def get_log_content(): + """获取日志文件内容""" + file_path = request.args.get('file') + if not file_path: + return jsonify({'code': 1, 'message': '缺少文件参数'}) + + # 防止目录遍历攻击 + file_path = file_path.replace('..', '').replace('//', '/') + full_path = os.path.join(LOG_BASE_DIR, file_path) + + # 验证文件路径是否在日志目录下 + if not full_path.startswith(LOG_BASE_DIR): + return jsonify({'code': 1, 'message': '无效的文件路径'}) + + if not os.path.exists(full_path) or not os.path.isfile(full_path): + return jsonify({'code': 1, 'message': '日志文件不存在'}) + + try: + size = os.path.getsize(full_path) + # 限制读取大小,最大 5MB + max_size = 5 * 1024 * 1024 + if size > max_size: + with open(full_path, 'r', encoding='utf-8') as f: + f.seek(size - max_size) + content = '... (日志文件较大,已显示最后 5MB 内容) ...\n\n' + f.read() + else: + with open(full_path, 'r', encoding='utf-8') as f: + content = f.read() + + return jsonify({ + 'code': 0, + 'data': { + 'file': os.path.basename(file_path), + 'path': file_path, + 'size': format_file_size(size), + 'content': content + } + }) + except Exception as e: + return jsonify({'code': 1, 'message': f'读取日志文件失败: {str(e)}'}) diff --git a/src/main.py b/src/main.py index 84fdcf7..8483209 100644 --- a/src/main.py +++ b/src/main.py @@ -7,6 +7,9 @@ import json import pymysql import yaml import time +import logging +from datetime import datetime +from logging.handlers import TimedRotatingFileHandler, RotatingFileHandler from flask import Flask, request, jsonify, send_from_directory from flask_cors import CORS from werkzeug.utils import secure_filename @@ -30,6 +33,82 @@ def load_config(): CONFIG = load_config() +# ============ 日志系统配置 ============ +LOG_BASE_DIR = os.path.join(PROJECT_ROOT, 'logs') + + +def setup_logger(name='app'): + """配置日志系统,按日期分目录存储""" + # 创建当天的日志目录 + today = datetime.now().strftime('%Y-%m-%d') + log_dir = os.path.join(LOG_BASE_DIR, today) + os.makedirs(log_dir, exist_ok=True) + + # 获取或创建 logger + logger = logging.getLogger(name) + logger.setLevel(logging.DEBUG) + + # 清除已存在的处理器 + logger.handlers.clear() + + # 1. 全部日志处理器 (TimedRotatingFileHandler - 每天午夜分割) + all_log_path = os.path.join(log_dir, 'all.log') + all_handler = TimedRotatingFileHandler(all_log_path, when='midnight', interval=1, backupCount=30, encoding='utf-8') + all_handler.setLevel(logging.DEBUG) + all_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + )) + + # 2. 错误日志处理器 (RotatingFileHandler - 10MB大小分割,保留10个备份) + error_log_path = os.path.join(log_dir, 'error.log') + error_handler = RotatingFileHandler(error_log_path, maxBytes=10*1024*1024, backupCount=10, encoding='utf-8') + error_handler.setLevel(logging.ERROR) + error_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s in %(module)s: %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + )) + + # 3. 请求日志处理器 + request_log_path = os.path.join(log_dir, 'request.log') + request_handler = RotatingFileHandler(request_log_path, maxBytes=10*1024*1024, backupCount=10, encoding='utf-8') + request_handler.setLevel(logging.INFO) + request_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(message)s', + datefmt='%Y-%m-%d %H:%M:%S' + )) + + # 4. 控制台处理器 + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(logging.Formatter( + '[%(asctime)s] %(levelname)s: %(message)s', + datefmt='%H:%M:%S' + )) + + # 添加处理器到 logger + logger.addHandler(all_handler) + logger.addHandler(error_handler) + logger.addHandler(console_handler) + + # 为请求日志创建单独的 logger + request_logger = logging.getLogger('request') + request_logger.setLevel(logging.INFO) + request_logger.handlers.clear() + request_logger.addHandler(request_handler) + request_logger.addHandler(console_handler) + + return logger + + +# 初始化日志系统 +logger = setup_logger('app') +request_logger = logging.getLogger('request') + +logger.info('=' * 50) +logger.info('服务启动') +logger.info('=' * 50) + def get_db_connection(): """获取数据库连接""" @@ -47,7 +126,7 @@ def get_db_connection(): def init_database(): """初始化数据库表""" - print("正在初始化数据库...") + logger.info("正在初始化数据库...") try: conn = get_db_connection() cursor = conn.cursor() @@ -60,6 +139,7 @@ def init_database(): base_model VARCHAR(255), train_type VARCHAR(50), train_method VARCHAR(50), + gpus JSON COMMENT 'GPU硬件选择,支持多卡训练', dataset_id INT, valid_split VARCHAR(50), valid_ratio INT DEFAULT 10, @@ -212,31 +292,31 @@ def init_database(): for i, table_sql in enumerate(tables): try: cursor.execute(table_sql) - print(f" 表 {i+1}/{len(tables)} 创建/检查成功") + logger.debug(f"表 {i+1}/{len(tables)} 创建/检查成功") except Exception as e: - print(f" 表 {i+1} 创建失败: {e}") + logger.error(f"表 {i+1} 创建失败: {e}") # 为已存在的表添加缺失的列(静默处理,不显示重复列的提示) - for table_col in [("model_manage", "purpose"), ("model_eval", "name")]: + for table_col in [("model_manage", "purpose"), ("model_eval", "name"), ("fine_tune", "gpus")]: try: table_name, col_name = table_col - cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} VARCHAR(255) DEFAULT ''") - print(f" {table_name} 表添加 {col_name} 列成功") - except Exception as e: + cursor.execute(f"ALTER TABLE {table_name} ADD COLUMN {col_name} JSON") + logger.debug(f"{table_name} 表添加 {col_name} 列成功") + except Exception: pass # 列已存在时不输出任何信息 # 插入默认管理员用户 cursor.execute("SELECT * FROM users WHERE username = 'admin'") if not cursor.fetchone(): cursor.execute("INSERT INTO users (username, password, role) VALUES ('admin', 'admin', 'admin')") - print(" 默认管理员用户创建成功") + logger.info("默认管理员用户创建成功") conn.commit() cursor.close() conn.close() - print("数据库初始化完成") + logger.info("数据库初始化完成") except Exception as e: - print(f"数据库初始化失败: {e}") + logger.error(f"数据库初始化失败: {e}") raise @@ -250,6 +330,16 @@ CORS(app, origins="*", methods=["GET", "POST", "PUT", "DELETE", "OPTIONS"], allo register_blueprints(app) +# ============ 请求日志中间件 ============ +@app.after_request +def log_request(response): + """记录所有API请求""" + # 排除健康检查接口 + if request.path != '/api/health': + request_logger.info(f'{request.method} {request.path} - {response.status_code}') + return response + + # ============ 健康检查 ============ @app.route('/api/health', methods=['GET']) def health_check(): @@ -272,6 +362,7 @@ def health_check(): } }) except Exception as e: + logger.error(f"健康检查失败: {e}") return jsonify({'status': 'error', 'code': 1, 'message': str(e)}) @@ -689,4 +780,9 @@ if __name__ == '__main__': # 启动前先初始化数据库 init_database() app_config = CONFIG['app'] - app.run(host=app_config['host'], port=app_config['port'], debug=app_config.get('debug', True)) + host = app_config['host'] + port = app_config['port'] + debug = app_config.get('debug', True) + logger.info(f'服务启动于 http://{host}:{port}') + logger.info(f'Debug模式: {debug}') + app.run(host=host, port=port, debug=debug) diff --git a/web/pages/fine-tune-create.html b/web/pages/fine-tune-create.html index 6722044..6c323f9 100644 --- a/web/pages/fine-tune-create.html +++ b/web/pages/fine-tune-create.html @@ -225,6 +225,18 @@

训练配置

+ +
+ +
+ +
正在加载GPU设备...
+
+

支持选择多个GPU进行训练(多卡训练)

+
+
@@ -627,6 +639,9 @@ // 初始化训练方法参数显示 toggleTrainMethod(); + // 加载GPU列表 + loadGPUList(); + // 设置侧边栏当前页高亮 const currentPage = 'fine-tune'; document.querySelectorAll('.nav-link').forEach(link => { @@ -790,17 +805,113 @@ } } + // 加载GPU列表 + async function loadGPUList() { + const container = document.getElementById('gpuSelectionArea'); + if (!container) return; + + try { + // 模拟GPU数据 - 实际项目中可以从API获取 + const gpuData = await fetchGPUs(); + renderGPUList(gpuData); + } catch (error) { + console.error('加载GPU列表失败:', error); + container.innerHTML = '
加载GPU设备失败,请刷新重试
'; + } + } + + // 获取GPU数据(模拟数据,实际可从API获取) + async function fetchGPUs() { + // 实际项目中可以调用后端API获取GPU信息 + // const response = await fetch(`${API_BASE}/gpus`); + // return await response.json(); + + // 模拟GPU数据 + return [ + { id: 'gpu0', name: 'NVIDIA A100 80GB', memory: '80GB', cuda_cores: 6912, available: true }, + { id: 'gpu1', name: 'NVIDIA A100 80GB', memory: '80GB', cuda_cores: 6912, available: true }, + { id: 'gpu2', name: 'NVIDIA A100 40GB', memory: '40GB', cuda_cores: 6912, available: true }, + { id: 'gpu3', name: 'NVIDIA A100 40GB', memory: '40GB', cuda_cores: 6912, available: false }, + { id: 'gpu4', name: 'NVIDIA V100 32GB', memory: '32GB', cuda_cores: 5120, available: true }, + { id: 'gpu5', name: 'NVIDIA V100 16GB', memory: '16GB', cuda_cores: 5120, available: false }, + { id: 'gpu6', name: 'NVIDIA RTX 3090', memory: '24GB', cuda_cores: 10496, available: true }, + { id: 'gpu7', name: 'NVIDIA RTX 4090', memory: '24GB', cuda_cores: 16384, available: true } + ]; + } + + // 渲染GPU列表(点击卡片选中,无需复选框) + function renderGPUList(gpus) { + const container = document.getElementById('gpuSelectionArea'); + if (!container) return; + + container.innerHTML = gpus.map(gpu => ` +
+
+ +
+
+ ${gpu.name} + ${gpu.available + ? '' + : ''} +
+
+ ${gpu.memory} + ${gpu.cuda_cores} CUDA +
+
+
+
+ `).join(''); + } + + // 切换GPU选择状态 + function toggleGPUSelection(gpuId) { + const card = document.getElementById(`gpu_card_${gpuId}`); + if (!card || card.classList.contains('opacity-50')) return; + + // 切换选中状态 + if (card.classList.contains('border-primary')) { + // 取消选中 + card.classList.remove('border-primary', 'bg-blue-50'); + card.querySelector('.fa-check-circle').classList.replace('text-primary', 'text-green-600'); + } else { + // 选中 + card.classList.add('border-primary', 'bg-blue-50'); + // 移除检查图标,添加选中标记 + const icon = card.querySelector('.fa-check-circle'); + if (icon) { + icon.classList.remove('fa-check-circle', 'text-green-600'); + icon.classList.add('fa-check', 'text-primary'); + } + } + } + + // 获取选中的GPU列表 + function getSelectedGPUs() { + const cards = document.querySelectorAll('.gpu-card.border-primary'); + return Array.from(cards).map(card => card.dataset.gpuId); + } + // 提交表单 async function submitForm() { const form = document.getElementById('createForm'); const formData = new FormData(form); const validSplit = formData.get('valid_split'); + // 获取选中的GPU + const selectedGPUs = getSelectedGPUs(); + const data = { name: formData.get('name'), base_model: formData.get('base_model'), train_type: formData.get('train_type'), train_method: formData.get('train_method'), + gpus: selectedGPUs, // 添加GPU选择 train_dataset_id: formData.get('train_dataset_id'), valid_split: validSplit, valid_ratio: parseInt(formData.get('valid_ratio')) || 10, @@ -814,6 +925,10 @@ showMessage('提示', '请输入任务名称', 'warning'); return; } + if (selectedGPUs.length === 0) { + showMessage('提示', '请选择至少一个GPU硬件', 'warning'); + return; + } if (!data.base_model) { showMessage('提示', '请选择基础模型', 'warning'); return; diff --git a/web/pages/main.html b/web/pages/main.html index d4d010d..580473d 100644 --- a/web/pages/main.html +++ b/web/pages/main.html @@ -236,6 +236,12 @@ 平台性能
+ @@ -328,6 +334,58 @@ }; const API_BASE = getApiBase(); + // 日志自动刷新相关变量 + let logRefreshTimer = null; + let logCountdownTimer = null; + let logCurrentInterval = 10; + let logFullContent = ''; // 存储完整日志内容 + + // 设置自动刷新间隔 + function setRefreshInterval() { + const select = document.getElementById('logRefreshInterval'); + const countdownEl = document.getElementById('logRefreshCountdown'); + const secondsEl = document.getElementById('countdownNumber'); + + if (!select) return; + + logCurrentInterval = parseInt(select.value) || 10; + + // 清除之前的定时器 + if (logRefreshTimer) { + clearInterval(logRefreshTimer); + logRefreshTimer = null; + } + if (logCountdownTimer) { + clearInterval(logCountdownTimer); + logCountdownTimer = null; + } + + // 如果选择关闭,不显示倒计时 + if (select.value === '0') { + countdownEl.classList.add('hidden'); + return; + } + + // 显示倒计时 + countdownEl.classList.remove('hidden'); + secondsEl.textContent = logCurrentInterval; + + // 启动倒计时 + let countdown = logCurrentInterval; + logCountdownTimer = setInterval(() => { + countdown--; + if (countdown <= 0) { + countdown = logCurrentInterval; + } + secondsEl.textContent = countdown; + }, 1000); + + // 启动自动刷新 + logRefreshTimer = setInterval(() => { + refreshLogs(); + }, logCurrentInterval * 1000); + } + // 获取系统性能监控数据 async function fetchSystemMetrics() { try { @@ -511,6 +569,12 @@ hasCreate: false, isHardwareMonitor: true }, + 'logs': { + title: '查看日志', + skipFetch: true, + hasCreate: false, + isLogViewer: true + }, 'model-compare-chat': { title: '模型对比', skipFetch: true, @@ -660,6 +724,9 @@ // 切换页面时清除选中状态 clearSelection(); + // 离开日志页面时停止自动刷新 + stopLogAutoRefresh(); + const container = document.getElementById('page-content'); const config = tableConfigs[pageName]; @@ -732,6 +799,10 @@ } else if (config.isHardwareMonitor) { // 硬件监控页面使用模拟数据,不调用API container.innerHTML = renderConfigPage(config, null); + } else if (config.isLogViewer) { + // 日志查看页面 + container.innerHTML = renderLogViewerPage(config); + initLogViewer(); } else if (config.isForm) { const data = await fetchData(`${API_BASE}/${config.api}`); container.innerHTML = renderConfigPage(config, data); @@ -1106,6 +1177,227 @@ `; } + // 渲染日志查看页面 + function renderLogViewerPage(config) { + return ` +
+
+

${config.title}

+
+ +
+
+
+ +
+
+ + +
+
+ + +
+ +
+ +
+ + +
+ +
+
+ 请选择日志文件 +
+ + + +
+
+
日志内容将在这里显示...
+
+
+
+ `; + } + + // 初始化日志查看器 + function initLogViewer() { + const datePicker = document.getElementById('logDatePicker'); + if (datePicker) { + const today = new Date().toISOString().split('T')[0]; + datePicker.value = today; + loadLogFiles(); + } + // 启动自动刷新 + setRefreshInterval(); + } + + // 加载日志文件列表 + async function loadLogFiles() { + const datePicker = document.getElementById('logDatePicker'); + const logTypeSelect = document.getElementById('logTypeSelect'); + const selectedDate = datePicker.value; + + logTypeSelect.innerHTML = ''; + + try { + const response = await fetch(`${API_BASE}/log-files?date=${selectedDate}`); + const result = await response.json(); + + if (result.code === 0 && result.data) { + logTypeSelect.innerHTML = ''; + result.data.forEach(log => { + const option = document.createElement('option'); + option.value = log.file; + option.textContent = log.name + ' (' + log.size + ')'; + logTypeSelect.appendChild(option); + }); + // 如果有日志文件,自动加载第一个 + if (result.data.length > 0) { + logTypeSelect.value = result.data[0].file; + loadSelectedLog(); + } + } else { + logTypeSelect.innerHTML = ''; + document.getElementById('logContent').textContent = '该日期暂无日志文件'; + document.getElementById('logFileInfo').textContent = '无日志文件'; + } + } catch (error) { + console.error('加载日志文件列表失败:', error); + logTypeSelect.innerHTML = ''; + document.getElementById('logContent').textContent = '加载日志文件列表失败: ' + error.message; + } + } + + // 加载选中的日志 + async function loadSelectedLog() { + const logTypeSelect = document.getElementById('logTypeSelect'); + const logFile = logTypeSelect.value; + const logContent = document.getElementById('logContent'); + const logFileInfo = document.getElementById('logFileInfo'); + + if (!logFile) { + logContent.textContent = '请选择日志文件'; + logFileInfo.textContent = '无日志文件'; + return; + } + + logContent.textContent = '加载中...'; + logFileInfo.textContent = '加载中...'; + + try { + const response = await fetch(`${API_BASE}/log-content?file=${encodeURIComponent(logFile)}`); + const result = await response.json(); + + if (result.code === 0) { + logFullContent = result.data.content || ''; + logContent.textContent = logFullContent || '(空日志)'; + logFileInfo.textContent = result.data.file + ' (' + result.data.size + ')'; + // 清空搜索框和匹配计数 + document.getElementById('logSearchInput').value = ''; + document.getElementById('logMatchCount').textContent = ''; + // 滚动到最底部 + scrollToLogBottom(); + } else { + logContent.textContent = '加载失败: ' + (result.message || '未知错误'); + logFileInfo.textContent = '加载失败'; + } + } catch (error) { + console.error('加载日志内容失败:', error); + logContent.textContent = '加载日志内容失败: ' + error.message; + logFileInfo.textContent = '加载失败'; + } + } + + // 刷新日志 + function refreshLogs() { + loadLogFiles(); + if (document.getElementById('logTypeSelect').value) { + loadSelectedLog(); + } + // 重置倒计时 + const select = document.getElementById('logRefreshInterval'); + const secondsEl = document.getElementById('countdownNumber'); + if (select && select.value !== '0' && secondsEl) { + secondsEl.textContent = logCurrentInterval; + } + } + + // 停止日志自动刷新(离开页面时调用) + function stopLogAutoRefresh() { + if (logRefreshTimer) { + clearInterval(logRefreshTimer); + logRefreshTimer = null; + } + if (logCountdownTimer) { + clearInterval(logCountdownTimer); + logCountdownTimer = null; + } + } + + // 滚动到日志底部 + function scrollToLogBottom() { + const logContent = document.getElementById('logContent'); + if (logContent) { + logContent.scrollTop = logContent.scrollHeight; + } + } + + // 过滤日志内容 + function filterLogContent() { + const searchInput = document.getElementById('logSearchInput'); + const matchCount = document.getElementById('logMatchCount'); + const logContent = document.getElementById('logContent'); + + if (!searchInput || !matchCount || !logContent) return; + + const keyword = searchInput.value.trim(); + + if (!keyword) { + logContent.textContent = logFullContent || '(空日志)'; + matchCount.textContent = ''; + scrollToLogBottom(); + return; + } + + const lines = logFullContent.split('\n'); + const matchingLines = lines.filter(line => line.toLowerCase().includes(keyword.toLowerCase())); + + if (matchingLines.length > 0) { + logContent.textContent = matchingLines.join('\n'); + matchCount.textContent = `(${matchingLines.length}条匹配)`; + // 滚动到最底部查看最新匹配 + scrollToLogBottom(); + } else { + logContent.textContent = '未找到匹配的日志'; + matchCount.textContent = '(0条匹配)'; + } + } + + // 清空日志内容显示 + function clearLogContent() { + document.getElementById('logContent').textContent = '日志内容将在这里显示...'; + document.getElementById('logFileInfo').textContent = '请选择日志文件'; + document.getElementById('logTypeSelect').value = ''; + document.getElementById('logSearchInput').value = ''; + document.getElementById('logMatchCount').textContent = ''; + logFullContent = ''; + } + // 渲染工具卡片页面 function renderToolsPage(config) { // 渲染单个工具卡片 @@ -2511,6 +2803,64 @@ } } } + + // ============ Web日志系统 ============ + const webLogger = { + _currentPage: 'main', + + // 初始化当前页面名称 + init: function(pageName) { + this._currentPage = pageName || 'unknown'; + }, + + // 发送日志到服务器 + _sendLog: async function(level, message) { + try { + await fetch(`${API_BASE}/web-log`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + level: level, + message: message, + page: this._currentPage, + timestamp: new Date().toISOString() + }) + }); + } catch (e) { + // 发送失败时只记录到控制台 + console.warn('日志发送失败:', e); + } + }, + + info: function(message) { + console.log(`[INFO] ${message}`); + this._sendLog('info', message); + }, + + error: function(message) { + console.error(`[ERROR] ${message}`); + this._sendLog('error', message); + }, + + warning: function(message) { + console.warn(`[WARNING] ${message}`); + this._sendLog('warning', message); + }, + + debug: function(message) { + console.debug(`[DEBUG] ${message}`); + this._sendLog('debug', message); + } + }; + + // 页面加载完成后初始化日志 + document.addEventListener('DOMContentLoaded', function() { + // 获取当前页面名称 + const path = window.location.pathname; + const pageName = path.split('/').pop().replace('.html', '') || 'main'; + webLogger.init(pageName); + webLogger.info('页面加载完成'); + }); diff --git a/web/pages/model-eval.html b/web/pages/model-eval.html index baa28e3..89e85d3 100644 --- a/web/pages/model-eval.html +++ b/web/pages/model-eval.html @@ -94,6 +94,23 @@
+ + +