Compare commits

...

4 Commits

Author SHA1 Message Date
snaily
73e98a185d fix:修复gemini格式不能查询模型列表的问题 2025-04-13 12:45:23 +08:00
snaily
73a7c81f85 feat(logs): 添加错误日志详情查看功能并优化列表显示
本次提交主要围绕错误日志模块进行了功能增强和优化:

- **后端 (`database/services.py`, `router/log_routes.py`):**
    - 新增了根据日志 ID 获取单个错误日志完整详情(包括 `error_log` 和 `request_msg`)的数据库服务函数 (`get_error_log_details`) 和对应的 API 路由 (`/api/logs/errors/{log_id}/details`)。
    - 修改了获取错误日志列表的 API (`/api/logs/errors`):
        - 在返回数据中增加了 `error_code` 字段,以便前端展示。
        - 优化了数据库查询,明确指定需要选择的列,提升性能。
        - 将默认排序方式从按请求时间改为按日志 ID 降序排列,使最新的错误优先显示。
        - 改进了未授权访问时的处理,返回标准的 401 HTTP 状态码。
    - 更新了相关的 Pydantic 模型以匹配新的数据结构。

- **前端 (`static/js/error_logs.js`, `templates/error_logs.html`):**
    - 在错误日志列表页面,将原先显示部分错误日志内容的列修改为显示 "错误码"。
    - 实现了点击 "详情" 按钮时,通过异步请求新的详情 API 获取并展示完整的错误日志信息(包括详细错误日志和请求消息)的功能。
    - 在详情模态框中添加了加载状态提示和获取数据失败时的错误处理逻辑。
2025-04-13 04:36:34 +08:00
snaily
86dba93974 fix: 修复 error_logs.html 中的脚本路径错误 2025-04-13 01:16:59 +08:00
snaily
439165bc6c refactor: 移除 auth.js 并修复 error_logs.html 脚本路径
- 删除了不再使用的 `app/static/js/auth.js` 文件。
- 修正了 `app/templates/error_logs.html` 中 `error_logs.js` 的脚本引用路径,移除了 `url_for` 函数调用,直接使用静态路径。
2025-04-13 01:08:42 +08:00
6 changed files with 213 additions and 112 deletions

View File

