feat: M2-08 admin — KnowledgeOps page (candidates, chunks, RAG debug)
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 9s

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

View File

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

View File

@ -23,6 +23,7 @@ export const adminMenuItems: AdminMenuItem[] = [
{ path: '/knowledge', name: '知识库管理', icon: <BookOutlined />, children: [ { path: '/knowledge', name: '知识库管理', icon: <BookOutlined />, children: [
{ path: '/knowledge/bases', name: '知识库列表' }, { path: '/knowledge/bases', name: '知识库列表' },
{ path: '/knowledge/sources', name: '知识源列表' }, { path: '/knowledge/sources', name: '知识源列表' },
{ path: '/knowledge/ops', name: '知识运维' },
]}, ]},
{ path: '/imports', name: '文档导入', icon: <ImportOutlined /> }, { path: '/imports', name: '文档导入', icon: <ImportOutlined /> },
{ path: '/files', name: '文件与 COS', icon: <FileOutlined /> }, { path: '/files', name: '文件与 COS', icon: <FileOutlined /> },

View File

@ -0,0 +1,90 @@
import { useState } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Table, Tag, Typography, Button, App, Tabs, Input, Card, Space } from 'antd'
import { ReloadOutlined, SearchOutlined, ExperimentOutlined } from '@ant-design/icons'
import { api } from '@/services/http-client'
import dayjs from 'dayjs'
const { Title, Text } = Typography
export default function KnowledgeOpsPage() {
const qc = useQueryClient()
const { message } = App.useApp()
const [sourceId, setSourceId] = useState('')
const [debugQuery, setDebugQuery] = useState('')
const { data: candidates } = useQuery({
queryKey: ['ops', 'candidates'],
queryFn: (): Promise<any> => api.get('/admin-api/knowledge-bases/candidates?limit=50'),
})
const { data: chunks } = useQuery({
queryKey: ['ops', 'chunks', sourceId],
queryFn: (): Promise<any> => api.get(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`),
enabled: !!sourceId,
})
const handleDebugSearch = async () => {
if (!debugQuery) return
const res: any = await api.post('/admin-api/vector/debug-search', { query: debugQuery })
message.info(JSON.stringify(res.data))
}
const candidateCols = [
{ title: '标题', dataIndex: 'title', width: 200, ellipsis: true },
{ title: '用户', dataIndex: 'userId', width: 120, ellipsis: true },
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => <Tag color={s==='PENDING'?'gold':s==='ACCEPTED'?'green':'red'}>{s}</Tag> },
{ title: '置信度', dataIndex: 'confidence', width: 80, align: 'center' as const, render: (v: any) => v ? `${(Number(v)*100).toFixed(0)}%` : '-' },
{ title: '难度', dataIndex: 'difficulty', width: 70 },
{ title: '时间', dataIndex: 'createdAt', width: 130, render: (d: string) => dayjs(d).format('MM-DD HH:mm') },
]
const chunkCols = [
{ title: '#', dataIndex: 'chunkIndex', width: 50 },
{ title: '内容', dataIndex: 'content', ellipsis: true, render: (v: string) => <Text style={{ fontSize: 12 }}>{v?.slice(0, 150)}</Text> },
{ title: '页', dataIndex: 'pageNumber', width: 50 },
{ title: '章节', dataIndex: 'sectionTitle', width: 150, ellipsis: true },
{ title: 'Tokens', dataIndex: 'tokenCount', width: 70, align: 'center' as const },
{ title: 'Embedding', dataIndex: 'embeddingStatus', width: 80, render: (s: string) => <Tag color={s==='done'?'green':'default'}>{s}</Tag> },
]
return (
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 16 }}>
<Title level={5} style={{ margin: 0 }}><ExperimentOutlined /> </Title>
<Button icon={<ReloadOutlined />} onClick={() => qc.invalidateQueries({ queryKey: ['ops'] })}></Button>
</div>
<Tabs items={[
{
key: 'candidates', label: '候选巡检',
children: <Table dataSource={candidates || []} columns={candidateCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" />,
},
{
key: 'chunks', label: 'Chunk 管理',
children: (
<>
<Space style={{ marginBottom: 16 }}>
<Input.Search placeholder="输入 Source ID" value={sourceId} onChange={e => setSourceId(e.target.value)} onSearch={v => setSourceId(v)} enterButton={<SearchOutlined />} style={{ width: 300 }} />
</Space>
{sourceId && <Table dataSource={chunks || []} columns={chunkCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" />}
</>
),
},
{
key: 'rag-debug', label: 'RAG 调试',
children: (
<Card size="small" title="检索调试">
<Space>
<Input.Search placeholder="输入查询文本" value={debugQuery} onChange={e => setDebugQuery(e.target.value)} onSearch={handleDebugSearch} enterButton="检索" style={{ width: 400 }} />
</Space>
<div style={{ marginTop: 8 }}>
<Text type="secondary"> RAG M3 </Text>
</div>
</Card>
),
},
]} />
</div>
)
}