Build lightweight AI agent admin
This commit is contained in:
@@ -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
|
||||
Reference in New Issue
Block a user