@@ -175,7 +175,15 @@ async def get_error_logs(
List[Dict[str, Any]]: 错误日志列表
"""
try:
query = select(ErrorLog)
query = select(
ErrorLog.id,
ErrorLog.gemini_key,
ErrorLog.model_name,
ErrorLog.error_type,
ErrorLog.error_log,
ErrorLog.error_code,
ErrorLog.request_time
)
# Apply filters
if key_search:
@@ -192,7 +200,7 @@ async def get_error_logs(
query = query.where(ErrorLog.request_time < end_date)
# Apply ordering, limit, and offset
query = query.order_by(ErrorLog.request_time.desc()).limit(limit).offset(offset)
query = query.order_by(ErrorLog.id.desc()).limit(limit).offset(offset)
result = await database.fetch_all(query)
return [dict(row) for row in result]
@@ -242,6 +250,37 @@ async def get_error_logs_count(
logger.exception(f"Failed to count error logs with filters: {str(e)}") # Use exception for stack trace
raise
# 新增函数:获取单条错误日志详情
async def get_error_log_details(log_id: int) -> Optional[Dict[str, Any]]:
"""
根据 ID 获取单个错误日志的详细信息
Args:
log_id (int): 错误日志的 ID
Returns:
Optional[Dict[str, Any]]: 包含日志详细信息的字典,如果未找到则返回 None
"""
try:
query = select(ErrorLog).where(ErrorLog.id == log_id)
result = await database.fetch_one(query)
if result:
# 将 request_msg (JSONB) 转换为字符串以便在 API 中返回
log_dict = dict(result)
if 'request_msg' in log_dict and log_dict['request_msg'] is not None:
# 确保即使是 None 或非 JSON 数据也能处理
try:
log_dict['request_msg'] = json.dumps(log_dict['request_msg'], ensure_ascii=False, indent=2)
except TypeError:
log_dict['request_msg'] = str(log_dict['request_msg']) # Fallback to string
return log_dict
else:
return None
except Exception as e:
logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}")
raise
# 新增函数:添加请求日志
async def add_request_log(
model_name: Optional[str],

View File

@@ -53,8 +53,8 @@ async def list_models(
model_mapping = {x.get("name", "").split("/", maxsplit=1)[1]: x for x in models_json["models"]}
# 添加搜索模型
if model_service.search_models:
for name in model_service.search_models:
if settings.SEARCH_MODELS:
for name in settings.SEARCH_MODELS:
model = model_mapping.get(name)
if not model:
continue
@@ -68,8 +68,8 @@ async def list_models(
models_json["models"].append(item)
# 添加图像生成模型
if model_service.image_models:
for name in model_service.image_models:
if settings.IMAGE_MODELS:
for name in settings.IMAGE_MODELS:
model = model_mapping.get(name)
if not model:
continue

View File

@@ -1,15 +1,15 @@
"""
日志路由模块
"""
from typing import Any, Dict, List, Optional
from typing import List, Optional
from datetime import datetime
from pydantic import BaseModel
from fastapi import APIRouter, HTTPException, Request, Query
from fastapi.responses import RedirectResponse
from fastapi import APIRouter, HTTPException, Request, Query, Path
from app.core.security import verify_auth_token
from app.log.logger import get_log_routes_logger
from app.database.services import get_error_logs, get_error_logs_count
# 假设这些服务函数已更新或添加
from app.database.services import get_error_logs, get_error_logs_count, get_error_log_details
# 创建路由
router = APIRouter(prefix="/api/logs", tags=["logs"])
@@ -18,45 +18,61 @@ logger = get_log_routes_logger()
# Define a response model that includes the total count for pagination
class ErrorLogResponse(BaseModel):
logs: List[Dict[str, Any]]
# 用于列表响应的模型,假设 get_error_logs 返回包含 error_code 的字典
class ErrorLogListItem(BaseModel):
id: int
gemini_key: Optional[str] = None
error_type: Optional[str] = None
error_code: Optional[int] = None # 列表显示错误码 (应为整数)
model_name: Optional[str] = None
request_time: Optional[datetime] = None
class ErrorLogListResponse(BaseModel):
logs: List[ErrorLogListItem] # 使用定义的模型列表
total: int
@router.get("/errors", response_model=ErrorLogResponse)
@router.get("/errors", response_model=ErrorLogListResponse)
async def get_error_logs_api(
request: Request,
limit: int = Query(20, ge=1, le=1000), # Default to 20 to match frontend
limit: int = Query(10, ge=1, le=1000),
offset: int = Query(0, ge=0),
key_search: Optional[str] = Query(None, description="Search term for Gemini key (partial match)"),
error_search: Optional[str] = Query(None, description="Search term for error type or log message"),
start_date: Optional[datetime] = Query(None, description="Start datetime for filtering (YYYY-MM-DDTHH:MM)"),
end_date: Optional[datetime] = Query(None, description="End datetime for filtering (YYYY-MM-DDTHH:MM)")
error_search: Optional[str] = Query(None, description="Search term for error type or log message"), # 数据库查询需处理
start_date: Optional[datetime] = Query(None, description="Start datetime for filtering"),
end_date: Optional[datetime] = Query(None, description="End datetime for filtering")
):
"""
获取错误日志
获取错误日志列表 (返回错误码)
Args:
request: 请求对象
limit: 限制数量
offset: 偏移量
key_search: 密钥搜索
error_search: 错误搜索 (可能搜索类型或日志内容由DB层决定)
start_date: 开始日期
end_date: 结束日期
Returns:
ErrorLogResponse: An object containing the list of logs and the total count.
ErrorLogListResponse: An object containing the list of logs (with error_code) and the total count.
"""
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning("Unauthorized access attempt to error logs")
return RedirectResponse(url="/", status_code=302)
logger.warning("Unauthorized access attempt to error logs list")
# API 返回 401 更合适
raise HTTPException(status_code=401, detail="Not authenticated")
try:
# Fetch logs with search parameters
logs = await get_error_logs(
# 假设 get_error_logs 现在返回包含 error_code 的字典列表
# 并且可以接受 include_error_code 参数 (如果需要显式指定)
logs_data = await get_error_logs(
limit=limit,
offset=offset,
key_search=key_search,
error_search=error_search,
error_search=error_search, # 数据库查询需要处理这个
start_date=start_date,
end_date=end_date
end_date=end_date,
# include_error_code=True # 如果需要显式传递
)
# Fetch total count with the same search parameters
total_count = await get_error_logs_count(
@@ -65,7 +81,45 @@ async def get_error_logs_api(
start_date=start_date,
end_date=end_date
)
return ErrorLogResponse(logs=logs, total=total_count)
# 验证并转换数据以匹配 Pydantic 模型
validated_logs = [ErrorLogListItem(**log) for log in logs_data]
return ErrorLogListResponse(logs=validated_logs, total=total_count)
except Exception as e:
logger.exception(f"Failed to get error logs: {str(e)}") # Use logger.exception for stack trace
raise HTTPException(status_code=500, detail=f"Failed to get error logs: {str(e)}")
logger.exception(f"Failed to get error logs list: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get error logs list: {str(e)}")
# 新增:获取错误日志详情的路由
class ErrorLogDetailResponse(BaseModel):
id: int
gemini_key: Optional[str] = None
error_type: Optional[str] = None
error_log: Optional[str] = None # 详情接口返回完整的 error_log
request_msg: Optional[str] = None # 详情接口返回 request_msg
model_name: Optional[str] = None
request_time: Optional[datetime] = None
@router.get("/errors/{log_id}/details", response_model=ErrorLogDetailResponse)
async def get_error_log_detail_api(request: Request, log_id: int = Path(..., ge=1)):
"""
根据日志 ID 获取错误日志的详细信息 (包括 error_log 和 request_msg)
"""
auth_token = request.cookies.get("auth_token")
if not auth_token or not verify_auth_token(auth_token):
logger.warning(f"Unauthorized access attempt to error log details for ID: {log_id}")
raise HTTPException(status_code=401, detail="Not authenticated")
try:
# 假设存在一个函数 get_error_log_details(log_id) 来获取完整信息
log_details = await get_error_log_details(log_id=log_id)
if not log_details:
raise HTTPException(status_code=404, detail="Error log not found")
# 假设 get_error_log_details 返回一个字典或兼容 Pydantic 的对象
return ErrorLogDetailResponse(**log_details)
except HTTPException as http_exc:
# Re-raise HTTPException (like 404)
raise http_exc
except Exception as e:
logger.exception(f"Failed to get error log details for ID {log_id}: {str(e)}")
raise HTTPException(status_code=500, detail=f"Failed to get error log details: {str(e)}")

View File

@@ -1,18 +0,0 @@
if ('serviceWorker' in navigator) {
window.addEventListener('load', () => {
navigator.serviceWorker.register('/static/service-worker.js')
.then(registration => {
console.log('ServiceWorker注册成功:', registration.scope);
})
.catch(error => {
console.log('ServiceWorker注册失败:', error);
});
});
}
document.addEventListener('DOMContentLoaded', () => {
const copyrightYear = document.querySelector('.copyright script');
if (copyrightYear) {
copyrightYear.textContent = new Date().getFullYear();
}
});

View File

@@ -246,18 +246,13 @@ async function loadErrorLogs() {
throw new Error(errorData?.detail || `网络响应异常: ${response.statusText}`);
}
const data = await response.json();
// Assuming the API returns an object like { logs: [], total: count }
// If it only returns an array, we can't get the total count accurately for pagination
if (Array.isArray(data)) {
errorLogs = data;
renderErrorLogs(errorLogs); // Pass data directly
updatePagination(errorLogs.length, -1); // Indicate unknown total
} else if (data && Array.isArray(data.logs)) {
errorLogs = data.logs;
renderErrorLogs(errorLogs); // Pass logs array
updatePagination(errorLogs.length, data.total || -1); // Pass total count if available
// API 现在返回 { logs: [], total: count }
if (data && Array.isArray(data.logs)) {
errorLogs = data.logs; // Store the list data (contains error_code)
renderErrorLogs(errorLogs);
updatePagination(errorLogs.length, data.total || -1);
} else {
throw new Error('无法识别的API响应格式');
throw new Error('无法识别的API响应格式');
}
@@ -302,8 +297,8 @@ function renderErrorLogs(logs) {
} catch (e) { console.error("Error formatting date:", e); }
// Truncate error log content for display
const errorLogContent = log.error_log ? log.error_log.substring(0, 100) + (log.error_log.length > 100 ? '...' : '') : '无';
// Display error code instead of truncated log
const errorCodeContent = log.error_code || '无';
// Mask the Gemini key for display in the table
const maskKey = (key) => {
@@ -316,7 +311,7 @@ function renderErrorLogs(logs) {
<td>${sequentialId}</td> <!-- Use sequential ID -->
<td title="${log.gemini_key || ''}">${maskedKey}</td>
<td>${log.error_type || '未知'}</td>
<td class="error-log-content" title="${log.error_log || ''}">${errorLogContent}</td>
<td class="error-code-content" title="${log.error_code || ''}">${errorCodeContent}</td>
<td>${log.model_name || '未知'}</td>
<td>${formattedTime}</td>
<td>
@@ -338,57 +333,88 @@ function renderErrorLogs(logs) {
});
}
// 显示错误日志详情 (Custom Modal Logic)
function showLogDetails(logId) {
const log = errorLogs.find(l => l.id === logId);
if (!log || !logDetailModal) return;
// 显示错误日志详情 (从 API 获取)
async function showLogDetails(logId) {
if (!logDetailModal) return;
// Format date
let formattedTime = 'N/A';
try {
const requestTime = new Date(log.request_time);
if (!isNaN(requestTime)) {
formattedTime = requestTime.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
});
}
} catch (e) { console.error("Error formatting date:", e); }
// Show loading state in modal (optional)
// Clear previous content and show a spinner or message
document.getElementById('modalGeminiKey').textContent = '加载中...';
document.getElementById('modalErrorType').textContent = '加载中...';
document.getElementById('modalErrorLog').textContent = '加载中...';
document.getElementById('modalRequestMsg').textContent = '加载中...';
document.getElementById('modalModelName').textContent = '加载中...';
document.getElementById('modalRequestTime').textContent = '加载中...';
// Format request message (handle potential JSON)
let formattedRequestMsg = '无';
if (log.request_msg) {
try {
// Check if it's already an object/array
if (typeof log.request_msg === 'object' && log.request_msg !== null) {
formattedRequestMsg = JSON.stringify(log.request_msg, null, 2);
}
// Check if it's a JSON string
else if (typeof log.request_msg === 'string' && log.request_msg.trim().startsWith('{') || log.request_msg.trim().startsWith('[')) {
formattedRequestMsg = JSON.stringify(JSON.parse(log.request_msg), null, 2);
}
else {
formattedRequestMsg = String(log.request_msg);
}
} catch (e) {
formattedRequestMsg = String(log.request_msg); // Fallback to string
console.warn("Could not parse request_msg as JSON:", e);
}
}
// Populate modal content (show full key in modal)
document.getElementById('modalGeminiKey').textContent = log.gemini_key || '无';
document.getElementById('modalErrorType').textContent = log.error_type || '未知';
document.getElementById('modalErrorLog').textContent = log.error_log || '无';
document.getElementById('modalRequestMsg').textContent = formattedRequestMsg;
document.getElementById('modalModelName').textContent = log.model_name || '未知';
document.getElementById('modalRequestTime').textContent = formattedTime;
// Show the modal
logDetailModal.classList.add('show');
// Optional: Prevent body scrolling when modal is open
document.body.style.overflow = 'hidden';
document.body.style.overflow = 'hidden'; // Prevent body scrolling
try {
const response = await fetch(`/api/logs/errors/${logId}/details`);
if (!response.ok) {
let errorData;
try {
errorData = await response.json();
} catch (e) { /* ignore */ }
throw new Error(errorData?.detail || `获取日志详情失败: ${response.statusText}`);
}
const logDetails = await response.json();
// Format date
let formattedTime = 'N/A';
try {
const requestTime = new Date(logDetails.request_time);
if (!isNaN(requestTime)) {
formattedTime = requestTime.toLocaleString('zh-CN', {
year: 'numeric', month: '2-digit', day: '2-digit',
hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
});
}
} catch (e) { console.error("Error formatting date:", e); }
// Format request message (handle potential JSON)
let formattedRequestMsg = '无';
if (logDetails.request_msg) {
try {
if (typeof logDetails.request_msg === 'object' && logDetails.request_msg !== null) {
formattedRequestMsg = JSON.stringify(logDetails.request_msg, null, 2);
} else if (typeof logDetails.request_msg === 'string') {
// Try parsing if it looks like JSON, otherwise display as string
const trimmedMsg = logDetails.request_msg.trim();
if (trimmedMsg.startsWith('{') || trimmedMsg.startsWith('[')) {
formattedRequestMsg = JSON.stringify(JSON.parse(logDetails.request_msg), null, 2);
} else {
formattedRequestMsg = logDetails.request_msg;
}
} else {
formattedRequestMsg = String(logDetails.request_msg);
}
} catch (e) {
formattedRequestMsg = String(logDetails.request_msg); // Fallback
console.warn("Could not parse request_msg as JSON:", e);
}
}
// Populate modal content with fetched details
document.getElementById('modalGeminiKey').textContent = logDetails.gemini_key || '无';
document.getElementById('modalErrorType').textContent = logDetails.error_type || '未知';
document.getElementById('modalErrorLog').textContent = logDetails.error_log || '无'; // Full error log
document.getElementById('modalRequestMsg').textContent = formattedRequestMsg; // Full request message
document.getElementById('modalModelName').textContent = logDetails.model_name || '未知';
document.getElementById('modalRequestTime').textContent = formattedTime;
} catch (error) {
console.error('获取日志详情失败:', error);
// Show error in modal
document.getElementById('modalGeminiKey').textContent = '错误';
document.getElementById('modalErrorType').textContent = '错误';
document.getElementById('modalErrorLog').textContent = `加载失败: ${error.message}`;
document.getElementById('modalRequestMsg').textContent = '错误';
document.getElementById('modalModelName').textContent = '错误';
document.getElementById('modalRequestTime').textContent = '错误';
// Optionally show a notification
showNotification(`加载日志详情失败: ${error.message}`, 'error', 5000);
}
}
// Close Log Detail Modal

View File

@@ -105,7 +105,7 @@
<th class="px-5 py-3 font-semibold rounded-tl-lg">ID</th> <!-- Increased padding, adjusted rounding -->
<th class="px-5 py-3 font-semibold">Gemini密钥</th>
<th class="px-5 py-3 font-semibold">错误类型</th>
<th class="px-5 py-3 font-semibold">错误日志</th>
<th class="px-5 py-3 font-semibold">错误</th>
<th class="px-5 py-3 font-semibold">模型名称</th>
<th class="px-5 py-3 font-semibold">请求时间</th>
<th class="px-5 py-3 font-semibold rounded-tr-lg">操作</th> <!-- Adjusted rounding -->
@@ -233,7 +233,7 @@
{% endblock %}
{% block body_scripts %}
<script src="{{ url_for('static', path='/js/error_logs.js') }}"></script>
<script src="/static/js/error_logs.js"></script>
<script>
// error_logs.html specific JS initialization (if any)
// e.g., initialize date pickers or other elements if needed