Merge pull request #670 from wanxunyang/feature/app-message-log
feat(app): add app message log query API
This commit is contained in:
@@ -8,6 +8,7 @@ from fastapi import APIRouter
|
|||||||
from . import (
|
from . import (
|
||||||
api_key_controller,
|
api_key_controller,
|
||||||
app_controller,
|
app_controller,
|
||||||
|
app_log_controller,
|
||||||
auth_controller,
|
auth_controller,
|
||||||
chunk_controller,
|
chunk_controller,
|
||||||
document_controller,
|
document_controller,
|
||||||
@@ -70,6 +71,7 @@ manager_router.include_router(chunk_controller.router)
|
|||||||
manager_router.include_router(test_controller.router)
|
manager_router.include_router(test_controller.router)
|
||||||
manager_router.include_router(knowledgeshare_controller.router)
|
manager_router.include_router(knowledgeshare_controller.router)
|
||||||
manager_router.include_router(app_controller.router)
|
manager_router.include_router(app_controller.router)
|
||||||
|
manager_router.include_router(app_log_controller.router)
|
||||||
manager_router.include_router(upload_controller.router)
|
manager_router.include_router(upload_controller.router)
|
||||||
manager_router.include_router(memory_agent_controller.router)
|
manager_router.include_router(memory_agent_controller.router)
|
||||||
manager_router.include_router(memory_dashboard_controller.router)
|
manager_router.include_router(memory_dashboard_controller.router)
|
||||||
|
|||||||
129
api/app/controllers/app_log_controller.py
Normal file
129
api/app/controllers/app_log_controller.py
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
"""应用日志(消息记录)接口"""
|
||||||
|
import uuid
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, Query
|
||||||
|
from sqlalchemy import select, desc, func
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
|
from app.core.logging_config import get_business_logger
|
||||||
|
from app.core.response_utils import success
|
||||||
|
from app.db import get_db
|
||||||
|
from app.dependencies import get_current_user, cur_workspace_access_guard
|
||||||
|
from app.models.conversation_model import Conversation, Message
|
||||||
|
from app.schemas.app_log_schema import AppLogConversation, AppLogConversationDetail, AppLogMessage
|
||||||
|
from app.schemas.response_schema import PageData, PageMeta
|
||||||
|
from app.services.app_service import AppService
|
||||||
|
|
||||||
|
router = APIRouter(prefix="/apps", tags=["App Logs"])
|
||||||
|
logger = get_business_logger()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{app_id}/logs", summary="应用日志 - 会话列表")
|
||||||
|
@cur_workspace_access_guard()
|
||||||
|
def list_app_logs(
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
pagesize: int = Query(20, ge=1, le=100),
|
||||||
|
user_id: Optional[str] = None,
|
||||||
|
is_draft: Optional[bool] = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""查看应用下所有会话记录(分页)
|
||||||
|
|
||||||
|
- 支持按 user_id 筛选
|
||||||
|
- 支持按 is_draft 筛选(草稿会话 / 发布会话)
|
||||||
|
- 按最新更新时间倒序排列
|
||||||
|
"""
|
||||||
|
workspace_id = current_user.current_workspace_id
|
||||||
|
|
||||||
|
# 验证应用访问权限
|
||||||
|
service = AppService(db)
|
||||||
|
service.get_app(app_id, workspace_id)
|
||||||
|
|
||||||
|
stmt = select(Conversation).where(
|
||||||
|
Conversation.app_id == app_id,
|
||||||
|
Conversation.workspace_id == workspace_id,
|
||||||
|
Conversation.is_active.is_(True),
|
||||||
|
)
|
||||||
|
|
||||||
|
if user_id:
|
||||||
|
stmt = stmt.where(Conversation.user_id == user_id)
|
||||||
|
|
||||||
|
if is_draft is not None:
|
||||||
|
stmt = stmt.where(Conversation.is_draft == is_draft)
|
||||||
|
|
||||||
|
total = int(db.execute(
|
||||||
|
select(func.count()).select_from(stmt.subquery())
|
||||||
|
).scalar_one())
|
||||||
|
|
||||||
|
stmt = stmt.order_by(desc(Conversation.updated_at))
|
||||||
|
stmt = stmt.offset((page - 1) * pagesize).limit(pagesize)
|
||||||
|
|
||||||
|
conversations = list(db.scalars(stmt).all())
|
||||||
|
|
||||||
|
items = [AppLogConversation.model_validate(c) for c in conversations]
|
||||||
|
meta = PageMeta(page=page, pagesize=pagesize, total=total, hasnext=(page * pagesize) < total)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"查询应用日志会话列表",
|
||||||
|
extra={"app_id": str(app_id), "total": total, "page": page}
|
||||||
|
)
|
||||||
|
|
||||||
|
return success(data=PageData(page=meta, items=items))
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{app_id}/logs/{conversation_id}", summary="应用日志 - 会话消息详情")
|
||||||
|
@cur_workspace_access_guard()
|
||||||
|
def get_app_log_detail(
|
||||||
|
app_id: uuid.UUID,
|
||||||
|
conversation_id: uuid.UUID,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
current_user=Depends(get_current_user),
|
||||||
|
):
|
||||||
|
"""查看某会话的完整消息记录
|
||||||
|
|
||||||
|
- 返回会话基本信息 + 所有消息(按时间正序)
|
||||||
|
- 消息 meta_data 包含模型名、token 用量等信息
|
||||||
|
"""
|
||||||
|
workspace_id = current_user.current_workspace_id
|
||||||
|
|
||||||
|
# 验证应用访问权限
|
||||||
|
service = AppService(db)
|
||||||
|
service.get_app(app_id, workspace_id)
|
||||||
|
|
||||||
|
# 查询会话(确保属于该应用和工作空间)
|
||||||
|
conversation = db.scalars(
|
||||||
|
select(Conversation).where(
|
||||||
|
Conversation.id == conversation_id,
|
||||||
|
Conversation.app_id == app_id,
|
||||||
|
Conversation.workspace_id == workspace_id,
|
||||||
|
Conversation.is_active.is_(True),
|
||||||
|
)
|
||||||
|
).first()
|
||||||
|
|
||||||
|
if not conversation:
|
||||||
|
from app.core.exceptions import ResourceNotFoundException
|
||||||
|
raise ResourceNotFoundException("会话", str(conversation_id))
|
||||||
|
|
||||||
|
# 查询消息(按时间正序)
|
||||||
|
messages = list(db.scalars(
|
||||||
|
select(Message)
|
||||||
|
.where(Message.conversation_id == conversation_id)
|
||||||
|
.order_by(Message.created_at)
|
||||||
|
).all())
|
||||||
|
|
||||||
|
detail = AppLogConversationDetail.model_validate(conversation)
|
||||||
|
detail.messages = [AppLogMessage.model_validate(m) for m in messages]
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"查询应用日志会话详情",
|
||||||
|
extra={
|
||||||
|
"app_id": str(app_id),
|
||||||
|
"conversation_id": str(conversation_id),
|
||||||
|
"message_count": len(messages)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
return success(data=detail)
|
||||||
53
api/app/schemas/app_log_schema.py
Normal file
53
api/app/schemas/app_log_schema.py
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
"""应用日志(消息记录)Schema"""
|
||||||
|
import uuid
|
||||||
|
import datetime
|
||||||
|
from typing import Optional, Dict, Any, List
|
||||||
|
|
||||||
|
from pydantic import BaseModel, Field, ConfigDict, field_serializer
|
||||||
|
|
||||||
|
|
||||||
|
class AppLogMessage(BaseModel):
|
||||||
|
"""单条消息记录"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
conversation_id: uuid.UUID
|
||||||
|
role: str = Field(description="角色: user / assistant / system")
|
||||||
|
content: str
|
||||||
|
meta_data: Optional[Dict[str, Any]] = None
|
||||||
|
created_at: datetime.datetime
|
||||||
|
|
||||||
|
@field_serializer("created_at", when_used="json")
|
||||||
|
def _serialize_created_at(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
@field_serializer("meta_data", when_used="json")
|
||||||
|
def _serialize_meta_data(self, data: Optional[Dict[str, Any]]):
|
||||||
|
return data or {}
|
||||||
|
|
||||||
|
|
||||||
|
class AppLogConversation(BaseModel):
|
||||||
|
"""会话摘要(用于列表)"""
|
||||||
|
model_config = ConfigDict(from_attributes=True)
|
||||||
|
|
||||||
|
id: uuid.UUID
|
||||||
|
app_id: uuid.UUID
|
||||||
|
user_id: Optional[str] = None
|
||||||
|
title: Optional[str] = None
|
||||||
|
message_count: int = 0
|
||||||
|
is_draft: bool
|
||||||
|
created_at: datetime.datetime
|
||||||
|
updated_at: datetime.datetime
|
||||||
|
|
||||||
|
@field_serializer("created_at", when_used="json")
|
||||||
|
def _serialize_created_at(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
@field_serializer("updated_at", when_used="json")
|
||||||
|
def _serialize_updated_at(self, dt: datetime.datetime):
|
||||||
|
return int(dt.timestamp() * 1000) if dt else None
|
||||||
|
|
||||||
|
|
||||||
|
class AppLogConversationDetail(AppLogConversation):
|
||||||
|
"""会话详情(包含消息列表)"""
|
||||||
|
messages: List[AppLogMessage] = Field(default_factory=list)
|
||||||
Reference in New Issue
Block a user