274 lines
8.4 KiB
Python
274 lines
8.4 KiB
Python
"""
|
|
回复器监控API
|
|
提供回复器日志数据的查询接口
|
|
|
|
性能优化:
|
|
1. 聊天摘要只统计文件数量和最新时间戳,不读取文件内容
|
|
2. 日志列表使用文件名解析时间戳,只在需要时读取完整内容
|
|
3. 详情按需加载
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from typing import Dict, List, Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query
|
|
from pydantic import BaseModel
|
|
|
|
from src.webui.dependencies import require_auth
|
|
|
|
router = APIRouter(prefix="/api/replier", tags=["replier"], dependencies=[Depends(require_auth)])
|
|
|
|
# 回复器日志目录
|
|
REPLY_LOG_DIR = Path("logs/reply")
|
|
|
|
|
|
class ReplierChatSummary(BaseModel):
|
|
"""聊天摘要 - 轻量级,不读取文件内容"""
|
|
|
|
chat_id: str
|
|
reply_count: int
|
|
latest_timestamp: float
|
|
latest_filename: str
|
|
|
|
|
|
class ReplyLogSummary(BaseModel):
|
|
"""回复日志摘要"""
|
|
|
|
chat_id: str
|
|
timestamp: float
|
|
filename: str
|
|
model: str
|
|
success: bool
|
|
llm_ms: float
|
|
overall_ms: float
|
|
output_preview: str
|
|
|
|
|
|
class ReplyLogDetail(BaseModel):
|
|
"""回复日志详情"""
|
|
|
|
type: str
|
|
chat_id: str
|
|
timestamp: float
|
|
prompt: str
|
|
output: str
|
|
processed_output: List[str]
|
|
model: str
|
|
reasoning: str
|
|
think_level: int
|
|
timing: Dict
|
|
error: Optional[str] = None
|
|
success: bool
|
|
|
|
|
|
class ReplierOverview(BaseModel):
|
|
"""回复器总览 - 轻量级统计"""
|
|
|
|
total_chats: int
|
|
total_replies: int
|
|
chats: List[ReplierChatSummary]
|
|
|
|
|
|
class PaginatedReplyLogs(BaseModel):
|
|
"""分页的回复日志列表"""
|
|
|
|
data: List[ReplyLogSummary]
|
|
total: int
|
|
page: int
|
|
page_size: int
|
|
chat_id: str
|
|
|
|
|
|
def parse_timestamp_from_filename(filename: str) -> float:
|
|
"""从文件名解析时间戳: 1766497488220_af92bdb1.json -> 1766497488.220"""
|
|
try:
|
|
timestamp_str = filename.split("_")[0]
|
|
# 时间戳是毫秒级,需要转换为秒
|
|
return float(timestamp_str) / 1000
|
|
except (ValueError, IndexError):
|
|
return 0
|
|
|
|
|
|
@router.get("/overview", response_model=ReplierOverview)
|
|
async def get_replier_overview():
|
|
"""
|
|
获取回复器总览 - 轻量级接口
|
|
只统计文件数量,不读取文件内容
|
|
"""
|
|
if not REPLY_LOG_DIR.exists():
|
|
return ReplierOverview(total_chats=0, total_replies=0, chats=[])
|
|
|
|
chats = []
|
|
total_replies = 0
|
|
|
|
for chat_dir in REPLY_LOG_DIR.iterdir():
|
|
if not chat_dir.is_dir():
|
|
continue
|
|
|
|
# 只统计json文件数量
|
|
json_files = list(chat_dir.glob("*.json"))
|
|
reply_count = len(json_files)
|
|
total_replies += reply_count
|
|
|
|
if reply_count == 0:
|
|
continue
|
|
|
|
# 从文件名获取最新时间戳
|
|
latest_file = max(json_files, key=lambda f: parse_timestamp_from_filename(f.name))
|
|
latest_timestamp = parse_timestamp_from_filename(latest_file.name)
|
|
|
|
chats.append(
|
|
ReplierChatSummary(
|
|
chat_id=chat_dir.name,
|
|
reply_count=reply_count,
|
|
latest_timestamp=latest_timestamp,
|
|
latest_filename=latest_file.name,
|
|
)
|
|
)
|
|
|
|
# 按最新时间戳排序
|
|
chats.sort(key=lambda x: x.latest_timestamp, reverse=True)
|
|
|
|
return ReplierOverview(total_chats=len(chats), total_replies=total_replies, chats=chats)
|
|
|
|
|
|
@router.get("/chat/{chat_id}/logs", response_model=PaginatedReplyLogs)
|
|
async def get_chat_reply_logs(
|
|
chat_id: str,
|
|
page: int = Query(1, ge=1),
|
|
page_size: int = Query(20, ge=1, le=100),
|
|
search: Optional[str] = Query(None, description="搜索关键词,匹配提示词内容"),
|
|
):
|
|
"""
|
|
获取指定聊天的回复日志列表(分页)
|
|
需要读取文件内容获取摘要信息
|
|
支持搜索提示词内容
|
|
"""
|
|
chat_dir = REPLY_LOG_DIR / chat_id
|
|
if not chat_dir.exists():
|
|
return PaginatedReplyLogs(data=[], total=0, page=page, page_size=page_size, chat_id=chat_id)
|
|
|
|
# 先获取所有文件并按时间戳排序
|
|
json_files = list(chat_dir.glob("*.json"))
|
|
json_files.sort(key=lambda f: parse_timestamp_from_filename(f.name), reverse=True)
|
|
|
|
# 如果有搜索关键词,需要过滤文件
|
|
if search:
|
|
search_lower = search.lower()
|
|
filtered_files = []
|
|
for log_file in json_files:
|
|
try:
|
|
with open(log_file, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
prompt = data.get("prompt", "")
|
|
if search_lower in prompt.lower():
|
|
filtered_files.append(log_file)
|
|
except Exception:
|
|
continue
|
|
json_files = filtered_files
|
|
|
|
total = len(json_files)
|
|
|
|
# 分页 - 只读取当前页的文件
|
|
offset = (page - 1) * page_size
|
|
page_files = json_files[offset : offset + page_size]
|
|
|
|
logs = []
|
|
for log_file in page_files:
|
|
try:
|
|
with open(log_file, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
output = data.get("output", "")
|
|
logs.append(
|
|
ReplyLogSummary(
|
|
chat_id=data.get("chat_id", chat_id),
|
|
timestamp=data.get("timestamp", parse_timestamp_from_filename(log_file.name)),
|
|
filename=log_file.name,
|
|
model=data.get("model", ""),
|
|
success=data.get("success", True),
|
|
llm_ms=data.get("timing", {}).get("llm_ms", 0),
|
|
overall_ms=data.get("timing", {}).get("overall_ms", 0),
|
|
output_preview=output[:100] if output else "",
|
|
)
|
|
)
|
|
except Exception:
|
|
# 文件读取失败时使用文件名信息
|
|
logs.append(
|
|
ReplyLogSummary(
|
|
chat_id=chat_id,
|
|
timestamp=parse_timestamp_from_filename(log_file.name),
|
|
filename=log_file.name,
|
|
model="",
|
|
success=False,
|
|
llm_ms=0,
|
|
overall_ms=0,
|
|
output_preview="[读取失败]",
|
|
)
|
|
)
|
|
|
|
return PaginatedReplyLogs(data=logs, total=total, page=page, page_size=page_size, chat_id=chat_id)
|
|
|
|
|
|
@router.get("/log/{chat_id}/{filename}", response_model=ReplyLogDetail)
|
|
async def get_reply_log_detail(chat_id: str, filename: str):
|
|
"""获取回复日志详情 - 按需加载完整内容"""
|
|
log_file = REPLY_LOG_DIR / chat_id / filename
|
|
if not log_file.exists():
|
|
raise HTTPException(status_code=404, detail="日志文件不存在")
|
|
|
|
try:
|
|
with open(log_file, "r", encoding="utf-8") as f:
|
|
data = json.load(f)
|
|
return ReplyLogDetail(
|
|
type=data.get("type", "reply"),
|
|
chat_id=data.get("chat_id", chat_id),
|
|
timestamp=data.get("timestamp", 0),
|
|
prompt=data.get("prompt", ""),
|
|
output=data.get("output", ""),
|
|
processed_output=data.get("processed_output", []),
|
|
model=data.get("model", ""),
|
|
reasoning=data.get("reasoning", ""),
|
|
think_level=data.get("think_level", 0),
|
|
timing=data.get("timing", {}),
|
|
error=data.get("error"),
|
|
success=data.get("success", True),
|
|
)
|
|
except Exception as e:
|
|
raise HTTPException(status_code=500, detail=f"读取日志失败: {str(e)}") from e
|
|
|
|
|
|
# ========== 兼容接口 ==========
|
|
|
|
|
|
@router.get("/stats")
|
|
async def get_replier_stats():
|
|
"""获取回复器统计信息"""
|
|
overview = await get_replier_overview()
|
|
|
|
# 获取最近10条回复的摘要
|
|
recent_replies = []
|
|
for chat in overview.chats[:5]: # 从最近5个聊天中获取
|
|
try:
|
|
chat_logs = await get_chat_reply_logs(chat.chat_id, page=1, page_size=2)
|
|
recent_replies.extend(chat_logs.data)
|
|
except Exception:
|
|
continue
|
|
|
|
# 按时间排序取前10
|
|
recent_replies.sort(key=lambda x: x.timestamp, reverse=True)
|
|
recent_replies = recent_replies[:10]
|
|
|
|
return {
|
|
"total_chats": overview.total_chats,
|
|
"total_replies": overview.total_replies,
|
|
"recent_replies": recent_replies,
|
|
}
|
|
|
|
|
|
@router.get("/chats")
|
|
async def get_replier_chat_list():
|
|
"""获取所有聊天ID列表"""
|
|
overview = await get_replier_overview()
|
|
return [chat.chat_id for chat in overview.chats]
|