feat: M2-07 audit — admin chat log viewer page
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 10s

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
WangDL 2026-05-24 13:58:40 +08:00
parent 1c864f0de1
commit d77423ef95
3 changed files with 65 additions and 0 deletions

View File

@ -31,6 +31,7 @@ const UserManagement = lazy(() => import('./pages/UserManagement'))
const MemberManagement = lazy(() => import('./pages/MemberManagement'))
const ImportMonitorPage = lazy(() => import('./pages/ImportMonitor'))
const KnowledgeOpsPage = lazy(() => import('./pages/KnowledgeOps'))
const ChatLogsPage = lazy(() => import('./pages/ChatLogs'))
const HermesSettings = lazy(() => import('./pages/HermesSettings'))
const TaskAssistant = lazy(() => import('./pages/TaskAssistant'))
const Placeholder = lazy(() => import('./pages/Placeholder'))
@ -102,6 +103,7 @@ function App() {
<Route path="knowledge/bases" element={<Suspense fallback={<PageLoading />}><KnowledgeBasesPage /></Suspense>} />
<Route path="knowledge/sources" element={<Placeholder title="知识源列表" />} />
<Route path="knowledge/ops" element={<Suspense fallback={<PageLoading />}><KnowledgeOpsPage /></Suspense>} />
<Route path="chat-logs" element={<Suspense fallback={<PageLoading />}><ChatLogsPage /></Suspense>} />
<Route path="imports" element={<Suspense fallback={<PageLoading />}><ImportMonitorPage /></Suspense>} />
<Route path="files" element={<Suspense fallback={<PageLoading />}><FilesAdminPage /></Suspense>} />
<Route

View File

@ -37,6 +37,7 @@ export const adminMenuItems: AdminMenuItem[] = [
{ path: '/metrics', name: '接口监控' },
{ path: '/ai-gateway', name: 'AI Gateway' },
{ path: '/servers', name: '服务器' },
{ path: '/chat-logs', name: '对话日志' },
{ path: '/events', name: '事件队列' },
{ path: '/vector', name: '向量检索' },
{ path: '/config', name: '配置管理' },

62
src/pages/ChatLogs.tsx Normal file
View File

@ -0,0 +1,62 @@
import { useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Table, Tag, Typography, Button, Drawer } from 'antd'
import { ReloadOutlined, MessageOutlined, EyeOutlined } from '@ant-design/icons'
import { api } from '@/services/http-client'
import dayjs from 'dayjs'
const { Title, Text } = Typography
export default function ChatLogsPage() {
const qc = useQueryClient()
const [drawerOpen, setDrawerOpen] = useState(false)
const [selectedSession, setSelectedSession] = useState<any>(null)
const { data: sessions, isLoading } = useQuery({
queryKey: ['chat-logs', 'sessions'],
queryFn: (): Promise<any> => api.get('/admin-api/rag-chat/sessions'),
staleTime: 10_000,
})
const { data: messages } = useQuery({
queryKey: ['chat-logs', 'messages', selectedSession?.id],
queryFn: (): Promise<any> => api.get(`/admin-api/rag-chat/sessions/${selectedSession?.id}/messages`),
enabled: !!selectedSession?.id,
})
const msgCols = [
{ title: '角色', dataIndex: 'role', width: 70, render: (r: string) => <Tag color={r==='user'?'blue':'green'}>{r==='user'?'用户':'AI'}</Tag> },
{ title: '内容', dataIndex: 'content', ellipsis: true, render: (v: string) => <Text style={{ fontSize: 13 }}>{v?.slice(0, 300)}</Text> },
{ title: 'Tokens', dataIndex: 'tokens', width: 70, align: 'center' as const },
{ title: '时间', dataIndex: 'createdAt', width: 130, render: (d: string) => dayjs(d).format('HH:mm:ss') },
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Title level={5} style={{ margin: 0 }}><MessageOutlined /> </Title>
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['chat-logs'] })}></Button>
</div>
<Table
dataSource={sessions || []} loading={isLoading} rowKey="id"
pagination={{ pageSize: 20 }} size="small"
columns={[
{ title: '标题', dataIndex: 'title', width: 200, ellipsis: true },
{ title: '用户', dataIndex: 'userId', width: 140, ellipsis: true },
{ title: '知识库', dataIndex: 'knowledgeBaseId', width: 140, ellipsis: true },
{ title: '消息数', dataIndex: ['_count','messages'], width: 70, align: 'center' },
{ title: '更新时间', dataIndex: 'updatedAt', width: 140, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
{
title: '操作', width: 70,
render: (_: any, r: any) => (
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => { setSelectedSession(r); setDrawerOpen(true) }}></Button>
),
},
]}
/>
<Drawer title={`对话: ${selectedSession?.title || ''}`} open={drawerOpen} onClose={() => setDrawerOpen(false)} width={700}>
<Table dataSource={messages || []} columns={msgCols} rowKey="id" pagination={false} size="small" />
</Drawer>
</div>
)
}