重构绝大部分模块以适配新版本的数据库和数据模型,修复缺少依赖问题,更新 pyproject
This commit is contained in:
@@ -1,23 +1,25 @@
|
||||
"""麦麦 2025 年度总结 API 路由"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Cookie, Header
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime
|
||||
from sqlalchemy import func as fn
|
||||
from typing import Any, Optional
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from fastapi import APIRouter, Cookie, Depends, Header, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import desc, func
|
||||
from sqlmodel import col, select
|
||||
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import (
|
||||
LLMUsage,
|
||||
OnlineTime,
|
||||
Messages,
|
||||
ChatStreams,
|
||||
PersonInfo,
|
||||
Emoji,
|
||||
ActionRecord,
|
||||
Expression,
|
||||
ActionRecords,
|
||||
Images,
|
||||
Jargon,
|
||||
Messages,
|
||||
ModelUsage,
|
||||
OnlineTime,
|
||||
PersonInfo,
|
||||
)
|
||||
from src.common.logger import get_logger
|
||||
from src.webui.core import verify_auth_token_from_cookie_or_header
|
||||
|
||||
logger = get_logger("webui.annual_report")
|
||||
@@ -45,7 +47,7 @@ class TimeFootprintData(BaseModel):
|
||||
first_message_content: Optional[str] = Field(None, description="初次消息内容(截断)")
|
||||
busiest_day: Optional[str] = Field(None, description="最忙碌的一天")
|
||||
busiest_day_count: int = Field(0, description="最忙碌那天的消息数")
|
||||
hourly_distribution: List[int] = Field(default_factory=lambda: [0] * 24, description="24小时活跃分布")
|
||||
hourly_distribution: list[int] = Field(default_factory=lambda: [0] * 24, description="24小时活跃分布")
|
||||
midnight_chat_count: int = Field(0, description="深夜(0-4点)互动次数")
|
||||
is_night_owl: bool = Field(False, description="是否是夜猫子")
|
||||
|
||||
@@ -54,8 +56,8 @@ class SocialNetworkData(BaseModel):
|
||||
"""社交网络数据"""
|
||||
|
||||
total_groups: int = Field(0, description="加入的群组总数")
|
||||
top_groups: List[Dict[str, Any]] = Field(default_factory=list, description="话痨群组TOP5")
|
||||
top_users: List[Dict[str, Any]] = Field(default_factory=list, description="互动最多的用户TOP5")
|
||||
top_groups: list[dict[str, Any]] = Field(default_factory=list, description="话痨群组TOP5")
|
||||
top_users: list[dict[str, Any]] = Field(default_factory=list, description="互动最多的用户TOP5")
|
||||
at_count: int = Field(0, description="被@次数")
|
||||
mentioned_count: int = Field(0, description="被提及次数")
|
||||
longest_companion_user: Optional[str] = Field(None, description="最长情陪伴的用户")
|
||||
@@ -69,11 +71,11 @@ class BrainPowerData(BaseModel):
|
||||
total_cost: float = Field(0.0, description="年度总花费")
|
||||
favorite_model: Optional[str] = Field(None, description="最爱用的模型")
|
||||
favorite_model_count: int = Field(0, description="最爱模型的调用次数")
|
||||
model_distribution: List[Dict[str, Any]] = Field(default_factory=list, description="模型使用分布")
|
||||
top_reply_models: List[Dict[str, Any]] = Field(default_factory=list, description="最喜欢的回复模型TOP5")
|
||||
model_distribution: list[dict[str, Any]] = Field(default_factory=list, description="模型使用分布")
|
||||
top_reply_models: list[dict[str, Any]] = Field(default_factory=list, description="最喜欢的回复模型TOP5")
|
||||
most_expensive_cost: float = Field(0.0, description="最昂贵的一次思考花费")
|
||||
most_expensive_time: Optional[str] = Field(None, description="最昂贵思考的时间")
|
||||
top_token_consumers: List[Dict[str, Any]] = Field(default_factory=list, description="烧钱大户TOP3")
|
||||
top_token_consumers: list[dict[str, Any]] = Field(default_factory=list, description="烧钱大户TOP3")
|
||||
silence_rate: float = Field(0.0, description="高冷指数(沉默率)")
|
||||
total_actions: int = Field(0, description="总动作数")
|
||||
no_reply_count: int = Field(0, description="选择沉默的次数")
|
||||
@@ -88,23 +90,23 @@ class BrainPowerData(BaseModel):
|
||||
class ExpressionVibeData(BaseModel):
|
||||
"""个性与表达数据"""
|
||||
|
||||
top_emoji: Optional[Dict[str, Any]] = Field(None, description="表情包之王")
|
||||
top_emojis: List[Dict[str, Any]] = Field(default_factory=list, description="TOP3表情包")
|
||||
top_expressions: List[Dict[str, Any]] = Field(default_factory=list, description="印象最深刻的表达风格")
|
||||
top_emoji: Optional[dict[str, Any]] = Field(None, description="表情包之王")
|
||||
top_emojis: list[dict[str, Any]] = Field(default_factory=list, description="TOP3表情包")
|
||||
top_expressions: list[dict[str, Any]] = Field(default_factory=list, description="印象最深刻的表达风格")
|
||||
rejected_expression_count: int = Field(0, description="被拒绝的表达次数")
|
||||
checked_expression_count: int = Field(0, description="已检查的表达次数")
|
||||
total_expressions: int = Field(0, description="表达总数")
|
||||
action_types: List[Dict[str, Any]] = Field(default_factory=list, description="动作类型分布")
|
||||
action_types: list[dict[str, Any]] = Field(default_factory=list, description="动作类型分布")
|
||||
image_processed_count: int = Field(0, description="处理的图片数量")
|
||||
late_night_reply: Optional[Dict[str, Any]] = Field(None, description="深夜还在回复")
|
||||
favorite_reply: Optional[Dict[str, Any]] = Field(None, description="最喜欢的回复")
|
||||
late_night_reply: Optional[dict[str, Any]] = Field(None, description="深夜还在回复")
|
||||
favorite_reply: Optional[dict[str, Any]] = Field(None, description="最喜欢的回复")
|
||||
|
||||
|
||||
class AchievementData(BaseModel):
|
||||
"""趣味成就数据"""
|
||||
|
||||
new_jargon_count: int = Field(0, description="新学到的黑话数量")
|
||||
sample_jargons: List[Dict[str, Any]] = Field(default_factory=list, description="代表性黑话示例")
|
||||
sample_jargons: list[dict[str, Any]] = Field(default_factory=list, description="代表性黑话示例")
|
||||
total_messages: int = Field(0, description="总消息数")
|
||||
total_replies: int = Field(0, description="总回复数")
|
||||
|
||||
@@ -115,11 +117,11 @@ class AnnualReportData(BaseModel):
|
||||
year: int = Field(2025, description="报告年份")
|
||||
bot_name: str = Field("麦麦", description="Bot名称")
|
||||
generated_at: str = Field(..., description="报告生成时间")
|
||||
time_footprint: TimeFootprintData = Field(default_factory=TimeFootprintData)
|
||||
social_network: SocialNetworkData = Field(default_factory=SocialNetworkData)
|
||||
brain_power: BrainPowerData = Field(default_factory=BrainPowerData)
|
||||
expression_vibe: ExpressionVibeData = Field(default_factory=ExpressionVibeData)
|
||||
achievements: AchievementData = Field(default_factory=AchievementData)
|
||||
time_footprint: TimeFootprintData = Field(default_factory=lambda: TimeFootprintData.model_construct())
|
||||
social_network: SocialNetworkData = Field(default_factory=lambda: SocialNetworkData.model_construct())
|
||||
brain_power: BrainPowerData = Field(default_factory=lambda: BrainPowerData.model_construct())
|
||||
expression_vibe: ExpressionVibeData = Field(default_factory=lambda: ExpressionVibeData.model_construct())
|
||||
achievements: AchievementData = Field(default_factory=lambda: AchievementData.model_construct())
|
||||
|
||||
|
||||
# ==================== 辅助函数 ====================
|
||||
@@ -144,15 +146,18 @@ def get_year_datetime_range(year: int = 2025) -> tuple[datetime, datetime]:
|
||||
|
||||
async def get_time_footprint(year: int = 2025) -> TimeFootprintData:
|
||||
"""获取时光足迹数据"""
|
||||
data = TimeFootprintData()
|
||||
data = TimeFootprintData.model_construct()
|
||||
start_ts, end_ts = get_year_time_range(year)
|
||||
start_dt, end_dt = get_year_datetime_range(year)
|
||||
|
||||
try:
|
||||
# 1. 年度在线时长
|
||||
online_records = list(
|
||||
OnlineTime.select().where((OnlineTime.start_timestamp >= start_dt) | (OnlineTime.end_timestamp <= end_dt))
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(OnlineTime).where(
|
||||
col(OnlineTime.start_timestamp) >= start_dt,
|
||||
col(OnlineTime.end_timestamp) <= end_dt,
|
||||
)
|
||||
online_records = session.exec(statement).all()
|
||||
total_seconds = 0
|
||||
for record in online_records:
|
||||
try:
|
||||
@@ -165,50 +170,66 @@ async def get_time_footprint(year: int = 2025) -> TimeFootprintData:
|
||||
data.total_online_hours = round(total_seconds / 3600, 2)
|
||||
|
||||
# 2. 初次相遇 - 年度第一条消息
|
||||
first_msg = (
|
||||
Messages.select()
|
||||
.where((Messages.time >= start_ts) & (Messages.time <= end_ts))
|
||||
.order_by(Messages.time.asc())
|
||||
.first()
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(Messages)
|
||||
.where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
.order_by(col(Messages.timestamp).asc())
|
||||
.limit(1)
|
||||
)
|
||||
first_msg = session.exec(statement).first()
|
||||
if first_msg:
|
||||
data.first_message_time = datetime.fromtimestamp(first_msg.time).strftime("%Y-%m-%d %H:%M:%S")
|
||||
data.first_message_time = first_msg.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
data.first_message_user = first_msg.user_nickname or first_msg.user_id or "未知用户"
|
||||
content = first_msg.processed_plain_text or first_msg.display_message or ""
|
||||
data.first_message_content = content[:50] + "..." if len(content) > 50 else content
|
||||
|
||||
# 3. 最忙碌的一天
|
||||
# 使用 SQLite 的 date 函数按日期分组
|
||||
busiest_query = (
|
||||
Messages.select(
|
||||
fn.date(Messages.time, "unixepoch").alias("day"),
|
||||
fn.COUNT(Messages.id).alias("count"),
|
||||
day_expr = func.date(col(Messages.timestamp))
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(
|
||||
day_expr.label("day"),
|
||||
func.count().label("count"),
|
||||
)
|
||||
.where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
.group_by(day_expr)
|
||||
.order_by(func.count().desc())
|
||||
.limit(1)
|
||||
)
|
||||
.where((Messages.time >= start_ts) & (Messages.time <= end_ts))
|
||||
.group_by(fn.date(Messages.time, "unixepoch"))
|
||||
.order_by(fn.COUNT(Messages.id).desc())
|
||||
.limit(1)
|
||||
)
|
||||
busiest_result = list(busiest_query.dicts())
|
||||
busiest_result = session.exec(statement).all()
|
||||
if busiest_result:
|
||||
data.busiest_day = busiest_result[0].get("day")
|
||||
data.busiest_day_count = busiest_result[0].get("count", 0)
|
||||
data.busiest_day = busiest_result[0][0]
|
||||
data.busiest_day_count = busiest_result[0][1] or 0
|
||||
|
||||
# 4. 昼夜节律 - 24小时活跃分布
|
||||
hourly_query = (
|
||||
Messages.select(
|
||||
fn.strftime("%H", Messages.time, "unixepoch").alias("hour"),
|
||||
fn.COUNT(Messages.id).alias("count"),
|
||||
hour_expr = func.strftime("%H", col(Messages.timestamp))
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(
|
||||
hour_expr.label("hour"),
|
||||
func.count().label("count"),
|
||||
)
|
||||
.where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
.group_by(hour_expr)
|
||||
)
|
||||
.where((Messages.time >= start_ts) & (Messages.time <= end_ts))
|
||||
.group_by(fn.strftime("%H", Messages.time, "unixepoch"))
|
||||
)
|
||||
hourly_rows = session.exec(statement).all()
|
||||
hourly_distribution = [0] * 24
|
||||
for row in hourly_query.dicts():
|
||||
for row in hourly_rows:
|
||||
try:
|
||||
hour = int(row.get("hour", 0))
|
||||
hour = int(row[0] or 0)
|
||||
if 0 <= hour < 24:
|
||||
hourly_distribution[hour] = row.get("count", 0)
|
||||
hourly_distribution[hour] = row[1] or 0
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
data.hourly_distribution = hourly_distribution
|
||||
@@ -234,7 +255,7 @@ async def get_social_network(year: int = 2025) -> SocialNetworkData:
|
||||
"""获取社交网络数据"""
|
||||
from src.config.config import global_config
|
||||
|
||||
data = SocialNetworkData()
|
||||
data = SocialNetworkData.model_construct()
|
||||
start_ts, end_ts = get_year_time_range(year)
|
||||
|
||||
# 获取 bot 自身的 QQ 账号,用于过滤
|
||||
@@ -242,91 +263,110 @@ async def get_social_network(year: int = 2025) -> SocialNetworkData:
|
||||
|
||||
try:
|
||||
# 1. 加入的群组总数
|
||||
data.total_groups = ChatStreams.select().where(ChatStreams.group_id.is_null(False)).count()
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count(func.distinct(col(Messages.group_id)))).where(
|
||||
col(Messages.group_id).is_not(None),
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
data.total_groups = int(session.exec(statement).first() or 0)
|
||||
|
||||
# 2. 话痨群组 TOP3
|
||||
top_groups_query = (
|
||||
Messages.select(
|
||||
Messages.chat_info_group_id,
|
||||
Messages.chat_info_group_name,
|
||||
fn.COUNT(Messages.id).alias("count"),
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(
|
||||
col(Messages.group_id),
|
||||
func.max(col(Messages.group_name)).label("group_name"),
|
||||
func.count().label("count"),
|
||||
)
|
||||
.where(
|
||||
col(Messages.group_id).is_not(None),
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
.group_by(col(Messages.group_id))
|
||||
.order_by(func.count().desc())
|
||||
.limit(5)
|
||||
)
|
||||
.where(
|
||||
(Messages.time >= start_ts) & (Messages.time <= end_ts) & (Messages.chat_info_group_id.is_null(False))
|
||||
)
|
||||
.group_by(Messages.chat_info_group_id)
|
||||
.order_by(fn.COUNT(Messages.id).desc())
|
||||
.limit(5)
|
||||
)
|
||||
top_groups_rows = session.exec(statement).all()
|
||||
data.top_groups = [
|
||||
{
|
||||
"group_id": row["chat_info_group_id"],
|
||||
"group_name": row["chat_info_group_name"] or "未知群组",
|
||||
"message_count": row["count"],
|
||||
"is_webui": str(row["chat_info_group_id"]).startswith("webui_"),
|
||||
"group_id": row[0],
|
||||
"group_name": row[1] or "未知群组",
|
||||
"message_count": row[2] or 0,
|
||||
"is_webui": str(row[0]).startswith("webui_"),
|
||||
}
|
||||
for row in top_groups_query.dicts()
|
||||
for row in top_groups_rows
|
||||
]
|
||||
|
||||
# 3. 互动最多的用户 TOP5(过滤 bot 自身)
|
||||
top_users_query = (
|
||||
Messages.select(
|
||||
Messages.user_id,
|
||||
Messages.user_nickname,
|
||||
fn.COUNT(Messages.id).alias("count"),
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(
|
||||
col(Messages.user_id),
|
||||
func.max(col(Messages.user_nickname)).label("user_nickname"),
|
||||
func.count().label("count"),
|
||||
)
|
||||
.where(
|
||||
col(Messages.user_id).is_not(None),
|
||||
col(Messages.user_id) != bot_qq,
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
.group_by(col(Messages.user_id))
|
||||
.order_by(func.count().desc())
|
||||
.limit(5)
|
||||
)
|
||||
.where(
|
||||
(Messages.time >= start_ts)
|
||||
& (Messages.time <= end_ts)
|
||||
& (Messages.user_id.is_null(False))
|
||||
& (Messages.user_id != bot_qq) # 过滤 bot 自身
|
||||
)
|
||||
.group_by(Messages.user_id)
|
||||
.order_by(fn.COUNT(Messages.id).desc())
|
||||
.limit(5)
|
||||
)
|
||||
top_users_rows = session.exec(statement).all()
|
||||
data.top_users = [
|
||||
{
|
||||
"user_id": row["user_id"],
|
||||
"user_nickname": row["user_nickname"] or "未知用户",
|
||||
"message_count": row["count"],
|
||||
"is_webui": str(row["user_id"]).startswith("webui_"),
|
||||
"user_id": row[0],
|
||||
"user_nickname": row[1] or "未知用户",
|
||||
"message_count": row[2] or 0,
|
||||
"is_webui": str(row[0]).startswith("webui_"),
|
||||
}
|
||||
for row in top_users_query.dicts()
|
||||
for row in top_users_rows
|
||||
]
|
||||
|
||||
# 4. 被@次数
|
||||
data.at_count = (
|
||||
Messages.select()
|
||||
.where((Messages.time >= start_ts) & (Messages.time <= end_ts) & (Messages.is_at == True))
|
||||
.count()
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(Messages.is_at) == True,
|
||||
)
|
||||
data.at_count = int(session.exec(statement).first() or 0)
|
||||
|
||||
# 5. 被提及次数
|
||||
data.mentioned_count = (
|
||||
Messages.select()
|
||||
.where((Messages.time >= start_ts) & (Messages.time <= end_ts) & (Messages.is_mentioned == True))
|
||||
.count()
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(Messages.is_mentioned) == True,
|
||||
)
|
||||
data.mentioned_count = int(session.exec(statement).first() or 0)
|
||||
|
||||
# 6. 最长情陪伴的用户(过滤 bot 自身)
|
||||
companion_query = (
|
||||
ChatStreams.select(
|
||||
ChatStreams.user_id,
|
||||
ChatStreams.user_nickname,
|
||||
(ChatStreams.last_active_time - ChatStreams.create_time).alias("duration"),
|
||||
with get_db_session() as session:
|
||||
statement = select(PersonInfo).where(
|
||||
col(PersonInfo.user_id) != bot_qq,
|
||||
col(PersonInfo.first_known_time).is_not(None),
|
||||
col(PersonInfo.last_known_time).is_not(None),
|
||||
)
|
||||
.where(
|
||||
(ChatStreams.user_id.is_null(False)) & (ChatStreams.user_id != bot_qq) # 过滤 bot 自身
|
||||
)
|
||||
.order_by((ChatStreams.last_active_time - ChatStreams.create_time).desc())
|
||||
.limit(1)
|
||||
)
|
||||
companion_result = list(companion_query.dicts())
|
||||
if companion_result:
|
||||
data.longest_companion_user = companion_result[0].get("user_nickname") or "未知用户"
|
||||
duration = companion_result[0].get("duration", 0) or 0
|
||||
data.longest_companion_days = int(duration / 86400) # 转换为天
|
||||
persons = session.exec(statement).all()
|
||||
if persons:
|
||||
|
||||
def _companion_days(person: PersonInfo) -> float:
|
||||
if not person.first_known_time or not person.last_known_time:
|
||||
return 0.0
|
||||
return (person.last_known_time - person.first_known_time).total_seconds()
|
||||
|
||||
longest = max(persons, key=_companion_days)
|
||||
data.longest_companion_user = longest.person_name or longest.user_nickname or longest.user_id
|
||||
data.longest_companion_days = int(_companion_days(longest) / 86400)
|
||||
else:
|
||||
data.longest_companion_user = None
|
||||
data.longest_companion_days = 0
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取社交网络数据失败: {e}")
|
||||
@@ -339,154 +379,139 @@ async def get_social_network(year: int = 2025) -> SocialNetworkData:
|
||||
|
||||
async def get_brain_power(year: int = 2025) -> BrainPowerData:
|
||||
"""获取最强大脑数据"""
|
||||
data = BrainPowerData()
|
||||
data = BrainPowerData.model_construct()
|
||||
start_dt, end_dt = get_year_datetime_range(year)
|
||||
start_ts, end_ts = get_year_time_range(year)
|
||||
|
||||
try:
|
||||
# 1. 年度消耗 Token 总量和总花费
|
||||
token_query = LLMUsage.select(
|
||||
fn.COALESCE(fn.SUM(LLMUsage.total_tokens), 0).alias("total_tokens"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.cost), 0).alias("total_cost"),
|
||||
).where((LLMUsage.timestamp >= start_dt) & (LLMUsage.timestamp <= end_dt))
|
||||
result = token_query.dicts().get()
|
||||
data.total_tokens = int(result.get("total_tokens", 0) or 0)
|
||||
data.total_cost = round(float(result.get("total_cost", 0) or 0), 4)
|
||||
with get_db_session() as session:
|
||||
statement = select(
|
||||
func.sum(col(ModelUsage.total_tokens)).label("total_tokens"),
|
||||
func.sum(col(ModelUsage.cost)).label("total_cost"),
|
||||
).where(col(ModelUsage.timestamp) >= start_dt, col(ModelUsage.timestamp) <= end_dt)
|
||||
result = session.exec(statement).first()
|
||||
if result:
|
||||
data.total_tokens = int(result[0] or 0)
|
||||
data.total_cost = round(float(result[1] or 0), 4)
|
||||
|
||||
# 2. 最爱用的模型
|
||||
model_query = (
|
||||
LLMUsage.select(
|
||||
fn.COALESCE(LLMUsage.model_assign_name, LLMUsage.model_name).alias("model"),
|
||||
fn.COUNT(LLMUsage.id).alias("count"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.total_tokens), 0).alias("tokens"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.cost), 0).alias("cost"),
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(ModelUsage)
|
||||
.where(col(ModelUsage.timestamp) >= start_dt, col(ModelUsage.timestamp) <= end_dt)
|
||||
.order_by(desc(col(ModelUsage.timestamp)))
|
||||
)
|
||||
.where((LLMUsage.timestamp >= start_dt) & (LLMUsage.timestamp <= end_dt))
|
||||
.group_by(fn.COALESCE(LLMUsage.model_assign_name, LLMUsage.model_name))
|
||||
.order_by(fn.COUNT(LLMUsage.id).desc())
|
||||
.limit(10)
|
||||
)
|
||||
model_results = list(model_query.dicts())
|
||||
records = session.exec(statement).all()
|
||||
|
||||
model_agg: dict[str, dict[str, float | int]] = {}
|
||||
for record in records:
|
||||
model_name = record.model_assign_name or record.model_name or "unknown"
|
||||
if model_name not in model_agg:
|
||||
model_agg[model_name] = {"count": 0, "tokens": 0, "cost": 0.0}
|
||||
bucket = model_agg[model_name]
|
||||
bucket["count"] = int(bucket["count"]) + 1
|
||||
bucket["tokens"] = int(bucket["tokens"]) + int(record.total_tokens or 0)
|
||||
bucket["cost"] = float(bucket["cost"]) + float(record.cost or 0.0)
|
||||
|
||||
model_results = sorted(
|
||||
model_agg.items(),
|
||||
key=lambda item: float(item[1]["count"]),
|
||||
reverse=True,
|
||||
)[:10]
|
||||
if model_results:
|
||||
data.favorite_model = model_results[0].get("model")
|
||||
data.favorite_model_count = model_results[0].get("count", 0)
|
||||
data.favorite_model = model_results[0][0]
|
||||
data.favorite_model_count = int(model_results[0][1]["count"])
|
||||
data.model_distribution = [
|
||||
{
|
||||
"model": row["model"],
|
||||
"count": row["count"],
|
||||
"tokens": row["tokens"],
|
||||
"cost": round(row["cost"], 4),
|
||||
"model": model_name,
|
||||
"count": int(bucket["count"]),
|
||||
"tokens": int(bucket["tokens"]),
|
||||
"cost": round(float(bucket["cost"]), 4),
|
||||
}
|
||||
for row in model_results
|
||||
for model_name, bucket in model_results
|
||||
]
|
||||
|
||||
# 3. 最昂贵的一次思考
|
||||
expensive_query = (
|
||||
LLMUsage.select(LLMUsage.cost, LLMUsage.timestamp)
|
||||
.where((LLMUsage.timestamp >= start_dt) & (LLMUsage.timestamp <= end_dt))
|
||||
.order_by(LLMUsage.cost.desc())
|
||||
.limit(1)
|
||||
)
|
||||
expensive_result = expensive_query.first()
|
||||
if expensive_result:
|
||||
data.most_expensive_cost = round(expensive_result.cost or 0, 4)
|
||||
data.most_expensive_time = expensive_result.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if records:
|
||||
expensive_record = max(records, key=lambda record: record.cost or 0.0)
|
||||
data.most_expensive_cost = round(expensive_record.cost or 0.0, 4)
|
||||
data.most_expensive_time = expensive_record.timestamp.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 4. 烧钱大户 TOP3 (按用户,过滤 system)
|
||||
consumer_query = (
|
||||
LLMUsage.select(
|
||||
LLMUsage.user_id,
|
||||
fn.COALESCE(fn.SUM(LLMUsage.cost), 0).alias("cost"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.total_tokens), 0).alias("tokens"),
|
||||
)
|
||||
.where(
|
||||
(LLMUsage.timestamp >= start_dt)
|
||||
& (LLMUsage.timestamp <= end_dt)
|
||||
& (LLMUsage.user_id != "system") # 过滤 system 用户
|
||||
& (LLMUsage.user_id.is_null(False))
|
||||
)
|
||||
.group_by(LLMUsage.user_id)
|
||||
.order_by(fn.SUM(LLMUsage.cost).desc())
|
||||
.limit(3)
|
||||
)
|
||||
consumer_agg: dict[str, dict[str, float | int]] = {}
|
||||
for record in records:
|
||||
user_id = record.model_api_provider_name
|
||||
if not user_id or user_id == "system":
|
||||
continue
|
||||
if user_id not in consumer_agg:
|
||||
consumer_agg[user_id] = {"cost": 0.0, "tokens": 0}
|
||||
bucket = consumer_agg[user_id]
|
||||
bucket["cost"] = float(bucket["cost"]) + float(record.cost or 0.0)
|
||||
bucket["tokens"] = int(bucket["tokens"]) + int(record.total_tokens or 0)
|
||||
|
||||
data.top_token_consumers = [
|
||||
{
|
||||
"user_id": row["user_id"],
|
||||
"cost": round(row["cost"], 4),
|
||||
"tokens": row["tokens"],
|
||||
"user_id": user_id,
|
||||
"cost": round(float(bucket["cost"]), 4),
|
||||
"tokens": int(bucket["tokens"]),
|
||||
}
|
||||
for row in consumer_query.dicts()
|
||||
for user_id, bucket in sorted(
|
||||
consumer_agg.items(),
|
||||
key=lambda item: float(item[1]["cost"]),
|
||||
reverse=True,
|
||||
)[:3]
|
||||
]
|
||||
|
||||
# 5. 最喜欢的回复模型 TOP5(按模型的回复次数统计,只统计 replyer 调用)
|
||||
# 假设 replyer 调用有特定的 model_assign_name 格式或可以通过某种方式识别
|
||||
reply_model_query = (
|
||||
LLMUsage.select(
|
||||
fn.COALESCE(LLMUsage.model_assign_name, LLMUsage.model_name).alias("model"),
|
||||
fn.COUNT(LLMUsage.id).alias("count"),
|
||||
)
|
||||
.where(
|
||||
(LLMUsage.timestamp >= start_dt)
|
||||
& (LLMUsage.timestamp <= end_dt)
|
||||
& (
|
||||
LLMUsage.model_assign_name.contains("replyer")
|
||||
| LLMUsage.model_assign_name.contains("回复")
|
||||
| LLMUsage.model_assign_name.is_null(True) # 包含没有 assign_name 的情况
|
||||
)
|
||||
)
|
||||
.group_by(fn.COALESCE(LLMUsage.model_assign_name, LLMUsage.model_name))
|
||||
.order_by(fn.COUNT(LLMUsage.id).desc())
|
||||
.limit(5)
|
||||
)
|
||||
data.top_reply_models = [{"model": row["model"], "count": row["count"]} for row in reply_model_query.dicts()]
|
||||
reply_model_agg: dict[str, int] = {}
|
||||
for record in records:
|
||||
model_assign_name = record.model_assign_name or ""
|
||||
if "replyer" not in model_assign_name and "回复" not in model_assign_name:
|
||||
continue
|
||||
model_name = model_assign_name or record.model_name or "unknown"
|
||||
reply_model_agg[model_name] = reply_model_agg.get(model_name, 0) + 1
|
||||
data.top_reply_models = [
|
||||
{"model": model_name, "count": count}
|
||||
for model_name, count in sorted(reply_model_agg.items(), key=lambda item: item[1], reverse=True)[:5]
|
||||
]
|
||||
|
||||
# 6. 高冷指数 (沉默率) - 基于 ActionRecords
|
||||
total_actions = (
|
||||
ActionRecords.select().where((ActionRecords.time >= start_ts) & (ActionRecords.time <= end_ts)).count()
|
||||
)
|
||||
no_reply_count = (
|
||||
ActionRecords.select()
|
||||
.where(
|
||||
(ActionRecords.time >= start_ts)
|
||||
& (ActionRecords.time <= end_ts)
|
||||
& (ActionRecords.action_name == "no_reply")
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(ActionRecord.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(ActionRecord.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
.count()
|
||||
)
|
||||
total_actions = int(session.exec(statement).first() or 0)
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(ActionRecord.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(ActionRecord.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(ActionRecord.action_name) == "no_reply",
|
||||
)
|
||||
no_reply_count = int(session.exec(statement).first() or 0)
|
||||
data.total_actions = total_actions
|
||||
data.no_reply_count = no_reply_count
|
||||
data.silence_rate = round(no_reply_count / total_actions * 100, 2) if total_actions > 0 else 0
|
||||
|
||||
# 6. 情绪波动 (兴趣值)
|
||||
interest_query = Messages.select(
|
||||
fn.AVG(Messages.interest_value).alias("avg_interest"),
|
||||
fn.MAX(Messages.interest_value).alias("max_interest"),
|
||||
).where((Messages.time >= start_ts) & (Messages.time <= end_ts) & (Messages.interest_value.is_null(False)))
|
||||
interest_result = interest_query.dicts().get()
|
||||
data.avg_interest_value = round(float(interest_result.get("avg_interest") or 0), 2)
|
||||
data.max_interest_value = round(float(interest_result.get("max_interest") or 0), 2)
|
||||
data.avg_interest_value = 0.0
|
||||
data.max_interest_value = 0.0
|
||||
|
||||
# 找到最高兴趣值的时间
|
||||
if data.max_interest_value > 0:
|
||||
max_interest_msg = (
|
||||
Messages.select(Messages.time)
|
||||
.where(
|
||||
(Messages.time >= start_ts)
|
||||
& (Messages.time <= end_ts)
|
||||
& (Messages.interest_value == data.max_interest_value)
|
||||
)
|
||||
.first()
|
||||
)
|
||||
if max_interest_msg:
|
||||
data.max_interest_time = datetime.fromtimestamp(max_interest_msg.time).strftime("%Y-%m-%d %H:%M:%S")
|
||||
data.max_interest_time = None
|
||||
|
||||
# 7. 思考深度 (基于 action_reasoning 长度)
|
||||
reasoning_records = ActionRecords.select(ActionRecords.action_reasoning, ActionRecords.time).where(
|
||||
(ActionRecords.time >= start_ts)
|
||||
& (ActionRecords.time <= end_ts)
|
||||
& (ActionRecords.action_reasoning.is_null(False))
|
||||
& (ActionRecords.action_reasoning != "")
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(ActionRecord).where(
|
||||
col(ActionRecord.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(ActionRecord.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(ActionRecord.action_reasoning).is_not(None),
|
||||
col(ActionRecord.action_reasoning) != "",
|
||||
)
|
||||
reasoning_records = session.exec(statement).all()
|
||||
reasoning_lengths = []
|
||||
max_len = 0
|
||||
max_len_time = None
|
||||
@@ -496,13 +521,13 @@ async def get_brain_power(year: int = 2025) -> BrainPowerData:
|
||||
reasoning_lengths.append(length)
|
||||
if length > max_len:
|
||||
max_len = length
|
||||
max_len_time = record.time
|
||||
max_len_time = record.timestamp
|
||||
|
||||
if reasoning_lengths:
|
||||
data.avg_reasoning_length = round(sum(reasoning_lengths) / len(reasoning_lengths), 1)
|
||||
data.max_reasoning_length = max_len
|
||||
if max_len_time:
|
||||
data.max_reasoning_time = datetime.fromtimestamp(max_len_time).strftime("%Y-%m-%d %H:%M:%S")
|
||||
data.max_reasoning_time = max_len_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取最强大脑数据失败: {e}")
|
||||
@@ -517,7 +542,7 @@ async def get_expression_vibe(year: int = 2025) -> ExpressionVibeData:
|
||||
"""获取个性与表达数据"""
|
||||
from src.config.config import global_config
|
||||
|
||||
data = ExpressionVibeData()
|
||||
data = ExpressionVibeData.model_construct()
|
||||
start_ts, end_ts = get_year_time_range(year)
|
||||
|
||||
# 获取 bot 自身的 QQ 账号,用于筛选 bot 发送的消息
|
||||
@@ -525,75 +550,58 @@ async def get_expression_vibe(year: int = 2025) -> ExpressionVibeData:
|
||||
|
||||
try:
|
||||
# 1. 表情包之王 - 使用次数最多的表情包
|
||||
top_emoji_query = (
|
||||
Emoji.select(Emoji.id, Emoji.full_path, Emoji.description, Emoji.usage_count, Emoji.emoji_hash)
|
||||
.where(Emoji.is_registered == True)
|
||||
.order_by(Emoji.usage_count.desc())
|
||||
.limit(5)
|
||||
)
|
||||
top_emojis = list(top_emoji_query.dicts())
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(Images).where(col(Images.is_registered) == True).order_by(desc(col(Images.query_count))).limit(5)
|
||||
)
|
||||
top_emojis = session.exec(statement).all()
|
||||
if top_emojis:
|
||||
data.top_emoji = {
|
||||
"id": top_emojis[0].get("id"),
|
||||
"path": top_emojis[0].get("full_path"),
|
||||
"description": top_emojis[0].get("description"),
|
||||
"usage_count": top_emojis[0].get("usage_count", 0),
|
||||
"hash": top_emojis[0].get("emoji_hash"),
|
||||
"id": top_emojis[0].id,
|
||||
"path": top_emojis[0].full_path,
|
||||
"description": top_emojis[0].description,
|
||||
"usage_count": top_emojis[0].query_count,
|
||||
"hash": top_emojis[0].image_hash,
|
||||
}
|
||||
data.top_emojis = [
|
||||
{
|
||||
"id": e.get("id"),
|
||||
"path": e.get("full_path"),
|
||||
"description": e.get("description"),
|
||||
"usage_count": e.get("usage_count", 0),
|
||||
"hash": e.get("emoji_hash"),
|
||||
"id": e.id,
|
||||
"path": e.full_path,
|
||||
"description": e.description,
|
||||
"usage_count": e.query_count,
|
||||
"hash": e.image_hash,
|
||||
}
|
||||
for e in top_emojis
|
||||
]
|
||||
|
||||
# 2. 百变麦麦 - 最常用的表达风格
|
||||
expression_query = (
|
||||
Expression.select(
|
||||
Expression.style,
|
||||
fn.SUM(Expression.count).alias("total_count"),
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(Expression.style, func.sum(col(Expression.count)).label("total_count"))
|
||||
.where(
|
||||
col(Expression.last_active_time) >= datetime.fromtimestamp(start_ts),
|
||||
col(Expression.last_active_time) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
.group_by(Expression.style)
|
||||
.order_by(func.sum(col(Expression.count)).desc())
|
||||
.limit(5)
|
||||
)
|
||||
.where((Expression.last_active_time >= start_ts) & (Expression.last_active_time <= end_ts))
|
||||
.group_by(Expression.style)
|
||||
.order_by(fn.SUM(Expression.count).desc())
|
||||
.limit(5)
|
||||
)
|
||||
data.top_expressions = [
|
||||
{"style": row["style"], "count": row["total_count"]} for row in expression_query.dicts()
|
||||
]
|
||||
expression_rows = session.exec(statement).all()
|
||||
data.top_expressions = [{"style": row[0], "count": row[1] or 0} for row in expression_rows]
|
||||
|
||||
# 3. 被拒绝的表达
|
||||
data.rejected_expression_count = (
|
||||
Expression.select()
|
||||
.where(
|
||||
(Expression.last_active_time >= start_ts)
|
||||
& (Expression.last_active_time <= end_ts)
|
||||
& (Expression.rejected == True)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
data.rejected_expression_count = 0
|
||||
|
||||
# 4. 已检查的表达
|
||||
data.checked_expression_count = (
|
||||
Expression.select()
|
||||
.where(
|
||||
(Expression.last_active_time >= start_ts)
|
||||
& (Expression.last_active_time <= end_ts)
|
||||
& (Expression.checked == True)
|
||||
)
|
||||
.count()
|
||||
)
|
||||
data.checked_expression_count = 0
|
||||
|
||||
# 5. 表达总数
|
||||
data.total_expressions = (
|
||||
Expression.select()
|
||||
.where((Expression.last_active_time >= start_ts) & (Expression.last_active_time <= end_ts))
|
||||
.count()
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Expression.last_active_time) >= datetime.fromtimestamp(start_ts),
|
||||
col(Expression.last_active_time) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
data.total_expressions = int(session.exec(statement).first() or 0)
|
||||
|
||||
# 6. 动作类型分布 (过滤无意义的动作)
|
||||
# 过滤掉: no_reply_until_call, make_question, no_action, wait, complete_talk, listening, block_and_ignore
|
||||
@@ -608,28 +616,29 @@ async def get_expression_vibe(year: int = 2025) -> ExpressionVibeData:
|
||||
"listening",
|
||||
"block_and_ignore",
|
||||
]
|
||||
action_query = (
|
||||
ActionRecords.select(
|
||||
ActionRecords.action_name,
|
||||
fn.COUNT(ActionRecords.id).alias("count"),
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(ActionRecord.action_name, func.count().label("count"))
|
||||
.where(
|
||||
col(ActionRecord.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(ActionRecord.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(ActionRecord.action_name).not_in(excluded_actions),
|
||||
)
|
||||
.group_by(ActionRecord.action_name)
|
||||
.order_by(func.count().desc())
|
||||
.limit(10)
|
||||
)
|
||||
.where(
|
||||
(ActionRecords.time >= start_ts)
|
||||
& (ActionRecords.time <= end_ts)
|
||||
& (ActionRecords.action_name.not_in(excluded_actions))
|
||||
)
|
||||
.group_by(ActionRecords.action_name)
|
||||
.order_by(fn.COUNT(ActionRecords.id).desc())
|
||||
.limit(10)
|
||||
)
|
||||
data.action_types = [{"action": row["action_name"], "count": row["count"]} for row in action_query.dicts()]
|
||||
action_rows = session.exec(statement).all()
|
||||
data.action_types = [{"action": row[0], "count": row[1]} for row in action_rows]
|
||||
|
||||
# 7. 处理的图片数量
|
||||
data.image_processed_count = (
|
||||
Messages.select()
|
||||
.where((Messages.time >= start_ts) & (Messages.time <= end_ts) & (Messages.is_picid == True))
|
||||
.count()
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(Messages.is_picture) == True,
|
||||
)
|
||||
data.image_processed_count = int(session.exec(statement).first() or 0)
|
||||
|
||||
# 8. 深夜还在回复 (0-6点最晚的10条消息中随机抽取一条)
|
||||
import random
|
||||
@@ -648,21 +657,22 @@ async def get_expression_vibe(year: int = 2025) -> ExpressionVibeData:
|
||||
return content
|
||||
|
||||
# 使用 user_id 判断是否是 bot 发送的消息
|
||||
late_night_messages = list(
|
||||
Messages.select(
|
||||
Messages.time,
|
||||
Messages.processed_plain_text,
|
||||
Messages.display_message,
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(Messages)
|
||||
.where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(Messages.user_id) == bot_qq,
|
||||
)
|
||||
.order_by(desc(col(Messages.timestamp)))
|
||||
.limit(200)
|
||||
)
|
||||
.where(
|
||||
(Messages.time >= start_ts) & (Messages.time <= end_ts) & (Messages.user_id == bot_qq) # bot 发送的消息
|
||||
)
|
||||
.order_by(Messages.time.desc())
|
||||
)
|
||||
late_night_messages = session.exec(statement).all()
|
||||
# 筛选出0-6点的消息
|
||||
late_night_filtered = []
|
||||
for msg in late_night_messages:
|
||||
msg_dt = datetime.fromtimestamp(msg.time)
|
||||
msg_dt = msg.timestamp
|
||||
hour = msg_dt.hour
|
||||
if 0 <= hour < 6: # 0点到6点
|
||||
raw_content = msg.processed_plain_text or msg.display_message or ""
|
||||
@@ -671,7 +681,7 @@ async def get_expression_vibe(year: int = 2025) -> ExpressionVibeData:
|
||||
if cleaned_content and len(cleaned_content) > 2:
|
||||
late_night_filtered.append(
|
||||
{
|
||||
"time": msg.time,
|
||||
"time": msg_dt.timestamp(),
|
||||
"hour": hour,
|
||||
"minute": msg_dt.minute,
|
||||
"content": cleaned_content,
|
||||
@@ -693,13 +703,15 @@ async def get_expression_vibe(year: int = 2025) -> ExpressionVibeData:
|
||||
from collections import Counter
|
||||
import json as json_lib
|
||||
|
||||
reply_records = ActionRecords.select(ActionRecords.action_data).where(
|
||||
(ActionRecords.time >= start_ts)
|
||||
& (ActionRecords.time <= end_ts)
|
||||
& (ActionRecords.action_name == "reply")
|
||||
& (ActionRecords.action_data.is_null(False))
|
||||
& (ActionRecords.action_data != "")
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(ActionRecord).where(
|
||||
col(ActionRecord.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(ActionRecord.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(ActionRecord.action_name) == "reply",
|
||||
col(ActionRecord.action_data).is_not(None),
|
||||
col(ActionRecord.action_data) != "",
|
||||
)
|
||||
reply_records = session.exec(statement).all()
|
||||
|
||||
reply_contents = []
|
||||
for record in reply_records:
|
||||
@@ -762,21 +774,20 @@ async def get_expression_vibe(year: int = 2025) -> ExpressionVibeData:
|
||||
|
||||
async def get_achievements(year: int = 2025) -> AchievementData:
|
||||
"""获取趣味成就数据"""
|
||||
data = AchievementData()
|
||||
data = AchievementData.model_construct()
|
||||
start_ts, end_ts = get_year_time_range(year)
|
||||
|
||||
try:
|
||||
# 1. 新学到的黑话数量
|
||||
# Jargon 表没有时间字段,统计全部已确认的黑话
|
||||
data.new_jargon_count = Jargon.select().where(Jargon.is_jargon == True).count()
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(col(Jargon.is_jargon) == True)
|
||||
data.new_jargon_count = int(session.exec(statement).first() or 0)
|
||||
|
||||
# 2. 代表性黑话示例
|
||||
jargon_samples = (
|
||||
Jargon.select(Jargon.content, Jargon.meaning, Jargon.count)
|
||||
.where(Jargon.is_jargon == True)
|
||||
.order_by(Jargon.count.desc())
|
||||
.limit(5)
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(Jargon).where(col(Jargon.is_jargon) == True).order_by(desc(col(Jargon.count))).limit(5)
|
||||
jargon_samples = session.exec(statement).all()
|
||||
data.sample_jargons = [
|
||||
{
|
||||
"content": j.content,
|
||||
@@ -787,14 +798,21 @@ async def get_achievements(year: int = 2025) -> AchievementData:
|
||||
]
|
||||
|
||||
# 3. 总消息数
|
||||
data.total_messages = Messages.select().where((Messages.time >= start_ts) & (Messages.time <= end_ts)).count()
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
)
|
||||
data.total_messages = int(session.exec(statement).first() or 0)
|
||||
|
||||
# 4. 总回复数 (有 reply_to 的消息)
|
||||
data.total_replies = (
|
||||
Messages.select()
|
||||
.where((Messages.time >= start_ts) & (Messages.time <= end_ts) & (Messages.reply_to.is_null(False)))
|
||||
.count()
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Messages.timestamp) >= datetime.fromtimestamp(start_ts),
|
||||
col(Messages.timestamp) <= datetime.fromtimestamp(end_ts),
|
||||
col(Messages.reply_to).is_not(None),
|
||||
)
|
||||
data.total_replies = int(session.exec(statement).first() or 0)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取趣味成就数据失败: {e}")
|
||||
|
||||
@@ -7,16 +7,19 @@
|
||||
|
||||
import time
|
||||
import uuid
|
||||
from typing import Dict, Any, Optional, List
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect, Query, Depends, Cookie, Header
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import case, func as fn
|
||||
from typing import Any, Dict, List, Optional
|
||||
|
||||
from fastapi import APIRouter, Cookie, Depends, Header, Query, WebSocket, WebSocketDisconnect
|
||||
from pydantic import BaseModel
|
||||
from sqlalchemy import case, desc, func
|
||||
from sqlmodel import col, select, delete
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.common.database.database_model import Messages, PersonInfo
|
||||
from src.config.config import global_config
|
||||
from src.chat.message_receive.bot import chat_bot
|
||||
from src.webui.core import verify_auth_token_from_cookie_or_header, get_token_manager
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import Messages, PersonInfo
|
||||
from src.common.logger import get_logger
|
||||
from src.config.config import global_config
|
||||
from src.webui.core import get_token_manager, verify_auth_token_from_cookie_or_header
|
||||
from src.webui.routers.websocket.auth import verify_ws_token
|
||||
|
||||
logger = get_logger("webui.chat")
|
||||
@@ -97,7 +100,7 @@ class ChatHistoryManager:
|
||||
"id": msg.message_id,
|
||||
"type": "bot" if is_bot else "user",
|
||||
"content": msg.processed_plain_text or msg.display_message or "",
|
||||
"timestamp": msg.time,
|
||||
"timestamp": msg.timestamp.timestamp(),
|
||||
"sender_name": msg.user_nickname or (global_config.bot.nickname if is_bot else "未知用户"),
|
||||
"sender_id": "bot" if is_bot else user_id,
|
||||
"is_bot": is_bot,
|
||||
@@ -113,12 +116,14 @@ class ChatHistoryManager:
|
||||
target_group_id = group_id if group_id else WEBUI_CHAT_GROUP_ID
|
||||
try:
|
||||
# 查询指定群的消息,按时间排序
|
||||
messages = (
|
||||
Messages.select()
|
||||
.where(Messages.chat_info_group_id == target_group_id)
|
||||
.order_by(Messages.time.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(Messages)
|
||||
.where(col(Messages.group_id) == target_group_id)
|
||||
.order_by(desc(col(Messages.timestamp)))
|
||||
.limit(limit)
|
||||
)
|
||||
messages = session.exec(statement).all()
|
||||
|
||||
# 转换为列表并反转(使最旧的消息在前)
|
||||
# 传递 group_id 以便正确判断虚拟群中的机器人消息
|
||||
@@ -139,7 +144,10 @@ class ChatHistoryManager:
|
||||
"""
|
||||
target_group_id = group_id if group_id else WEBUI_CHAT_GROUP_ID
|
||||
try:
|
||||
deleted = Messages.delete().where(Messages.chat_info_group_id == target_group_id).execute()
|
||||
with get_db_session() as session:
|
||||
statement = delete(Messages).where(col(Messages.group_id) == target_group_id)
|
||||
result = session.exec(statement)
|
||||
deleted = result.rowcount or 0
|
||||
logger.info(f"已清空 {deleted} 条聊天记录 (group_id={target_group_id})")
|
||||
return deleted
|
||||
except Exception as e:
|
||||
@@ -172,14 +180,14 @@ class ChatConnectionManager:
|
||||
del self.user_sessions[user_id]
|
||||
logger.info(f"WebUI 聊天会话已断开: session={session_id}")
|
||||
|
||||
async def send_message(self, session_id: str, message: dict):
|
||||
async def send_message(self, session_id: str, message: dict[str, Any]):
|
||||
if session_id in self.active_connections:
|
||||
try:
|
||||
await self.active_connections[session_id].send_json(message)
|
||||
except Exception as e:
|
||||
logger.error(f"发送消息失败: {e}")
|
||||
|
||||
async def broadcast(self, message: dict):
|
||||
async def broadcast(self, message: dict[str, Any]):
|
||||
"""广播消息给所有连接"""
|
||||
for session_id in list(self.active_connections.keys()):
|
||||
await self.send_message(session_id, message)
|
||||
@@ -292,16 +300,18 @@ async def get_available_platforms(_auth: bool = Depends(require_auth)):
|
||||
"""
|
||||
try:
|
||||
# 查询所有不同的平台
|
||||
platforms = (
|
||||
PersonInfo.select(PersonInfo.platform, fn.COUNT(PersonInfo.id).alias("count"))
|
||||
.group_by(PersonInfo.platform)
|
||||
.order_by(fn.COUNT(PersonInfo.id).desc())
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(PersonInfo.platform, func.count().label("count"))
|
||||
.group_by(PersonInfo.platform)
|
||||
.order_by(func.count().desc())
|
||||
)
|
||||
platforms = session.exec(statement).all()
|
||||
|
||||
result = []
|
||||
for p in platforms:
|
||||
if p.platform: # 排除空平台
|
||||
result.append({"platform": p.platform, "count": p.count})
|
||||
for platform, count in platforms:
|
||||
if platform:
|
||||
result.append({"platform": platform, "count": count})
|
||||
|
||||
return {"success": True, "platforms": result}
|
||||
except Exception as e:
|
||||
@@ -325,31 +335,36 @@ async def get_persons_by_platform(
|
||||
"""
|
||||
try:
|
||||
# 构建查询
|
||||
query = PersonInfo.select().where(PersonInfo.platform == platform)
|
||||
statement = select(PersonInfo).where(col(PersonInfo.platform) == platform)
|
||||
|
||||
# 搜索过滤
|
||||
if search:
|
||||
query = query.where(
|
||||
(PersonInfo.person_name.contains(search))
|
||||
| (PersonInfo.nickname.contains(search))
|
||||
| (PersonInfo.user_id.contains(search))
|
||||
statement = statement.where(
|
||||
(col(PersonInfo.person_name).contains(search))
|
||||
| (col(PersonInfo.user_nickname).contains(search))
|
||||
| (col(PersonInfo.user_id).contains(search))
|
||||
)
|
||||
|
||||
# 按最后交互时间排序,优先显示活跃用户
|
||||
query = query.order_by(case((PersonInfo.last_know.is_null(), 1), else_=0), PersonInfo.last_know.desc())
|
||||
query = query.limit(limit)
|
||||
statement = statement.order_by(
|
||||
case((col(PersonInfo.last_known_time).is_(None), 1), else_=0),
|
||||
col(PersonInfo.last_known_time).desc(),
|
||||
).limit(limit)
|
||||
|
||||
with get_db_session() as session:
|
||||
persons = session.exec(statement).all()
|
||||
|
||||
result = []
|
||||
for person in query:
|
||||
for person in persons:
|
||||
result.append(
|
||||
{
|
||||
"person_id": person.person_id,
|
||||
"user_id": person.user_id,
|
||||
"person_name": person.person_name,
|
||||
"nickname": person.nickname,
|
||||
"nickname": person.user_nickname,
|
||||
"is_known": person.is_known,
|
||||
"platform": person.platform,
|
||||
"display_name": person.person_name or person.nickname or person.user_id,
|
||||
"display_name": person.person_name or person.user_nickname or person.user_id,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -448,7 +463,9 @@ async def websocket_chat(
|
||||
# 如果 URL 参数中提供了虚拟身份信息,自动配置
|
||||
if platform and person_id:
|
||||
try:
|
||||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||||
with get_db_session() as session:
|
||||
statement = select(PersonInfo).where(col(PersonInfo.person_id) == person_id).limit(1)
|
||||
person = session.exec(statement).first()
|
||||
if person:
|
||||
# 使用前端传递的 group_id,如果没有则生成一个稳定的
|
||||
virtual_group_id = group_id or f"{VIRTUAL_GROUP_ID_PREFIX}{platform}_{person.user_id}"
|
||||
@@ -457,7 +474,7 @@ async def websocket_chat(
|
||||
platform=person.platform,
|
||||
person_id=person.person_id,
|
||||
user_id=person.user_id,
|
||||
user_nickname=person.person_name or person.nickname or person.user_id,
|
||||
user_nickname=person.person_name or person.user_nickname or person.user_id,
|
||||
group_id=virtual_group_id,
|
||||
group_name=group_name or "WebUI虚拟群聊",
|
||||
)
|
||||
@@ -471,7 +488,7 @@ async def websocket_chat(
|
||||
|
||||
try:
|
||||
# 构建会话信息
|
||||
session_info_data = {
|
||||
session_info_data: dict[str, Any] = {
|
||||
"type": "session_info",
|
||||
"session_id": session_id,
|
||||
"user_id": user_id,
|
||||
@@ -641,7 +658,13 @@ async def websocket_chat(
|
||||
|
||||
# 获取用户信息
|
||||
try:
|
||||
person = PersonInfo.get_or_none(PersonInfo.person_id == virtual_data.get("person_id"))
|
||||
with get_db_session() as session:
|
||||
statement = (
|
||||
select(PersonInfo)
|
||||
.where(col(PersonInfo.person_id) == virtual_data.get("person_id"))
|
||||
.limit(1)
|
||||
)
|
||||
person = session.exec(statement).first()
|
||||
if not person:
|
||||
await chat_manager.send_message(
|
||||
session_id,
|
||||
@@ -665,7 +688,7 @@ async def websocket_chat(
|
||||
platform=person.platform,
|
||||
person_id=person.person_id,
|
||||
user_id=person.user_id,
|
||||
user_nickname=person.person_name or person.nickname or person.user_id,
|
||||
user_nickname=person.person_name or person.user_nickname or person.user_id,
|
||||
group_id=group_id,
|
||||
group_name=virtual_data.get("group_name", "WebUI虚拟群聊"),
|
||||
)
|
||||
@@ -769,7 +792,7 @@ async def get_chat_info(_auth: bool = Depends(require_auth)):
|
||||
}
|
||||
|
||||
|
||||
def get_webui_chat_broadcaster() -> tuple:
|
||||
def get_webui_chat_broadcaster() -> tuple[ChatConnectionManager, str]:
|
||||
"""获取 WebUI 聊天广播器,供外部模块使用
|
||||
|
||||
Returns:
|
||||
|
||||
@@ -3,11 +3,16 @@
|
||||
from fastapi import APIRouter, HTTPException, Header, Query, Cookie
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict
|
||||
from sqlalchemy import case
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
from sqlalchemy import case, func
|
||||
from sqlmodel import col, select, delete
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.common.database.database_model import Expression, ChatStreams
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import Expression
|
||||
from src.chat.message_receive.chat_stream import get_chat_manager
|
||||
from src.webui.core import verify_auth_token_from_cookie_or_header
|
||||
import time
|
||||
|
||||
logger = get_logger("webui.expression")
|
||||
|
||||
@@ -98,30 +103,32 @@ def verify_auth_token(
|
||||
|
||||
def expression_to_response(expression: Expression) -> ExpressionResponse:
|
||||
"""将 Expression 模型转换为响应对象"""
|
||||
last_active_time = expression.last_active_time.timestamp() if expression.last_active_time else 0.0
|
||||
create_date = expression.create_time.timestamp() if expression.create_time else None
|
||||
return ExpressionResponse(
|
||||
id=expression.id,
|
||||
id=expression.id if expression.id is not None else 0,
|
||||
situation=expression.situation,
|
||||
style=expression.style,
|
||||
last_active_time=expression.last_active_time,
|
||||
chat_id=expression.chat_id,
|
||||
create_date=expression.create_date,
|
||||
checked=expression.checked,
|
||||
rejected=expression.rejected,
|
||||
modified_by=expression.modified_by,
|
||||
last_active_time=last_active_time,
|
||||
chat_id=expression.session_id or "",
|
||||
create_date=create_date,
|
||||
checked=False,
|
||||
rejected=False,
|
||||
modified_by=None,
|
||||
)
|
||||
|
||||
|
||||
def get_chat_name(chat_id: str) -> str:
|
||||
"""根据 chat_id 获取聊天名称"""
|
||||
try:
|
||||
chat_stream = ChatStreams.get_or_none(ChatStreams.stream_id == chat_id)
|
||||
if chat_stream:
|
||||
# 优先使用群聊名称,否则使用用户昵称
|
||||
if chat_stream.group_name:
|
||||
return chat_stream.group_name
|
||||
elif chat_stream.user_nickname:
|
||||
return chat_stream.user_nickname
|
||||
return chat_id # 找不到时返回原始ID
|
||||
chat_stream = get_chat_manager().get_stream(chat_id)
|
||||
if not chat_stream:
|
||||
return chat_id
|
||||
if chat_stream.group_info and chat_stream.group_info.group_name:
|
||||
return chat_stream.group_info.group_name
|
||||
if chat_stream.user_info and chat_stream.user_info.user_nickname:
|
||||
return chat_stream.user_info.user_nickname
|
||||
return chat_id
|
||||
except Exception:
|
||||
return chat_id
|
||||
|
||||
@@ -130,12 +137,15 @@ def get_chat_names_batch(chat_ids: List[str]) -> Dict[str, str]:
|
||||
"""批量获取聊天名称"""
|
||||
result = {cid: cid for cid in chat_ids} # 默认值为原始ID
|
||||
try:
|
||||
chat_streams = ChatStreams.select().where(ChatStreams.stream_id.in_(chat_ids))
|
||||
for cs in chat_streams:
|
||||
if cs.group_name:
|
||||
result[cs.stream_id] = cs.group_name
|
||||
elif cs.user_nickname:
|
||||
result[cs.stream_id] = cs.user_nickname
|
||||
chat_manager = get_chat_manager()
|
||||
for chat_id in chat_ids:
|
||||
chat_stream = chat_manager.get_stream(chat_id)
|
||||
if not chat_stream:
|
||||
continue
|
||||
if chat_stream.group_info and chat_stream.group_info.group_name:
|
||||
result[chat_id] = chat_stream.group_info.group_name
|
||||
elif chat_stream.user_info and chat_stream.user_info.user_nickname:
|
||||
result[chat_id] = chat_stream.user_info.user_nickname
|
||||
except Exception as e:
|
||||
logger.warning(f"批量获取聊天名称失败: {e}")
|
||||
return result
|
||||
@@ -172,14 +182,17 @@ async def get_chat_list(maibot_session: Optional[str] = Cookie(None), authorizat
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
chat_list = []
|
||||
for cs in ChatStreams.select():
|
||||
chat_name = cs.group_name if cs.group_name else (cs.user_nickname if cs.user_nickname else cs.stream_id)
|
||||
for stream_id, stream in get_chat_manager().streams.items():
|
||||
chat_name = stream.group_info.group_name if stream.group_info and stream.group_info.group_name else None
|
||||
if not chat_name and stream.user_info and stream.user_info.user_nickname:
|
||||
chat_name = stream.user_info.user_nickname
|
||||
chat_name = chat_name or stream_id
|
||||
chat_list.append(
|
||||
ChatInfo(
|
||||
chat_id=cs.stream_id,
|
||||
chat_id=stream_id,
|
||||
chat_name=chat_name,
|
||||
platform=cs.platform,
|
||||
is_group=bool(cs.group_id),
|
||||
platform=stream.platform,
|
||||
is_group=bool(stream.group_info and stream.group_info.group_id),
|
||||
)
|
||||
)
|
||||
|
||||
@@ -221,29 +234,39 @@ async def get_expression_list(
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
# 构建查询
|
||||
query = Expression.select()
|
||||
statement = select(Expression)
|
||||
|
||||
# 搜索过滤
|
||||
if search:
|
||||
query = query.where((Expression.situation.contains(search)) | (Expression.style.contains(search)))
|
||||
statement = statement.where(
|
||||
(col(Expression.situation).contains(search)) | (col(Expression.style).contains(search))
|
||||
)
|
||||
|
||||
# 聊天ID过滤
|
||||
if chat_id:
|
||||
query = query.where(Expression.chat_id == chat_id)
|
||||
statement = statement.where(col(Expression.session_id) == chat_id)
|
||||
|
||||
# 排序:最后活跃时间倒序(NULL 值放在最后)
|
||||
query = query.order_by(
|
||||
case((Expression.last_active_time.is_null(), 1), else_=0), Expression.last_active_time.desc()
|
||||
statement = statement.order_by(
|
||||
case((col(Expression.last_active_time).is_(None), 1), else_=0),
|
||||
col(Expression.last_active_time).desc(),
|
||||
)
|
||||
|
||||
# 获取总数
|
||||
total = query.count()
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * page_size
|
||||
expressions = query.offset(offset).limit(page_size)
|
||||
statement = statement.offset(offset).limit(page_size)
|
||||
|
||||
with get_db_session() as session:
|
||||
expressions = session.exec(statement).all()
|
||||
|
||||
count_statement = select(Expression.id)
|
||||
if search:
|
||||
count_statement = count_statement.where(
|
||||
(col(Expression.situation).contains(search)) | (col(Expression.style).contains(search))
|
||||
)
|
||||
if chat_id:
|
||||
count_statement = count_statement.where(col(Expression.session_id) == chat_id)
|
||||
total = len(session.exec(count_statement).all())
|
||||
|
||||
# 转换为响应对象
|
||||
data = [expression_to_response(expr) for expr in expressions]
|
||||
|
||||
return ExpressionListResponse(success=True, total=total, page=page, page_size=page_size, data=data)
|
||||
@@ -272,7 +295,9 @@ async def get_expression_detail(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
expression = Expression.get_or_none(Expression.id == expression_id)
|
||||
with get_db_session() as session:
|
||||
statement = select(Expression).where(col(Expression.id) == expression_id).limit(1)
|
||||
expression = session.exec(statement).first()
|
||||
|
||||
if not expression:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {expression_id} 的表达方式")
|
||||
@@ -305,16 +330,22 @@ async def create_expression(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
current_time = time.time()
|
||||
current_time = datetime.now()
|
||||
|
||||
# 创建表达方式
|
||||
expression = Expression.create(
|
||||
situation=request.situation,
|
||||
style=request.style,
|
||||
chat_id=request.chat_id,
|
||||
last_active_time=current_time,
|
||||
create_date=current_time,
|
||||
)
|
||||
with get_db_session() as session:
|
||||
expression = Expression(
|
||||
situation=request.situation,
|
||||
style=request.style,
|
||||
context="",
|
||||
up_content="",
|
||||
content_list="[]",
|
||||
count=0,
|
||||
last_active_time=current_time,
|
||||
create_time=current_time,
|
||||
session_id=request.chat_id,
|
||||
)
|
||||
session.add(expression)
|
||||
|
||||
logger.info(f"表达方式已创建: ID={expression.id}, situation={request.situation}")
|
||||
|
||||
@@ -350,16 +381,18 @@ async def update_expression(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
expression = Expression.get_or_none(Expression.id == expression_id)
|
||||
with get_db_session() as session:
|
||||
statement = select(Expression).where(col(Expression.id) == expression_id).limit(1)
|
||||
expression = session.exec(statement).first()
|
||||
|
||||
if not expression:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {expression_id} 的表达方式")
|
||||
|
||||
# 冲突检测:如果要求未检查状态,但已经被检查了
|
||||
if request.require_unchecked and expression.checked:
|
||||
if request.require_unchecked and getattr(expression, "checked", False):
|
||||
raise HTTPException(
|
||||
status_code=409,
|
||||
detail=f"此表达方式已被{'AI自动' if expression.modified_by == 'ai' else '人工'}检查,请刷新列表",
|
||||
detail=f"此表达方式已被{'AI自动' if getattr(expression, 'modified_by', None) == 'ai' else '人工'}检查,请刷新列表",
|
||||
)
|
||||
|
||||
# 只更新提供的字段
|
||||
@@ -376,13 +409,18 @@ async def update_expression(
|
||||
update_data["modified_by"] = "user"
|
||||
|
||||
# 更新最后活跃时间
|
||||
update_data["last_active_time"] = time.time()
|
||||
update_data["last_active_time"] = datetime.now()
|
||||
|
||||
# 执行更新
|
||||
for field, value in update_data.items():
|
||||
setattr(expression, field, value)
|
||||
|
||||
expression.save()
|
||||
with get_db_session() as session:
|
||||
db_expression = session.exec(select(Expression).where(col(Expression.id) == expression_id).limit(1)).first()
|
||||
if not db_expression:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {expression_id} 的表达方式")
|
||||
for field, value in update_data.items():
|
||||
if hasattr(db_expression, field):
|
||||
setattr(db_expression, field, value)
|
||||
session.add(db_expression)
|
||||
expression = db_expression
|
||||
|
||||
logger.info(f"表达方式已更新: ID={expression_id}, 字段: {list(update_data.keys())}")
|
||||
|
||||
@@ -414,7 +452,9 @@ async def delete_expression(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
expression = Expression.get_or_none(Expression.id == expression_id)
|
||||
with get_db_session() as session:
|
||||
statement = select(Expression).where(col(Expression.id) == expression_id).limit(1)
|
||||
expression = session.exec(statement).first()
|
||||
|
||||
if not expression:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {expression_id} 的表达方式")
|
||||
@@ -423,7 +463,8 @@ async def delete_expression(
|
||||
situation = expression.situation
|
||||
|
||||
# 执行删除
|
||||
expression.delete_instance()
|
||||
with get_db_session() as session:
|
||||
session.exec(delete(Expression).where(col(Expression.id) == expression_id))
|
||||
|
||||
logger.info(f"表达方式已删除: ID={expression_id}, situation={situation}")
|
||||
|
||||
@@ -465,8 +506,9 @@ async def batch_delete_expressions(
|
||||
raise HTTPException(status_code=400, detail="未提供要删除的表达方式ID")
|
||||
|
||||
# 查找所有要删除的表达方式
|
||||
expressions = Expression.select().where(Expression.id.in_(request.ids))
|
||||
found_ids = [expr.id for expr in expressions]
|
||||
with get_db_session() as session:
|
||||
statements = select(Expression.id).where(col(Expression.id).in_(request.ids))
|
||||
found_ids = [expr_id for expr_id in session.exec(statements).all()]
|
||||
|
||||
# 检查是否有未找到的ID
|
||||
not_found_ids = set(request.ids) - set(found_ids)
|
||||
@@ -474,7 +516,9 @@ async def batch_delete_expressions(
|
||||
logger.warning(f"部分表达方式未找到: {not_found_ids}")
|
||||
|
||||
# 执行批量删除
|
||||
deleted_count = Expression.delete().where(Expression.id.in_(found_ids)).execute()
|
||||
with get_db_session() as session:
|
||||
result = session.exec(delete(Expression).where(col(Expression.id).in_(found_ids)))
|
||||
deleted_count = result.rowcount or 0
|
||||
|
||||
logger.info(f"批量删除了 {deleted_count} 个表达方式")
|
||||
|
||||
@@ -503,21 +547,21 @@ async def get_expression_stats(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
total = Expression.select().count()
|
||||
with get_db_session() as session:
|
||||
total = len(session.exec(select(Expression.id)).all())
|
||||
|
||||
# 按 chat_id 统计
|
||||
chat_stats = {}
|
||||
for expr in Expression.select(Expression.chat_id):
|
||||
chat_id = expr.chat_id
|
||||
chat_stats[chat_id] = chat_stats.get(chat_id, 0) + 1
|
||||
chat_stats = {}
|
||||
for chat_id in session.exec(select(Expression.session_id)).all():
|
||||
if chat_id:
|
||||
chat_stats[chat_id] = chat_stats.get(chat_id, 0) + 1
|
||||
|
||||
# 获取最近创建的记录数(7天内)
|
||||
seven_days_ago = time.time() - (7 * 24 * 60 * 60)
|
||||
recent = (
|
||||
Expression.select()
|
||||
.where((Expression.create_date.is_null(False)) & (Expression.create_date >= seven_days_ago))
|
||||
.count()
|
||||
)
|
||||
seven_days_ago = datetime.now() - timedelta(days=7)
|
||||
recent_statement = (
|
||||
select(func.count())
|
||||
.select_from(Expression)
|
||||
.where(col(Expression.create_time).is_not(None), col(Expression.create_time) >= seven_days_ago)
|
||||
)
|
||||
recent = session.exec(recent_statement).one()
|
||||
|
||||
return {
|
||||
"success": True,
|
||||
@@ -561,12 +605,13 @@ async def get_review_stats(maibot_session: Optional[str] = Cookie(None), authori
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
total = Expression.select().count()
|
||||
unchecked = Expression.select().where(Expression.checked == False).count()
|
||||
passed = Expression.select().where((Expression.checked == True) & (Expression.rejected == False)).count()
|
||||
rejected = Expression.select().where((Expression.checked == True) & (Expression.rejected == True)).count()
|
||||
ai_checked = Expression.select().where(Expression.modified_by == "ai").count()
|
||||
user_checked = Expression.select().where(Expression.modified_by == "user").count()
|
||||
with get_db_session() as session:
|
||||
total = len(session.exec(select(Expression.id)).all())
|
||||
unchecked = 0
|
||||
passed = 0
|
||||
rejected = 0
|
||||
ai_checked = 0
|
||||
user_checked = 0
|
||||
|
||||
return ReviewStatsResponse(
|
||||
total=total,
|
||||
@@ -620,31 +665,44 @@ async def get_review_list(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
query = Expression.select()
|
||||
statement = select(Expression)
|
||||
|
||||
# 根据筛选类型过滤
|
||||
if filter_type == "unchecked":
|
||||
query = query.where(Expression.checked == False)
|
||||
elif filter_type == "passed":
|
||||
query = query.where((Expression.checked == True) & (Expression.rejected == False))
|
||||
elif filter_type == "rejected":
|
||||
query = query.where((Expression.checked == True) & (Expression.rejected == True))
|
||||
if filter_type in {"unchecked", "passed", "rejected"}:
|
||||
statement = statement.where(col(Expression.id) == -1)
|
||||
# all 不需要额外过滤
|
||||
|
||||
# 搜索过滤
|
||||
if search:
|
||||
query = query.where((Expression.situation.contains(search)) | (Expression.style.contains(search)))
|
||||
statement = statement.where(
|
||||
(col(Expression.situation).contains(search)) | (col(Expression.style).contains(search))
|
||||
)
|
||||
|
||||
# 聊天ID过滤
|
||||
if chat_id:
|
||||
query = query.where(Expression.chat_id == chat_id)
|
||||
statement = statement.where(col(Expression.session_id) == chat_id)
|
||||
|
||||
# 排序:创建时间倒序
|
||||
query = query.order_by(case((Expression.create_date.is_null(), 1), else_=0), Expression.create_date.desc())
|
||||
statement = statement.order_by(
|
||||
case((col(Expression.create_time).is_(None), 1), else_=0),
|
||||
col(Expression.create_time).desc(),
|
||||
)
|
||||
|
||||
total = query.count()
|
||||
offset = (page - 1) * page_size
|
||||
expressions = query.offset(offset).limit(page_size)
|
||||
statement = statement.offset(offset).limit(page_size)
|
||||
|
||||
with get_db_session() as session:
|
||||
expressions = session.exec(statement).all()
|
||||
|
||||
count_statement = select(Expression.id)
|
||||
if filter_type in {"unchecked", "passed", "rejected"}:
|
||||
count_statement = count_statement.where(col(Expression.id) == -1)
|
||||
if search:
|
||||
count_statement = count_statement.where(
|
||||
(col(Expression.situation).contains(search)) | (col(Expression.style).contains(search))
|
||||
)
|
||||
if chat_id:
|
||||
count_statement = count_statement.where(col(Expression.session_id) == chat_id)
|
||||
total = len(session.exec(count_statement).all())
|
||||
|
||||
return ReviewListResponse(
|
||||
success=True,
|
||||
@@ -720,7 +778,8 @@ async def batch_review_expressions(
|
||||
|
||||
for item in request.items:
|
||||
try:
|
||||
expression = Expression.get_or_none(Expression.id == item.id)
|
||||
with get_db_session() as session:
|
||||
expression = session.exec(select(Expression).where(col(Expression.id) == item.id).limit(1)).first()
|
||||
|
||||
if not expression:
|
||||
results.append(
|
||||
@@ -730,23 +789,28 @@ async def batch_review_expressions(
|
||||
continue
|
||||
|
||||
# 冲突检测
|
||||
if item.require_unchecked and expression.checked:
|
||||
if item.require_unchecked:
|
||||
results.append(
|
||||
BatchReviewResultItem(
|
||||
id=item.id,
|
||||
success=False,
|
||||
message=f"已被{'AI自动' if expression.modified_by == 'ai' else '人工'}检查",
|
||||
)
|
||||
BatchReviewResultItem(id=item.id, success=False, message="当前模型不支持审核状态过滤")
|
||||
)
|
||||
failed += 1
|
||||
continue
|
||||
|
||||
# 更新状态
|
||||
expression.checked = True
|
||||
expression.rejected = item.rejected
|
||||
expression.modified_by = "user"
|
||||
expression.last_active_time = time.time()
|
||||
expression.save()
|
||||
with get_db_session() as session:
|
||||
db_expression = session.exec(
|
||||
select(Expression).where(col(Expression.id) == item.id).limit(1)
|
||||
).first()
|
||||
if not db_expression:
|
||||
results.append(
|
||||
BatchReviewResultItem(
|
||||
id=item.id, success=False, message=f"未找到 ID 为 {item.id} 的表达方式"
|
||||
)
|
||||
)
|
||||
failed += 1
|
||||
continue
|
||||
db_expression.last_active_time = datetime.now()
|
||||
session.add(db_expression)
|
||||
|
||||
results.append(
|
||||
BatchReviewResultItem(id=item.id, success=True, message="通过" if not item.rejected else "拒绝")
|
||||
|
||||
@@ -3,12 +3,16 @@
|
||||
from fastapi import APIRouter, HTTPException, Header, Query, Cookie
|
||||
from pydantic import BaseModel
|
||||
from typing import Optional, List, Dict
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import case
|
||||
from sqlmodel import col, select, delete
|
||||
|
||||
from src.common.logger import get_logger
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import PersonInfo
|
||||
from src.webui.core import verify_auth_token_from_cookie_or_header
|
||||
import json
|
||||
import time
|
||||
|
||||
logger = get_logger("webui.person")
|
||||
|
||||
@@ -29,7 +33,7 @@ class PersonInfoResponse(BaseModel):
|
||||
nickname: Optional[str]
|
||||
group_nick_name: Optional[List[Dict[str, str]]] # 解析后的 JSON
|
||||
memory_points: Optional[str]
|
||||
know_times: Optional[float]
|
||||
know_times: Optional[int]
|
||||
know_since: Optional[float]
|
||||
last_know: Optional[float]
|
||||
|
||||
@@ -112,20 +116,22 @@ def parse_group_nick_name(group_nick_name_str: Optional[str]) -> Optional[List[D
|
||||
|
||||
def person_to_response(person: PersonInfo) -> PersonInfoResponse:
|
||||
"""将 PersonInfo 模型转换为响应对象"""
|
||||
know_since = person.first_known_time.timestamp() if person.first_known_time else None
|
||||
last_know = person.last_known_time.timestamp() if person.last_known_time else None
|
||||
return PersonInfoResponse(
|
||||
id=person.id,
|
||||
id=person.id or 0,
|
||||
is_known=person.is_known,
|
||||
person_id=person.person_id,
|
||||
person_name=person.person_name,
|
||||
name_reason=person.name_reason,
|
||||
platform=person.platform,
|
||||
user_id=person.user_id,
|
||||
nickname=person.nickname,
|
||||
group_nick_name=parse_group_nick_name(person.group_nick_name),
|
||||
nickname=person.user_nickname,
|
||||
group_nick_name=parse_group_nick_name(person.group_nickname),
|
||||
memory_points=person.memory_points,
|
||||
know_times=person.know_times,
|
||||
know_since=person.know_since,
|
||||
last_know=person.last_know,
|
||||
know_times=person.know_counts,
|
||||
know_since=know_since,
|
||||
last_know=last_know,
|
||||
)
|
||||
|
||||
|
||||
@@ -157,36 +163,50 @@ async def get_person_list(
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
# 构建查询
|
||||
query = PersonInfo.select()
|
||||
statement = select(PersonInfo)
|
||||
|
||||
# 搜索过滤
|
||||
if search:
|
||||
query = query.where(
|
||||
(PersonInfo.person_name.contains(search))
|
||||
| (PersonInfo.nickname.contains(search))
|
||||
| (PersonInfo.user_id.contains(search))
|
||||
statement = statement.where(
|
||||
(col(PersonInfo.person_name).contains(search))
|
||||
| (col(PersonInfo.user_nickname).contains(search))
|
||||
| (col(PersonInfo.user_id).contains(search))
|
||||
)
|
||||
|
||||
# 已认识状态过滤
|
||||
if is_known is not None:
|
||||
query = query.where(PersonInfo.is_known == is_known)
|
||||
statement = statement.where(col(PersonInfo.is_known) == is_known)
|
||||
|
||||
# 平台过滤
|
||||
if platform:
|
||||
query = query.where(PersonInfo.platform == platform)
|
||||
statement = statement.where(col(PersonInfo.platform) == platform)
|
||||
|
||||
# 排序:最后更新时间倒序(NULL 值放在最后)
|
||||
# Peewee 不支持 nulls_last,使用 CASE WHEN 来实现
|
||||
query = query.order_by(case((PersonInfo.last_know.is_null(), 1), else_=0), PersonInfo.last_know.desc())
|
||||
statement = statement.order_by(
|
||||
case((col(PersonInfo.last_known_time).is_(None), 1), else_=0),
|
||||
col(PersonInfo.last_known_time).desc(),
|
||||
)
|
||||
|
||||
# 获取总数
|
||||
total = query.count()
|
||||
|
||||
# 分页
|
||||
offset = (page - 1) * page_size
|
||||
persons = query.offset(offset).limit(page_size)
|
||||
statement = statement.offset(offset).limit(page_size)
|
||||
|
||||
with get_db_session() as session:
|
||||
persons = session.exec(statement).all()
|
||||
|
||||
count_statement = select(PersonInfo.id)
|
||||
if search:
|
||||
count_statement = count_statement.where(
|
||||
(col(PersonInfo.person_name).contains(search))
|
||||
| (col(PersonInfo.user_nickname).contains(search))
|
||||
| (col(PersonInfo.user_id).contains(search))
|
||||
)
|
||||
if is_known is not None:
|
||||
count_statement = count_statement.where(col(PersonInfo.is_known) == is_known)
|
||||
if platform:
|
||||
count_statement = count_statement.where(col(PersonInfo.platform) == platform)
|
||||
total = len(session.exec(count_statement).all())
|
||||
|
||||
# 转换为响应对象
|
||||
data = [person_to_response(person) for person in persons]
|
||||
|
||||
return PersonListResponse(success=True, total=total, page=page, page_size=page_size, data=data)
|
||||
@@ -215,7 +235,9 @@ async def get_person_detail(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||||
with get_db_session() as session:
|
||||
statement = select(PersonInfo).where(col(PersonInfo.person_id) == person_id).limit(1)
|
||||
person = session.exec(statement).first()
|
||||
|
||||
if not person:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {person_id} 的人物信息")
|
||||
@@ -250,7 +272,9 @@ async def update_person(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||||
with get_db_session() as session:
|
||||
statement = select(PersonInfo).where(col(PersonInfo.person_id) == person_id).limit(1)
|
||||
person = session.exec(statement).first()
|
||||
|
||||
if not person:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {person_id} 的人物信息")
|
||||
@@ -262,13 +286,18 @@ async def update_person(
|
||||
raise HTTPException(status_code=400, detail="未提供任何需要更新的字段")
|
||||
|
||||
# 更新最后修改时间
|
||||
update_data["last_know"] = time.time()
|
||||
update_data["last_known_time"] = datetime.now()
|
||||
|
||||
# 执行更新
|
||||
for field, value in update_data.items():
|
||||
setattr(person, field, value)
|
||||
|
||||
person.save()
|
||||
with get_db_session() as session:
|
||||
db_person = session.exec(select(PersonInfo).where(col(PersonInfo.person_id) == person_id).limit(1)).first()
|
||||
if not db_person:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {person_id} 的人物信息")
|
||||
for field, value in update_data.items():
|
||||
if hasattr(db_person, field):
|
||||
setattr(db_person, field, value)
|
||||
session.add(db_person)
|
||||
person = db_person
|
||||
|
||||
logger.info(f"人物信息已更新: {person_id}, 字段: {list(update_data.keys())}")
|
||||
|
||||
@@ -300,16 +329,19 @@ async def delete_person(
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||||
with get_db_session() as session:
|
||||
statement = select(PersonInfo).where(col(PersonInfo.person_id) == person_id).limit(1)
|
||||
person = session.exec(statement).first()
|
||||
|
||||
if not person:
|
||||
raise HTTPException(status_code=404, detail=f"未找到 ID 为 {person_id} 的人物信息")
|
||||
|
||||
# 记录删除信息
|
||||
person_name = person.person_name or person.nickname or person.user_id
|
||||
person_name = person.person_name or person.user_nickname or person.user_id
|
||||
|
||||
# 执行删除
|
||||
person.delete_instance()
|
||||
with get_db_session() as session:
|
||||
session.exec(delete(PersonInfo).where(col(PersonInfo.person_id) == person_id))
|
||||
|
||||
logger.info(f"人物信息已删除: {person_id} ({person_name})")
|
||||
|
||||
@@ -336,15 +368,17 @@ async def get_person_stats(maibot_session: Optional[str] = Cookie(None), authori
|
||||
try:
|
||||
verify_auth_token(maibot_session, authorization)
|
||||
|
||||
total = PersonInfo.select().count()
|
||||
known = PersonInfo.select().where(PersonInfo.is_known).count()
|
||||
with get_db_session() as session:
|
||||
total = len(session.exec(select(PersonInfo.id)).all())
|
||||
known = len(session.exec(select(PersonInfo.id).where(col(PersonInfo.is_known) == True)).all())
|
||||
unknown = total - known
|
||||
|
||||
# 按平台统计
|
||||
platforms = {}
|
||||
for person in PersonInfo.select(PersonInfo.platform):
|
||||
platform = person.platform
|
||||
platforms[platform] = platforms.get(platform, 0) + 1
|
||||
with get_db_session() as session:
|
||||
for platform in session.exec(select(PersonInfo.platform)).all():
|
||||
if platform:
|
||||
platforms[platform] = platforms.get(platform, 0) + 1
|
||||
|
||||
return {"success": True, "data": {"total": total, "known": known, "unknown": unknown, "platforms": platforms}}
|
||||
|
||||
@@ -383,14 +417,17 @@ async def batch_delete_persons(
|
||||
|
||||
for person_id in request.person_ids:
|
||||
try:
|
||||
person = PersonInfo.get_or_none(PersonInfo.person_id == person_id)
|
||||
if person:
|
||||
person.delete_instance()
|
||||
deleted_count += 1
|
||||
logger.info(f"批量删除: {person_id}")
|
||||
else:
|
||||
failed_count += 1
|
||||
failed_ids.append(person_id)
|
||||
with get_db_session() as session:
|
||||
person = session.exec(
|
||||
select(PersonInfo).where(col(PersonInfo.person_id) == person_id).limit(1)
|
||||
).first()
|
||||
if person:
|
||||
session.exec(delete(PersonInfo).where(col(PersonInfo.person_id) == person_id))
|
||||
deleted_count += 1
|
||||
logger.info(f"批量删除: {person_id}")
|
||||
else:
|
||||
failed_count += 1
|
||||
failed_ids.append(person_id)
|
||||
except Exception as e:
|
||||
logger.error(f"删除 {person_id} 失败: {e}")
|
||||
failed_count += 1
|
||||
|
||||
@@ -1,13 +1,16 @@
|
||||
"""统计数据 API 路由"""
|
||||
|
||||
from fastapi import APIRouter, HTTPException, Depends, Cookie, Header
|
||||
from pydantic import BaseModel, Field
|
||||
from typing import Dict, Any, List, Optional
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy import func as fn
|
||||
from typing import Any, Optional
|
||||
|
||||
from fastapi import APIRouter, Cookie, Depends, Header, HTTPException
|
||||
from pydantic import BaseModel, Field
|
||||
from sqlalchemy import desc, func, or_
|
||||
from sqlmodel import col, select
|
||||
|
||||
from src.common.database.database import get_db_session
|
||||
from src.common.database.database_model import Messages, ModelUsage, OnlineTime
|
||||
from src.common.logger import get_logger
|
||||
from src.common.database.database_model import LLMUsage, OnlineTime, Messages
|
||||
from src.webui.core import verify_auth_token_from_cookie_or_header
|
||||
|
||||
logger = get_logger("webui.statistics")
|
||||
@@ -60,10 +63,10 @@ class DashboardData(BaseModel):
|
||||
"""仪表盘数据"""
|
||||
|
||||
summary: StatisticsSummary
|
||||
model_stats: List[ModelStatistics]
|
||||
hourly_data: List[TimeSeriesData]
|
||||
daily_data: List[TimeSeriesData]
|
||||
recent_activity: List[Dict[str, Any]]
|
||||
model_stats: list[ModelStatistics]
|
||||
hourly_data: list[TimeSeriesData]
|
||||
daily_data: list[TimeSeriesData]
|
||||
recent_activity: list[dict[str, Any]]
|
||||
|
||||
|
||||
@router.get("/dashboard", response_model=DashboardData)
|
||||
@@ -111,26 +114,44 @@ async def get_dashboard_data(hours: int = 24, _auth: bool = Depends(require_auth
|
||||
|
||||
async def _get_summary_statistics(start_time: datetime, end_time: datetime) -> StatisticsSummary:
|
||||
"""获取摘要统计数据(优化:使用数据库聚合)"""
|
||||
summary = StatisticsSummary()
|
||||
summary = StatisticsSummary(
|
||||
total_requests=0,
|
||||
total_cost=0.0,
|
||||
total_tokens=0,
|
||||
online_time=0.0,
|
||||
total_messages=0,
|
||||
total_replies=0,
|
||||
avg_response_time=0.0,
|
||||
cost_per_hour=0.0,
|
||||
tokens_per_hour=0.0,
|
||||
)
|
||||
|
||||
# 使用聚合查询替代全量加载
|
||||
query = LLMUsage.select(
|
||||
fn.COUNT(LLMUsage.id).alias("total_requests"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.cost), 0).alias("total_cost"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.prompt_tokens + LLMUsage.completion_tokens), 0).alias("total_tokens"),
|
||||
fn.COALESCE(fn.AVG(LLMUsage.time_cost), 0).alias("avg_response_time"),
|
||||
).where((LLMUsage.timestamp >= start_time) & (LLMUsage.timestamp <= end_time))
|
||||
with get_db_session() as session:
|
||||
statement = select(
|
||||
func.count().label("total_requests"),
|
||||
func.sum(col(ModelUsage.cost)).label("total_cost"),
|
||||
func.sum(col(ModelUsage.total_tokens)).label("total_tokens"),
|
||||
func.avg(col(ModelUsage.time_cost)).label("avg_response_time"),
|
||||
).where(col(ModelUsage.timestamp) >= start_time, col(ModelUsage.timestamp) <= end_time)
|
||||
result = session.execute(statement).first()
|
||||
|
||||
result = query.dicts().get()
|
||||
summary.total_requests = result["total_requests"]
|
||||
summary.total_cost = result["total_cost"]
|
||||
summary.total_tokens = result["total_tokens"]
|
||||
summary.avg_response_time = result["avg_response_time"] or 0.0
|
||||
if result:
|
||||
total_requests, total_cost, total_tokens, avg_response_time = result
|
||||
summary.total_requests = total_requests or 0
|
||||
summary.total_cost = float(total_cost or 0.0)
|
||||
summary.total_tokens = total_tokens or 0
|
||||
summary.avg_response_time = float(avg_response_time or 0.0)
|
||||
|
||||
# 查询在线时间 - 这个数据量通常不大,保留原逻辑
|
||||
online_records = list(
|
||||
OnlineTime.select().where((OnlineTime.start_timestamp >= start_time) | (OnlineTime.end_timestamp >= start_time))
|
||||
)
|
||||
with get_db_session() as session:
|
||||
statement = select(OnlineTime).where(
|
||||
or_(
|
||||
col(OnlineTime.start_timestamp) >= start_time,
|
||||
col(OnlineTime.end_timestamp) >= start_time,
|
||||
)
|
||||
)
|
||||
online_records = session.execute(statement).scalars().all()
|
||||
|
||||
for record in online_records:
|
||||
start = max(record.start_timestamp, start_time)
|
||||
@@ -139,18 +160,23 @@ async def _get_summary_statistics(start_time: datetime, end_time: datetime) -> S
|
||||
summary.online_time += (end - start).total_seconds()
|
||||
|
||||
# 查询消息数量 - 使用聚合优化
|
||||
messages_query = Messages.select(fn.COUNT(Messages.id).alias("total")).where(
|
||||
(Messages.time >= start_time.timestamp()) & (Messages.time <= end_time.timestamp())
|
||||
)
|
||||
summary.total_messages = messages_query.scalar() or 0
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Messages.timestamp) >= start_time,
|
||||
col(Messages.timestamp) <= end_time,
|
||||
)
|
||||
total_messages = session.execute(statement).scalar()
|
||||
summary.total_messages = int(total_messages or 0)
|
||||
|
||||
# 统计回复数量
|
||||
replies_query = Messages.select(fn.COUNT(Messages.id).alias("total")).where(
|
||||
(Messages.time >= start_time.timestamp())
|
||||
& (Messages.time <= end_time.timestamp())
|
||||
& (Messages.reply_to.is_null(False))
|
||||
)
|
||||
summary.total_replies = replies_query.scalar() or 0
|
||||
with get_db_session() as session:
|
||||
statement = select(func.count()).where(
|
||||
col(Messages.timestamp) >= start_time,
|
||||
col(Messages.timestamp) <= end_time,
|
||||
col(Messages.reply_to).is_not(None),
|
||||
)
|
||||
total_replies = session.execute(statement).scalar()
|
||||
summary.total_replies = int(total_replies or 0)
|
||||
|
||||
# 计算派生指标
|
||||
if summary.online_time > 0:
|
||||
@@ -161,55 +187,80 @@ async def _get_summary_statistics(start_time: datetime, end_time: datetime) -> S
|
||||
return summary
|
||||
|
||||
|
||||
async def _get_model_statistics(start_time: datetime) -> List[ModelStatistics]:
|
||||
async def _get_model_statistics(start_time: datetime) -> list[ModelStatistics]:
|
||||
"""获取模型统计数据(优化:使用数据库聚合和分组)"""
|
||||
# 使用GROUP BY聚合,避免全量加载
|
||||
query = (
|
||||
LLMUsage.select(
|
||||
fn.COALESCE(LLMUsage.model_assign_name, LLMUsage.model_name, "unknown").alias("model_name"),
|
||||
fn.COUNT(LLMUsage.id).alias("request_count"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.cost), 0).alias("total_cost"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.prompt_tokens + LLMUsage.completion_tokens), 0).alias("total_tokens"),
|
||||
fn.COALESCE(fn.AVG(LLMUsage.time_cost), 0).alias("avg_response_time"),
|
||||
)
|
||||
.where(LLMUsage.timestamp >= start_time)
|
||||
.group_by(fn.COALESCE(LLMUsage.model_assign_name, LLMUsage.model_name, "unknown"))
|
||||
.order_by(fn.COUNT(LLMUsage.id).desc())
|
||||
.limit(10) # 只取前10个
|
||||
statement = (
|
||||
select(ModelUsage)
|
||||
.where(col(ModelUsage.timestamp) >= start_time)
|
||||
.order_by(desc(col(ModelUsage.timestamp)))
|
||||
.limit(200)
|
||||
)
|
||||
|
||||
result = []
|
||||
for row in query.dicts():
|
||||
with get_db_session() as session:
|
||||
rows = session.execute(statement).all()
|
||||
|
||||
aggregates: dict[str, dict[str, float | int]] = {}
|
||||
for record in rows:
|
||||
model_name = record.model_assign_name or record.model_name or "unknown"
|
||||
if model_name not in aggregates:
|
||||
aggregates[model_name] = {
|
||||
"request_count": 0,
|
||||
"total_cost": 0.0,
|
||||
"total_tokens": 0,
|
||||
"total_time_cost": 0.0,
|
||||
"time_cost_count": 0,
|
||||
}
|
||||
bucket = aggregates[model_name]
|
||||
bucket["request_count"] = int(bucket["request_count"]) + 1
|
||||
bucket["total_cost"] = float(bucket["total_cost"]) + float(record.cost or 0.0)
|
||||
bucket["total_tokens"] = int(bucket["total_tokens"]) + int(record.total_tokens or 0)
|
||||
if record.time_cost:
|
||||
bucket["total_time_cost"] = float(bucket["total_time_cost"]) + float(record.time_cost)
|
||||
bucket["time_cost_count"] = int(bucket["time_cost_count"]) + 1
|
||||
|
||||
result: list[ModelStatistics] = []
|
||||
for model_name, bucket in sorted(
|
||||
aggregates.items(),
|
||||
key=lambda item: float(item[1]["request_count"]),
|
||||
reverse=True,
|
||||
)[:10]:
|
||||
time_cost_count = int(bucket["time_cost_count"])
|
||||
avg_time_cost = float(bucket["total_time_cost"]) / time_cost_count if time_cost_count > 0 else 0.0
|
||||
result.append(
|
||||
ModelStatistics(
|
||||
model_name=row["model_name"],
|
||||
request_count=row["request_count"],
|
||||
total_cost=row["total_cost"],
|
||||
total_tokens=row["total_tokens"],
|
||||
avg_response_time=row["avg_response_time"] or 0.0,
|
||||
model_name=model_name,
|
||||
request_count=int(bucket["request_count"]),
|
||||
total_cost=float(bucket["total_cost"]),
|
||||
total_tokens=int(bucket["total_tokens"]),
|
||||
avg_response_time=avg_time_cost,
|
||||
)
|
||||
)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
async def _get_hourly_statistics(start_time: datetime, end_time: datetime) -> List[TimeSeriesData]:
|
||||
async def _get_hourly_statistics(start_time: datetime, end_time: datetime) -> list[TimeSeriesData]:
|
||||
"""获取小时级统计数据(优化:使用数据库聚合)"""
|
||||
# SQLite的日期时间函数进行小时分组
|
||||
# 使用strftime将timestamp格式化为小时级别
|
||||
query = (
|
||||
LLMUsage.select(
|
||||
fn.strftime("%Y-%m-%dT%H:00:00", LLMUsage.timestamp).alias("hour"),
|
||||
fn.COUNT(LLMUsage.id).alias("requests"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.cost), 0).alias("cost"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.prompt_tokens + LLMUsage.completion_tokens), 0).alias("tokens"),
|
||||
hour_expr = func.strftime("%Y-%m-%dT%H:00:00", col(ModelUsage.timestamp))
|
||||
statement = (
|
||||
select(
|
||||
hour_expr.label("hour"),
|
||||
func.count().label("requests"),
|
||||
func.sum(col(ModelUsage.cost)).label("cost"),
|
||||
func.sum(col(ModelUsage.total_tokens)).label("tokens"),
|
||||
)
|
||||
.where((LLMUsage.timestamp >= start_time) & (LLMUsage.timestamp <= end_time))
|
||||
.group_by(fn.strftime("%Y-%m-%dT%H:00:00", LLMUsage.timestamp))
|
||||
.where(col(ModelUsage.timestamp) >= start_time, col(ModelUsage.timestamp) <= end_time)
|
||||
.group_by(hour_expr)
|
||||
)
|
||||
|
||||
with get_db_session() as session:
|
||||
rows = session.execute(statement).all()
|
||||
|
||||
# 转换为字典以快速查找
|
||||
data_dict = {row["hour"]: row for row in query.dicts()}
|
||||
data_dict = {row[0]: row for row in rows}
|
||||
|
||||
# 填充所有小时(包括没有数据的)
|
||||
result = []
|
||||
@@ -219,7 +270,12 @@ async def _get_hourly_statistics(start_time: datetime, end_time: datetime) -> Li
|
||||
if hour_str in data_dict:
|
||||
row = data_dict[hour_str]
|
||||
result.append(
|
||||
TimeSeriesData(timestamp=hour_str, requests=row["requests"], cost=row["cost"], tokens=row["tokens"])
|
||||
TimeSeriesData(
|
||||
timestamp=hour_str,
|
||||
requests=row[1] or 0,
|
||||
cost=float(row[2] or 0.0),
|
||||
tokens=row[3] or 0,
|
||||
)
|
||||
)
|
||||
else:
|
||||
result.append(TimeSeriesData(timestamp=hour_str, requests=0, cost=0.0, tokens=0))
|
||||
@@ -228,22 +284,26 @@ async def _get_hourly_statistics(start_time: datetime, end_time: datetime) -> Li
|
||||
return result
|
||||
|
||||
|
||||
async def _get_daily_statistics(start_time: datetime, end_time: datetime) -> List[TimeSeriesData]:
|
||||
async def _get_daily_statistics(start_time: datetime, end_time: datetime) -> list[TimeSeriesData]:
|
||||
"""获取日级统计数据(优化:使用数据库聚合)"""
|
||||
# 使用strftime按日期分组
|
||||
query = (
|
||||
LLMUsage.select(
|
||||
fn.strftime("%Y-%m-%dT00:00:00", LLMUsage.timestamp).alias("day"),
|
||||
fn.COUNT(LLMUsage.id).alias("requests"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.cost), 0).alias("cost"),
|
||||
fn.COALESCE(fn.SUM(LLMUsage.prompt_tokens + LLMUsage.completion_tokens), 0).alias("tokens"),
|
||||
day_expr = func.strftime("%Y-%m-%dT00:00:00", col(ModelUsage.timestamp))
|
||||
statement = (
|
||||
select(
|
||||
day_expr.label("day"),
|
||||
func.count().label("requests"),
|
||||
func.sum(col(ModelUsage.cost)).label("cost"),
|
||||
func.sum(col(ModelUsage.total_tokens)).label("tokens"),
|
||||
)
|
||||
.where((LLMUsage.timestamp >= start_time) & (LLMUsage.timestamp <= end_time))
|
||||
.group_by(fn.strftime("%Y-%m-%dT00:00:00", LLMUsage.timestamp))
|
||||
.where(col(ModelUsage.timestamp) >= start_time, col(ModelUsage.timestamp) <= end_time)
|
||||
.group_by(day_expr)
|
||||
)
|
||||
|
||||
with get_db_session() as session:
|
||||
rows = session.execute(statement).all()
|
||||
|
||||
# 转换为字典
|
||||
data_dict = {row["day"]: row for row in query.dicts()}
|
||||
data_dict = {row[0]: row for row in rows}
|
||||
|
||||
# 填充所有天
|
||||
result = []
|
||||
@@ -253,7 +313,12 @@ async def _get_daily_statistics(start_time: datetime, end_time: datetime) -> Lis
|
||||
if day_str in data_dict:
|
||||
row = data_dict[day_str]
|
||||
result.append(
|
||||
TimeSeriesData(timestamp=day_str, requests=row["requests"], cost=row["cost"], tokens=row["tokens"])
|
||||
TimeSeriesData(
|
||||
timestamp=day_str,
|
||||
requests=row[1] or 0,
|
||||
cost=float(row[2] or 0.0),
|
||||
tokens=row[3] or 0,
|
||||
)
|
||||
)
|
||||
else:
|
||||
result.append(TimeSeriesData(timestamp=day_str, requests=0, cost=0.0, tokens=0))
|
||||
@@ -262,9 +327,11 @@ async def _get_daily_statistics(start_time: datetime, end_time: datetime) -> Lis
|
||||
return result
|
||||
|
||||
|
||||
async def _get_recent_activity(limit: int = 10) -> List[Dict[str, Any]]:
|
||||
async def _get_recent_activity(limit: int = 10) -> list[dict[str, Any]]:
|
||||
"""获取最近活动"""
|
||||
records = list(LLMUsage.select().order_by(LLMUsage.timestamp.desc()).limit(limit))
|
||||
with get_db_session() as session:
|
||||
statement = select(ModelUsage).order_by(desc(col(ModelUsage.timestamp))).limit(limit)
|
||||
records = session.execute(statement).scalars().all()
|
||||
|
||||
activities = []
|
||||
for record in records:
|
||||
@@ -273,10 +340,10 @@ async def _get_recent_activity(limit: int = 10) -> List[Dict[str, Any]]:
|
||||
"timestamp": record.timestamp.isoformat(),
|
||||
"model": record.model_assign_name or record.model_name,
|
||||
"request_type": record.request_type,
|
||||
"tokens": (record.prompt_tokens or 0) + (record.completion_tokens or 0),
|
||||
"tokens": record.total_tokens or 0,
|
||||
"cost": record.cost or 0.0,
|
||||
"time_cost": record.time_cost or 0.0,
|
||||
"status": record.status,
|
||||
"status": None,
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user