fix: paginate chat detail messages

This commit is contained in:
2026-07-30 15:09:05 +08:00
parent 162658384f
commit 8e2dc1d781
6 changed files with 83 additions and 7 deletions

View File

@@ -52,6 +52,8 @@ const retrievalDetailOpen = ref(false);
const previewKnowledgeId = ref<number | null>(null); const previewKnowledgeId = ref<number | null>(null);
const chatDetail = ref<ChatDetail | null>(null); const chatDetail = ref<ChatDetail | null>(null);
const chatDetailOpen = ref(false); const chatDetailOpen = ref(false);
const chatDetailLoading = ref(false);
const chatDetailSessionId = ref<number | null>(null);
const selectedAiLog = ref<AiLogRecord | null>(null); const selectedAiLog = ref<AiLogRecord | null>(null);
const aiLogDetailOpen = ref(false); const aiLogDetailOpen = ref(false);
const recordTab = ref("chats"); const recordTab = ref("chats");
@@ -63,6 +65,7 @@ const pagers = reactive({
retrievals: { page: 1, pageSize: 20, total: 0 }, retrievals: { page: 1, pageSize: 20, total: 0 },
attention: { page: 1, pageSize: 20, total: 0 }, attention: { page: 1, pageSize: 20, total: 0 },
}); });
const chatDetailMessagePager = reactive({ page: 1, pageSize: 20, total: 0 });
const editingModelId = ref<number | null>(null); const editingModelId = ref<number | null>(null);
const editingKnowledgeId = ref<number | null>(null); const editingKnowledgeId = ref<number | null>(null);
const batchNodeToken = ref(""); const batchNodeToken = ref("");
@@ -739,13 +742,38 @@ async function resetChatFilters() {
} }
async function openChatDetail(row: ChatRecord) { async function openChatDetail(row: ChatRecord) {
chatDetail.value = await api.chatDetail(row.id); await openChatSession(row.id);
chatDetailOpen.value = true;
} }
async function openChatSession(sessionId: number) { async function openChatSession(sessionId: number) {
chatDetail.value = await api.chatDetail(sessionId);
chatDetailOpen.value = true; chatDetailOpen.value = true;
chatDetail.value = null;
chatDetailSessionId.value = sessionId;
Object.assign(chatDetailMessagePager, { page: 1, pageSize: chatDetailMessagePager.pageSize, total: 0 });
await loadChatDetail(sessionId, 1, chatDetailMessagePager.pageSize);
}
async function loadChatDetail(sessionId = chatDetailSessionId.value, page = chatDetailMessagePager.page, pageSize = chatDetailMessagePager.pageSize) {
if (!sessionId) return;
chatDetailLoading.value = true;
try {
const detail = await api.chatDetail(sessionId, { messagePage: page, messagePageSize: pageSize });
chatDetail.value = detail;
const messagePage = detail.messagesPage;
Object.assign(chatDetailMessagePager, {
page: messagePage?.page ?? page,
pageSize: messagePage?.pageSize ?? pageSize,
total: messagePage?.total ?? detail.messages.length,
});
} catch (error) {
ElMessage.error(error instanceof Error ? error.message : "聊天详情加载失败");
} finally {
chatDetailLoading.value = false;
}
}
async function changeChatDetailMessagePage(page: number, pageSize: number) {
await loadChatDetail(chatDetailSessionId.value, page, pageSize);
} }
async function openAiLogDetail(row: AiLogRecord) { async function openAiLogDetail(row: AiLogRecord) {
@@ -1206,6 +1234,7 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</el-drawer> </el-drawer>
<el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情"> <el-drawer v-model="chatDetailOpen" size="760px" title="聊天详情">
<div v-loading="chatDetailLoading" class="chat-detail-body">
<template v-if="chatDetail"> <template v-if="chatDetail">
<section class="chat-summary"> <section class="chat-summary">
<div><span>会话ID</span><strong>{{ chatDetail.session.id }}</strong></div> <div><span>会话ID</span><strong>{{ chatDetail.session.id }}</strong></div>
@@ -1235,6 +1264,12 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</div> </div>
</article> </article>
</section> </section>
<AdminPagination
:page="chatDetailMessagePager.page"
:page-size="chatDetailMessagePager.pageSize"
:total="chatDetailMessagePager.total"
@change="changeChatDetailMessagePage"
/>
<h3 class="detail-title">关联 AI 请求</h3> <h3 class="detail-title">关联 AI 请求</h3>
<el-collapse> <el-collapse>
@@ -1263,6 +1298,8 @@ function formatRecordDateTime(value: string, boundary: "start" | "end") {
</el-collapse-item> </el-collapse-item>
</el-collapse> </el-collapse>
</template> </template>
<el-empty v-else-if="!chatDetailLoading" description="聊天详情加载失败或暂无数据" :image-size="80" />
</div>
</el-drawer> </el-drawer>
<el-drawer v-model="aiLogDetailOpen" size="820px" title="AI 请求详情"> <el-drawer v-model="aiLogDetailOpen" size="820px" title="AI 请求详情">

View File

@@ -201,7 +201,8 @@ export const api = {
saveConfig: (payload: Record<string, unknown>) => saveConfig: (payload: Record<string, unknown>) =>
request<SystemConfigItem>("/admin/config", { method: "PUT", body: JSON.stringify(payload) }), request<SystemConfigItem>("/admin/config", { method: "PUT", body: JSON.stringify(payload) }),
chats: (query: ChatRecordQuery = {}) => request<PageResult<ChatRecord>>(`/admin/chat/list${queryString(query)}`), chats: (query: ChatRecordQuery = {}) => request<PageResult<ChatRecord>>(`/admin/chat/list${queryString(query)}`),
chatDetail: (sessionId: number) => request<ChatDetail>(`/admin/chat/${sessionId}`), chatDetail: (sessionId: number, query: { messagePage?: number; messagePageSize?: number } = {}) =>
request<ChatDetail>(`/admin/chat/${sessionId}${queryString(query)}`),
exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"), exportChats: (query: ChatRecordQuery = {}) => download(`/admin/chat/export${queryString(query)}`, "chat_records.csv"),
aiLogs: (query: { sessionId?: number; userId?: number; status?: string; page?: number; pageSize?: number } = {}) => aiLogs: (query: { sessionId?: number; userId?: number; status?: string; page?: number; pageSize?: number } = {}) =>
request<PageResult<AiLogRecord>>(`/admin/ai-log/list${queryString(query)}`), request<PageResult<AiLogRecord>>(`/admin/ai-log/list${queryString(query)}`),

View File

@@ -1738,6 +1738,10 @@ textarea {
align-items: center; align-items: center;
} }
.chat-detail-body {
min-height: 320px;
}
.chat-summary { .chat-summary {
display: grid; display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr)); grid-template-columns: repeat(4, minmax(0, 1fr));

View File

@@ -356,6 +356,7 @@ export interface RetrievedKnowledgeChunk {
export interface ChatDetail { export interface ChatDetail {
session: ChatRecord; session: ChatRecord;
messages: ChatMessageRecord[]; messages: ChatMessageRecord[];
messagesPage?: PageResult<ChatMessageRecord>;
aiLogs: AiLogRecord[]; aiLogs: AiLogRecord[];
} }

View File

@@ -83,6 +83,8 @@ def export_chats(
@router.get("/chat/{session_id}") @router.get("/chat/{session_id}")
def chat_detail( def chat_detail(
session_id: int, session_id: int,
messagePage: int = Query(default=1, ge=1),
messagePageSize: int = Query(default=20, ge=10, le=100),
db: Session = Depends(get_db), db: Session = Depends(get_db),
current_admin: Admin = Depends(get_current_admin), current_admin: Admin = Depends(get_current_admin),
) -> dict: ) -> dict:
@@ -97,10 +99,13 @@ def chat_detail(
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="会话不存在") raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="会话不存在")
session, user = session_row session, user = session_row
message_query = select(ChatMessage).where(ChatMessage.session_id == session_id)
message_total = db.scalar(select(func.count()).select_from(message_query.subquery())) or 0
messages = db.scalars( messages = db.scalars(
select(ChatMessage) message_query
.where(ChatMessage.session_id == session_id)
.order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc()) .order_by(ChatMessage.created_at.asc(), ChatMessage.id.asc())
.offset((messagePage - 1) * messagePageSize)
.limit(messagePageSize)
).all() ).all()
ai_logs = db.scalars( ai_logs = db.scalars(
select(AiRequestLog) select(AiRequestLog)
@@ -111,6 +116,12 @@ def chat_detail(
{ {
"session": _chat_row_dict(session, user), "session": _chat_row_dict(session, user),
"messages": [_message_dict(item) for item in messages], "messages": [_message_dict(item) for item in messages],
"messagesPage": page_result(
[_message_dict(item) for item in messages],
total=message_total,
page=messagePage,
page_size=messagePageSize,
),
"aiLogs": [_ai_log_dict(item, include_prompt=True) for item in ai_logs], "aiLogs": [_ai_log_dict(item, include_prompt=True) for item in ai_logs],
} }
) )

View File

@@ -3,9 +3,10 @@ from sqlalchemy.orm import Session
from sqlalchemy.pool import StaticPool from sqlalchemy.pool import StaticPool
from app.api.admin_agent_records import attention_list, retrieval_logs from app.api.admin_agent_records import attention_list, retrieval_logs
from app.api.admin_records import ai_logs from app.api.admin_records import ai_logs, chat_detail
from app.api.admin_users import list_users from app.api.admin_users import list_users
from app.models import Base from app.models import Base
from app.models.chat import ChatMessage, ChatSession
from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog from app.models.knowledge import HumanAttentionRecord, KnowledgeRetrievalLog
from app.models.logs import AiRequestLog from app.models.logs import AiRequestLog
from app.models.user import User from app.models.user import User
@@ -43,6 +44,27 @@ def test_ai_log_page_does_not_load_large_detail_fields():
assert all(item["retrievedChunks"] == [] for item in response["data"]["items"]) assert all(item["retrievedChunks"] == [] for item in response["data"]["items"])
def test_chat_detail_messages_are_paginated():
with _database() as db:
user = User(id=1, phone="13800000000", name="学员", daily_chat_limit=10)
session = ChatSession(id=1, user_id=1, title="长会话", message_count=25)
db.add_all([user, session])
db.add_all([
ChatMessage(id=index + 1, session_id=1, user_id=1, role="user", content=f"消息{index + 1}")
for index in range(25)
])
db.commit()
response = chat_detail(1, messagePage=2, messagePageSize=10, db=db, current_admin=object())
data = response["data"]
assert data["session"]["messageCount"] == 25
assert data["messagesPage"]["total"] == 25
assert data["messagesPage"]["page"] == 2
assert len(data["messages"]) == 10
assert data["messages"][0]["content"] == "消息11"
def test_retrieval_and_attention_lists_are_paginated(): def test_retrieval_and_attention_lists_are_paginated():
with _database() as db: with _database() as db:
for index in range(21): for index in range(21):