WebUI 前端 & 后端超级大重构

This commit is contained in:
DrSmoothl
2026-03-14 21:06:36 +08:00
parent 6ca5a2939e
commit 172615f18a
69 changed files with 3128 additions and 6581 deletions

View File

@@ -1,20 +1,21 @@
"""黑话(俚语)管理路由"""
from typing import Annotated, Any, List, Optional
from fastapi import APIRouter, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy import func as fn
from sqlmodel import Session, col, delete, select
import json
from typing import Annotated, Any, List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlmodel import Session, col, delete, select
from src.common.database.database import get_db_session
from src.common.database.database_model import ChatSession, Jargon
from src.common.logger import get_logger
from src.webui.dependencies import require_auth
logger = get_logger("webui.jargon")
router = APIRouter(prefix="/jargon", tags=["Jargon"])
router = APIRouter(prefix="/jargon", tags=["Jargon"], dependencies=[Depends(require_auth)])
# ==================== 辅助函数 ====================
@@ -33,14 +34,10 @@ def parse_chat_id_to_stream_ids(chat_id_str: str) -> List[str]:
parsed = json.loads(chat_id_str)
if isinstance(parsed, list):
# 格式: [["stream_id", user_id], ...]
stream_ids = []
for item in parsed:
if isinstance(item, list) and len(item) >= 1:
stream_ids.append(str(item[0]))
return stream_ids
else:
# 其他格式,返回原始字符串
return [chat_id_str]
return [str(item[0]) for item in parsed if isinstance(item, list) and len(item) >= 1]
# 其他格式,返回原始字符串
return [chat_id_str]
except (json.JSONDecodeError, TypeError):
# 不是有效的 JSON可能是直接的 stream_id
return [chat_id_str]
@@ -57,9 +54,7 @@ def get_display_name_for_chat_id(chat_id_str: str, session: Session) -> str:
return chat_id_str[:20]
stream_id = stream_ids[0]
chat_session = session.exec(select(ChatSession).where(col(ChatSession.session_id) == stream_id)).first()
if not chat_session:
if not (chat_session := session.exec(select(ChatSession).where(col(ChatSession.session_id) == stream_id)).first()):
return stream_id[:20]
if chat_session.group_id:
@@ -180,9 +175,59 @@ class ChatListResponse(BaseModel):
# ==================== 工具函数 ====================
def parse_session_id_dict(session_id_dict_str: Optional[str]) -> dict[str, int]:
"""解析会话计数字典。"""
if not session_id_dict_str:
return {}
try:
parsed = json.loads(session_id_dict_str)
except (json.JSONDecodeError, TypeError):
return {}
if not isinstance(parsed, dict):
return {}
session_counts: dict[str, int] = {}
for session_id, count in parsed.items():
if not isinstance(session_id, str):
continue
if isinstance(count, int):
session_counts[session_id] = count
else:
try:
session_counts[session_id] = int(count)
except (TypeError, ValueError):
session_counts[session_id] = 0
return session_counts
def dump_session_id_dict(session_counts: dict[str, int]) -> str:
"""序列化会话计数字典。"""
return json.dumps(session_counts, ensure_ascii=False)
def get_primary_chat_id(session_id_dict_str: Optional[str]) -> str:
"""从会话计数字典中选出主聊天 ID。"""
if not (session_counts := parse_session_id_dict(session_id_dict_str)):
return ""
return max(session_counts.items(), key=lambda item: item[1])[0]
def has_chat_id(session_id_dict_str: Optional[str], chat_id: str) -> bool:
"""判断记录是否包含指定聊天 ID。"""
return chat_id in parse_session_id_dict(session_id_dict_str)
def build_session_id_dict_for_chat(chat_id: str, count: int = 1) -> str:
"""为单个聊天 ID 构建会话计数字典。"""
return dump_session_id_dict({chat_id: count})
def jargon_to_dict(jargon: Jargon, session: Session) -> dict[str, Any]:
"""将 Jargon ORM 对象转换为字典"""
chat_id = jargon.session_id or ""
chat_id = get_primary_chat_id(jargon.session_id_dict)
chat_name = get_display_name_for_chat_id(chat_id, session) if chat_id else None
return {
@@ -191,7 +236,7 @@ def jargon_to_dict(jargon: Jargon, session: Session) -> dict[str, Any]:
"raw_content": jargon.raw_content,
"meaning": jargon.meaning,
"chat_id": chat_id,
"stream_id": jargon.session_id,
"stream_id": chat_id or None,
"chat_name": chat_name,
"count": jargon.count,
"is_jargon": jargon.is_jargon,
@@ -215,7 +260,6 @@ async def get_jargon_list(
"""获取黑话列表"""
try:
statement = select(Jargon)
count_statement = select(fn.count()).select_from(Jargon)
if search:
search_filter = (
@@ -224,28 +268,28 @@ async def get_jargon_list(
| (col(Jargon.raw_content).contains(search))
)
statement = statement.where(search_filter)
count_statement = count_statement.where(search_filter)
if chat_id:
stream_ids = parse_chat_id_to_stream_ids(chat_id)
if stream_ids:
chat_filter = col(Jargon.session_id).contains(stream_ids[0])
else:
chat_filter = col(Jargon.session_id) == chat_id
statement = statement.where(chat_filter)
count_statement = count_statement.where(chat_filter)
if is_jargon is not None:
statement = statement.where(col(Jargon.is_jargon) == is_jargon)
count_statement = count_statement.where(col(Jargon.is_jargon) == is_jargon)
statement = statement.order_by(col(Jargon.count).desc(), col(Jargon.id).desc())
statement = statement.offset((page - 1) * page_size).limit(page_size)
with get_db_session() as session:
total = session.exec(count_statement).one()
jargons = session.exec(statement).all()
data = [jargon_to_dict(jargon, session) for jargon in jargons]
if chat_id:
stream_ids = parse_chat_id_to_stream_ids(chat_id)
chat_ids = stream_ids or [chat_id]
jargons = [
jargon
for jargon in jargons
if any(has_chat_id(jargon.session_id_dict, current_chat_id) for current_chat_id in chat_ids)
]
total = len(jargons)
offset = (page - 1) * page_size
page_jargons = jargons[offset : offset + page_size]
data = [jargon_to_dict(jargon, session) for jargon in page_jargons]
return JargonListResponse(
success=True,
@@ -265,22 +309,16 @@ async def get_chat_list():
"""获取所有有黑话记录的聊天列表"""
try:
with get_db_session() as session:
statement = select(Jargon.session_id).distinct().where(col(Jargon.session_id).is_not(None))
chat_id_list = [chat_id for chat_id in session.exec(statement).all() if chat_id]
jargons = session.exec(select(Jargon)).all()
# 用于按 stream_id 去重
seen_stream_ids: set[str] = set()
for chat_id in chat_id_list:
stream_ids = parse_chat_id_to_stream_ids(chat_id)
if stream_ids:
seen_stream_ids.add(stream_ids[0])
for jargon in jargons:
seen_stream_ids.update(parse_session_id_dict(jargon.session_id_dict).keys())
result = []
with get_db_session() as session:
for stream_id in seen_stream_ids:
chat_session = session.exec(select(ChatSession).where(col(ChatSession.session_id) == stream_id)).first()
if chat_session:
if chat_session := session.exec(select(ChatSession).where(col(ChatSession.session_id) == stream_id)).first():
chat_name = str(chat_session.group_id) if chat_session.group_id else stream_id[:20]
result.append(
ChatInfoResponse(
@@ -312,30 +350,21 @@ async def get_jargon_stats():
"""获取黑话统计数据"""
try:
with get_db_session() as session:
total = session.exec(select(fn.count()).select_from(Jargon)).one()
jargons = session.exec(select(Jargon)).all()
confirmed_jargon = session.exec(select(fn.count()).select_from(Jargon).where(col(Jargon.is_jargon))).one()
confirmed_not_jargon = session.exec(
select(fn.count()).select_from(Jargon).where(col(Jargon.is_jargon).is_(False))
).one()
pending = session.exec(select(fn.count()).select_from(Jargon).where(col(Jargon.is_jargon).is_(None))).one()
total = len(jargons)
confirmed_jargon = sum(jargon.is_jargon is True for jargon in jargons)
confirmed_not_jargon = sum(jargon.is_jargon is False for jargon in jargons)
pending = sum(jargon.is_jargon is None for jargon in jargons)
complete_count = sum(jargon.is_complete for jargon in jargons)
complete_count = session.exec(select(fn.count()).select_from(Jargon).where(col(Jargon.is_complete))).one()
top_chats_counter: dict[str, int] = {}
for jargon in jargons:
for session_id in parse_session_id_dict(jargon.session_id_dict):
top_chats_counter[session_id] = top_chats_counter.get(session_id, 0) + 1
chat_count = session.exec(
select(fn.count()).select_from(
select(col(Jargon.session_id)).distinct().where(col(Jargon.session_id).is_not(None)).subquery()
)
).one()
top_chats = session.exec(
select(col(Jargon.session_id), fn.count().label("count"))
.where(col(Jargon.session_id).is_not(None))
.group_by(col(Jargon.session_id))
.order_by(fn.count().desc())
.limit(5)
).all()
top_chats_dict = {session_id: count for session_id, count in top_chats if session_id}
top_chats_dict = dict(sorted(top_chats_counter.items(), key=lambda item: item[1], reverse=True)[:5])
chat_count = len(top_chats_counter)
return JargonStatsResponse(
success=True,
@@ -360,8 +389,7 @@ async def get_jargon_detail(jargon_id: int):
"""获取黑话详情"""
try:
with get_db_session() as session:
jargon = session.exec(select(Jargon).where(col(Jargon.id) == jargon_id)).first()
if not jargon:
if not (jargon := session.exec(select(Jargon).where(col(Jargon.id) == jargon_id)).first()):
raise HTTPException(status_code=404, detail="黑话不存在")
data = JargonResponse(**jargon_to_dict(jargon, session))
@@ -379,19 +407,19 @@ async def create_jargon(request: JargonCreateRequest):
"""创建黑话"""
try:
with get_db_session() as session:
existing = session.exec(
select(Jargon).where(
(col(Jargon.content) == request.content) & (col(Jargon.session_id) == request.chat_id)
)
).first()
if existing:
same_content_jargons = session.exec(select(Jargon).where(col(Jargon.content) == request.content)).all()
existing = next(
(jargon for jargon in same_content_jargons if has_chat_id(jargon.session_id_dict, request.chat_id)),
None,
)
if existing is not None:
raise HTTPException(status_code=400, detail="该聊天中已存在相同内容的黑话")
jargon = Jargon(
content=request.content,
raw_content=request.raw_content,
meaning=request.meaning or "",
session_id=request.chat_id,
session_id_dict=build_session_id_dict_for_chat(request.chat_id),
count=0,
is_jargon=None,
is_complete=False,
@@ -420,13 +448,12 @@ async def update_jargon(jargon_id: int, request: JargonUpdateRequest):
if not jargon:
raise HTTPException(status_code=404, detail="黑话不存在")
update_data = request.model_dump(exclude_unset=True)
if update_data:
if update_data := request.model_dump(exclude_unset=True):
for field, value in update_data.items():
if field == "is_global":
continue
if field == "chat_id":
jargon.session_id = value
jargon.session_id_dict = build_session_id_dict_for_chat(value, max(jargon.count, 1))
continue
if value is not None or field in ["meaning", "raw_content", "is_jargon"]:
setattr(jargon, field, value)