Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
+490
View File
@@ -0,0 +1,490 @@
from fastapi import APIRouter, Depends, HTTPException, Query, Request
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional
from app.database import get_db
from app.base_schema import ResponseModel
from core.chat.schema import (
CreatePrivateConversationIn,
CreateGroupConversationIn,
UpdateConversationIn,
ConversationOut,
ConversationListOut,
ConversationMemberOut,
AddMembersIn,
ConversationSettingIn,
SendMessageIn,
ChatMessageOut,
ChatMessageListOut,
MarkReadIn,
)
from core.chat.service import ConversationService, ChatMessageService
router = APIRouter(prefix="/chat", tags=["聊天"])
# ============ 会话管理 ============
@router.get("/conversations", response_model=ConversationListOut, summary="我的会话列表")
async def get_conversations(
request: Request,
db: AsyncSession = Depends(get_db),
):
"""获取当前用户的会话列表"""
user_id = request.state.user_id
items = await ConversationService.get_user_conversations(db, user_id)
return ConversationListOut(
items=[ConversationOut(**item) for item in items],
total=len(items),
)
@router.post("/conversations/private", response_model=ConversationOut, summary="创建/获取单聊")
async def create_private_conversation(
request: Request,
data: CreatePrivateConversationIn,
db: AsyncSession = Depends(get_db),
):
"""创建或获取与某用户的单聊会话"""
user_id = request.state.user_id
if data.user_id == user_id:
raise HTTPException(status_code=400, detail="不能与自己创建单聊")
conv = await ConversationService.get_or_create_private(db, user_id, data.user_id)
# 获取对方用户信息
from core.user.model import User
from sqlalchemy import select
peer_result = await db.execute(select(User).where(User.id == data.user_id))
peer = peer_result.scalar_one_or_none()
member = await ConversationService.get_member(db, conv.id, user_id)
return ConversationOut(
id=conv.id,
type=conv.type,
name=conv.name,
avatar=conv.avatar,
owner_id=conv.owner_id,
last_message_time=conv.last_message_time,
last_message_preview=conv.last_message_preview,
member_count=conv.member_count,
sys_create_datetime=conv.sys_create_datetime,
unread_count=member.unread_count if member else 0,
is_muted=member.is_muted if member else False,
is_pinned=member.is_pinned if member else False,
peer_user_id=peer.id if peer else None,
peer_user_name=(peer.name or peer.username) if peer else None,
peer_user_avatar=peer.avatar if peer else None,
)
@router.post("/conversations/group", response_model=ConversationOut, summary="创建群聊")
async def create_group_conversation(
request: Request,
data: CreateGroupConversationIn,
db: AsyncSession = Depends(get_db),
):
"""创建群聊"""
user_id = request.state.user_id
if len(data.member_ids) < 1:
raise HTTPException(status_code=400, detail="群聊至少需要1个其他成员")
conv = await ConversationService.create_group(
db, name=data.name, owner_id=user_id,
member_ids=data.member_ids, avatar=data.avatar,
)
return ConversationOut(
id=conv.id,
type=conv.type,
name=conv.name,
avatar=conv.avatar,
owner_id=conv.owner_id,
last_message_time=conv.last_message_time,
last_message_preview=conv.last_message_preview,
member_count=conv.member_count,
sys_create_datetime=conv.sys_create_datetime,
)
@router.get("/conversations/{conversation_id}", response_model=ConversationOut, summary="会话详情")
async def get_conversation(
request: Request,
conversation_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取会话详情"""
user_id = request.state.user_id
conv = await ConversationService.get_by_id(db, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="会话不存在")
if not await ConversationService.is_member(db, conversation_id, user_id):
raise HTTPException(status_code=403, detail="非会话成员")
member = await ConversationService.get_member(db, conversation_id, user_id)
return ConversationOut(
id=conv.id,
type=conv.type,
name=conv.name,
avatar=conv.avatar,
owner_id=conv.owner_id,
last_message_time=conv.last_message_time,
last_message_preview=conv.last_message_preview,
member_count=conv.member_count,
sys_create_datetime=conv.sys_create_datetime,
unread_count=member.unread_count if member else 0,
is_muted=member.is_muted if member else False,
is_pinned=member.is_pinned if member else False,
)
@router.put("/conversations/{conversation_id}", response_model=ConversationOut, summary="更新群聊信息")
async def update_conversation(
request: Request,
conversation_id: str,
data: UpdateConversationIn,
db: AsyncSession = Depends(get_db),
):
"""更新群聊名称/头像"""
user_id = request.state.user_id
conv = await ConversationService.get_by_id(db, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="会话不存在")
if conv.type != "group":
raise HTTPException(status_code=400, detail="只能更新群聊信息")
if conv.owner_id != user_id:
# 检查是否是管理员
member = await ConversationService.get_member(db, conversation_id, user_id)
if not member or member.role not in ("owner", "admin"):
raise HTTPException(status_code=403, detail="无权限修改群聊信息")
updated = await ConversationService.update_conversation(
db, conversation_id, name=data.name, avatar=data.avatar,
)
return ConversationOut(
id=updated.id,
type=updated.type,
name=updated.name,
avatar=updated.avatar,
owner_id=updated.owner_id,
last_message_time=updated.last_message_time,
last_message_preview=updated.last_message_preview,
member_count=updated.member_count,
sys_create_datetime=updated.sys_create_datetime,
)
@router.delete("/conversations/{conversation_id}", response_model=ResponseModel, summary="解散群聊")
async def delete_conversation(
request: Request,
conversation_id: str,
db: AsyncSession = Depends(get_db),
):
"""解散群聊(仅群主)"""
user_id = request.state.user_id
conv = await ConversationService.get_by_id(db, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="会话不存在")
if conv.type != "group":
raise HTTPException(status_code=400, detail="单聊不能解散")
if conv.owner_id != user_id:
raise HTTPException(status_code=403, detail="只有群主可以解散群聊")
await ConversationService.delete_conversation(db, conversation_id)
return ResponseModel(message="群聊已解散")
# ============ 成员管理 ============
@router.get("/conversations/{conversation_id}/members", summary="获取成员列表")
async def get_members(
request: Request,
conversation_id: str,
db: AsyncSession = Depends(get_db),
):
"""获取会话成员列表"""
user_id = request.state.user_id
if not await ConversationService.is_member(db, conversation_id, user_id):
raise HTTPException(status_code=403, detail="非会话成员")
members = await ConversationService.get_members(db, conversation_id)
return [ConversationMemberOut(**m) for m in members]
@router.post("/conversations/{conversation_id}/members", response_model=ResponseModel, summary="添加成员")
async def add_members(
request: Request,
conversation_id: str,
data: AddMembersIn,
db: AsyncSession = Depends(get_db),
):
"""添加群成员"""
user_id = request.state.user_id
conv = await ConversationService.get_by_id(db, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="会话不存在")
if conv.type != "group":
raise HTTPException(status_code=400, detail="单聊不能添加成员")
if not await ConversationService.is_member(db, conversation_id, user_id):
raise HTTPException(status_code=403, detail="非会话成员")
added = await ConversationService.add_members(db, conversation_id, data.user_ids)
return ResponseModel(message=f"已添加 {added} 个成员")
@router.delete("/conversations/{conversation_id}/members/{member_user_id}", response_model=ResponseModel, summary="移除成员")
async def remove_member(
request: Request,
conversation_id: str,
member_user_id: str,
db: AsyncSession = Depends(get_db),
):
"""移除群成员(群主/管理员操作)"""
user_id = request.state.user_id
conv = await ConversationService.get_by_id(db, conversation_id)
if not conv:
raise HTTPException(status_code=404, detail="会话不存在")
if conv.type != "group":
raise HTTPException(status_code=400, detail="单聊不能移除成员")
member = await ConversationService.get_member(db, conversation_id, user_id)
if not member or member.role not in ("owner", "admin"):
raise HTTPException(status_code=403, detail="无权限移除成员")
if member_user_id == conv.owner_id:
raise HTTPException(status_code=400, detail="不能移除群主")
success = await ConversationService.remove_member(db, conversation_id, member_user_id)
if not success:
raise HTTPException(status_code=404, detail="成员不存在")
return ResponseModel(message="已移除成员")
# ============ 会话设置 ============
@router.put("/conversations/{conversation_id}/pin", response_model=ResponseModel, summary="置顶/取消置顶")
async def toggle_pin(
request: Request,
conversation_id: str,
data: ConversationSettingIn,
db: AsyncSession = Depends(get_db),
):
"""置顶或取消置顶会话"""
user_id = request.state.user_id
success = await ConversationService.update_setting(
db, conversation_id, user_id, "is_pinned", data.value,
)
if not success:
raise HTTPException(status_code=404, detail="会话不存在或非成员")
return ResponseModel(message="设置成功")
@router.put("/conversations/{conversation_id}/mute", response_model=ResponseModel, summary="免打扰设置")
async def toggle_mute(
request: Request,
conversation_id: str,
data: ConversationSettingIn,
db: AsyncSession = Depends(get_db),
):
"""设置或取消免打扰"""
user_id = request.state.user_id
success = await ConversationService.update_setting(
db, conversation_id, user_id, "is_muted", data.value,
)
if not success:
raise HTTPException(status_code=404, detail="会话不存在或非成员")
return ResponseModel(message="设置成功")
# ============ 在线状态 ============
@router.get("/users/online", summary="获取在线用户列表")
async def get_online_users(
request: Request,
):
"""获取当前所有在线用户ID列表"""
from core.websocket.consumers.base import manager
online_ids = list(manager.get_online_user_ids())
return {"user_ids": online_ids}
# ============ 消息管理 ============
@router.get("/messages/unread", summary="获取所有未读聊天消息")
async def get_unread_messages(
request: Request,
limit: int = Query(default=50, ge=1, le=100, description="最大数量"),
db: AsyncSession = Depends(get_db),
):
"""获取当前用户所有未读聊天消息(跨会话,按时间倒序)"""
user_id = request.state.user_id
items = await ChatMessageService.get_unread_messages(db, user_id, limit=limit)
return {"items": items, "total": len(items)}
@router.get("/conversations/{conversation_id}/messages", summary="获取消息列表")
async def get_messages(
request: Request,
conversation_id: str,
before_id: Optional[str] = Query(default=None, alias="beforeId", description="游标消息ID"),
limit: int = Query(default=30, ge=1, le=100, description="每页数量"),
db: AsyncSession = Depends(get_db),
):
"""获取消息列表(游标分页,向上加载历史)"""
user_id = request.state.user_id
if not await ConversationService.is_member(db, conversation_id, user_id):
raise HTTPException(status_code=403, detail="非会话成员")
items, has_more = await ChatMessageService.get_messages(
db, conversation_id, before_id=before_id, limit=limit,
)
return ChatMessageListOut(
items=[ChatMessageOut(**item) for item in items],
has_more=has_more,
)
@router.post("/conversations/{conversation_id}/messages", response_model=ChatMessageOut, summary="发送消息")
async def send_message(
request: Request,
conversation_id: str,
data: SendMessageIn,
db: AsyncSession = Depends(get_db),
):
"""发送消息(REST备用,主要走WebSocket"""
user_id = request.state.user_id
if not await ConversationService.is_member(db, conversation_id, user_id):
raise HTTPException(status_code=403, detail="非会话成员")
if data.msg_type == "text" and not data.content:
raise HTTPException(status_code=400, detail="文本消息内容不能为空")
if data.msg_type in ("image", "file", "voice") and not data.file_id:
raise HTTPException(status_code=400, detail="文件消息需要提供文件ID")
msg = await ChatMessageService.send_message(
db,
conversation_id=conversation_id,
sender_id=user_id,
msg_type=data.msg_type,
content=data.content,
file_id=data.file_id,
reply_to_id=data.reply_to_id,
extra=data.extra,
)
# 获取发送者信息
from core.user.model import User
from sqlalchemy import select
sender_result = await db.execute(select(User).where(User.id == user_id))
sender = sender_result.scalar_one_or_none()
# 通过WebSocket推送给会话其他成员
await _push_message_to_members(db, conversation_id, user_id, msg, sender)
return ChatMessageOut(
id=msg.id,
conversation_id=msg.conversation_id,
sender_id=msg.sender_id,
msg_type=msg.msg_type,
content=msg.content,
file_id=msg.file_id,
reply_to_id=msg.reply_to_id,
is_recalled=msg.is_recalled,
extra=msg.extra,
sys_create_datetime=msg.sys_create_datetime,
sender_name=(sender.name or sender.username) if sender else None,
sender_avatar=sender.avatar if sender else None,
)
@router.post("/messages/{message_id}/recall", response_model=ResponseModel, summary="撤回消息")
async def recall_message(
request: Request,
message_id: str,
db: AsyncSession = Depends(get_db),
):
"""撤回消息(2分钟内)"""
user_id = request.state.user_id
msg = await ChatMessageService.recall_message(db, message_id, user_id)
if not msg:
raise HTTPException(status_code=400, detail="无法撤回消息(超时或非本人消息)")
# 通过WebSocket通知撤回
member_ids = await ConversationService.get_member_user_ids(db, msg.conversation_id)
from core.websocket.consumers.base import manager
for uid in member_ids:
await manager.send_to_user(uid, {
"type": "chat.recalled",
"data": {
"conversation_id": msg.conversation_id,
"message_id": message_id,
},
})
return ResponseModel(message="消息已撤回")
@router.post("/conversations/{conversation_id}/read", response_model=ResponseModel, summary="标记已读")
async def mark_read(
request: Request,
conversation_id: str,
data: MarkReadIn,
db: AsyncSession = Depends(get_db),
):
"""标记已读到某条消息"""
user_id = request.state.user_id
success = await ChatMessageService.mark_read(
db, conversation_id, user_id, data.message_id,
)
if not success:
raise HTTPException(status_code=404, detail="会话不存在或非成员")
# 通过WebSocket通知已读回执
member_ids = await ConversationService.get_member_user_ids(db, conversation_id)
from core.websocket.consumers.base import manager
for uid in member_ids:
if uid != user_id:
await manager.send_to_user(uid, {
"type": "chat.read_receipt",
"data": {
"conversation_id": conversation_id,
"user_id": user_id,
"message_id": data.message_id,
},
})
return ResponseModel(message="已标记已读")
# ============ 辅助函数 ============
async def _push_message_to_members(db, conversation_id, sender_id, msg, sender):
"""通过WebSocket推送消息给会话成员"""
try:
member_ids = await ConversationService.get_member_user_ids(db, conversation_id)
from core.websocket.consumers.base import manager
message_data = {
"type": "chat.message",
"data": {
"id": msg.id,
"conversation_id": msg.conversation_id,
"sender_id": msg.sender_id,
"msg_type": msg.msg_type,
"content": msg.content,
"file_id": msg.file_id,
"reply_to_id": msg.reply_to_id,
"is_recalled": msg.is_recalled,
"extra": msg.extra,
"sys_create_datetime": msg.sys_create_datetime.isoformat() if msg.sys_create_datetime else None,
"sender_name": (sender.name or sender.username) if sender else None,
"sender_avatar": sender.avatar if sender else None,
},
}
for uid in member_ids:
await manager.send_to_user(uid, message_data)
except Exception as e:
import logging
logging.getLogger(__name__).error(f"推送聊天消息失败: {e}")
# 需要导入 Optional
from typing import Optional