feat: import monitor & knowledge ops enhancements
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 13s

This commit is contained in:
wangdl 2026-07-08 19:34:11 +08:00
parent 12ca018ead
commit 26f20cb826
8 changed files with 26 additions and 49 deletions

1
.gitignore vendored
View File

@ -23,3 +23,4 @@ dist-ssr
*.sln *.sln
*.sw? *.sw?
.env .env
uploads/

View File

@ -6,4 +6,5 @@ export const importsAdminAPI = {
detail: (id: string) => api.get<ImportJobDetail>(`/admin-api/imports/${id}`), detail: (id: string) => api.get<ImportJobDetail>(`/admin-api/imports/${id}`),
retry: (id: string) => api.post(`/admin-api/imports/${id}/retry`), retry: (id: string) => api.post(`/admin-api/imports/${id}/retry`),
batchRetry: (ids: string[]) => api.post('/admin-api/imports/batch-retry', { ids }), batchRetry: (ids: string[]) => api.post('/admin-api/imports/batch-retry', { ids }),
remove: (id: string) => api.delete<{ success: boolean; deletedId: string; deletedSource: boolean }>(`/admin-api/imports/${id}`),
} }

View File

@ -1,5 +1,5 @@
import { api } from '@/lib/api-client' import { api } from '@/lib/api-client'
import type { KnowledgeBase, KnowledgeSource, PaginatedResult, SourceReference, KnowledgeCandidate, KnowledgeChunk } from '@/types' import type { KnowledgeBase, KnowledgeSource, PaginatedResult, SourceReference, KnowledgeChunk } from '@/types'
export const knowledgeAPI = { export const knowledgeAPI = {
list(page = 1, limit = 20) { list(page = 1, limit = 20) {
@ -7,7 +7,6 @@ export const knowledgeAPI = {
}, },
getById(id: string) { return api.get<KnowledgeBase>(`/admin-api/knowledge-bases/${id}`) }, getById(id: string) { return api.get<KnowledgeBase>(`/admin-api/knowledge-bases/${id}`) },
remove(id: string) { return api.delete<void>(`/admin-api/knowledge-bases/${id}`) }, remove(id: string) { return api.delete<void>(`/admin-api/knowledge-bases/${id}`) },
candidates() { return api.get<KnowledgeCandidate[]>('/admin-api/knowledge-bases/candidates?limit=50') },
chunks(sourceId: string) { return api.get<KnowledgeChunk[]>(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) }, chunks(sourceId: string) { return api.get<KnowledgeChunk[]>(`/admin-api/knowledge-bases/chunks?sourceId=${sourceId}&limit=50`) },
sources(kbId: string) { return api.get<KnowledgeSource[]>(`/admin-api/knowledge-bases/${kbId}/sources`) }, sources(kbId: string) { return api.get<KnowledgeSource[]>(`/admin-api/knowledge-bases/${kbId}/sources`) },
sourceReferences(sourceId: string) { return api.get<SourceReference[]>(`/admin-api/knowledge-bases/sources/${sourceId}/references`) }, sourceReferences(sourceId: string) { return api.get<SourceReference[]>(`/admin-api/knowledge-bases/sources/${sourceId}/references`) },

View File

@ -91,7 +91,6 @@ export const importStatusLabels: Record<string, string> = {
CHUNKING: '分段中', CHUNKING: '分段中',
EMBEDDING: '向量化中', EMBEDDING: '向量化中',
INDEXING: '索引中', INDEXING: '索引中',
GENERATING_CANDIDATES: '生成候选中',
COMPLETED: '已完成', COMPLETED: '已完成',
FAILED: '失败', FAILED: '失败',
FAILED_RETRYABLE: '失败(可重试)', FAILED_RETRYABLE: '失败(可重试)',
@ -109,7 +108,6 @@ export const importStepLabels: Record<string, string> = {
CHUNKING: '文本分段', CHUNKING: '文本分段',
EMBEDDING: '向量化', EMBEDDING: '向量化',
INDEXING: '写入索引', INDEXING: '写入索引',
GENERATING_CANDIDATES: 'AI 生成候选',
COMPLETED: '已完成', COMPLETED: '已完成',
FAILED: '失败', FAILED: '失败',
FAILED_RETRYABLE: '失败(可重试)', FAILED_RETRYABLE: '失败(可重试)',
@ -130,11 +128,6 @@ export const userStatusLabels: Record<string, string> = {
// ── Knowledge ── // ── Knowledge ──
export const kbStatusLabels: Record<string, string> = { active: '正常' }; export const kbStatusLabels: Record<string, string> = { active: '正常' };
export const candidateStatusLabels: Record<string, string> = {
PENDING: '待审核',
ACCEPTED: '已通过',
REJECTED: '已拒绝',
};
// ── Hermes / Agent ── // ── Hermes / Agent ──
export const agentTaskStatusLabels: Record<string, string> = { export const agentTaskStatusLabels: Record<string, string> = {

View File

@ -1,7 +1,7 @@
import { useState, useCallback, useRef } from 'react' import { useState, useCallback, useRef } from 'react'
import { useQuery, useQueryClient } from '@tanstack/react-query' import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Table, Tag, Typography, Button, App, Drawer, Space, Select, Popconfirm, Row, Col, Checkbox } from 'antd' import { Table, Tag, Typography, Button, App, Drawer, Space, Select, Popconfirm, Row, Col, Checkbox } from 'antd'
import { ReloadOutlined, RetweetOutlined, ImportOutlined, EyeOutlined } from '@ant-design/icons' import { ReloadOutlined, RetweetOutlined, ImportOutlined, EyeOutlined, DeleteOutlined } from '@ant-design/icons'
import { importsAdminAPI, knowledgeAPI } from '@/api' import { importsAdminAPI, knowledgeAPI } from '@/api'
import { importStatusLabels, importStepLabels } from '@/constants/labels' import { importStatusLabels, importStepLabels } from '@/constants/labels'
import type { TablePaginationConfig } from 'antd' import type { TablePaginationConfig } from 'antd'
@ -73,6 +73,17 @@ export default function ImportMonitorPage() {
qc.invalidateQueries({ queryKey: ['admin-imports', 'list'] }) qc.invalidateQueries({ queryKey: ['admin-imports', 'list'] })
} }
const handleDelete = async (id: string) => {
try {
await importsAdminAPI.remove(id)
message.success('已删除')
refreshList()
} catch (err: unknown) {
const e = err as { response?: { data?: { message?: string } }; message?: string }
message.error(e?.response?.data?.message || e?.message || '删除失败')
}
}
const handleRetry = async (id: string, sourceDeleted?: boolean) => { const handleRetry = async (id: string, sourceDeleted?: boolean) => {
if (sourceDeleted) { if (sourceDeleted) {
message.warning('关联的资料来源已删除,无法重新解析。请删除此导入记录后重新上传文件。') message.warning('关联的资料来源已删除,无法重新解析。请删除此导入记录后重新上传文件。')
@ -135,7 +146,7 @@ export default function ImportMonitorPage() {
const statusColor: Record<string, string> = { const statusColor: Record<string, string> = {
COMPLETED: 'green', QUEUED: 'default', CLAIMED: 'blue', COMPLETED: 'green', QUEUED: 'default', CLAIMED: 'blue',
DOWNLOADING: 'processing', PARSING: 'processing', CHUNKING: 'processing', DOWNLOADING: 'processing', PARSING: 'processing', CHUNKING: 'processing',
EMBEDDING: 'processing', INDEXING: 'processing', GENERATING_CANDIDATES: 'processing', EMBEDDING: 'processing', INDEXING: 'processing',
FAILED: 'red', FAILED_RETRYABLE: 'orange', CANCELLED: 'default', FAILED: 'red', FAILED_RETRYABLE: 'orange', CANCELLED: 'default',
} }
@ -155,6 +166,9 @@ export default function ImportMonitorPage() {
<Space size={0}> <Space size={0}>
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => { setSelectedJob(r); setDrawerOpen(true) }}></Button> <Button type="link" size="small" icon={<EyeOutlined />} onClick={() => { setSelectedJob(r); setDrawerOpen(true) }}></Button>
<Button type="link" size="small" icon={<RetweetOutlined />} disabled={r.sourceDeleted} onClick={() => handleRetry(r.id, r.sourceDeleted)}></Button> <Button type="link" size="small" icon={<RetweetOutlined />} disabled={r.sourceDeleted} onClick={() => handleRetry(r.id, r.sourceDeleted)}></Button>
<Popconfirm title="确认删除此导入记录?关联的 Source 和文件也将被删除。" onConfirm={() => handleDelete(r.id)}>
<Button type="link" size="small" danger icon={<DeleteOutlined />}></Button>
</Popconfirm>
</Space> </Space>
) }, ) },
] ]
@ -198,8 +212,13 @@ export default function ImportMonitorPage() {
size="small" size="small"
onChange={(_pagination: TablePaginationConfig, _filters: Record<string, FilterValue>, sorter: SorterResult<ImportJob> | SorterResult<ImportJob>[]) => { onChange={(_pagination: TablePaginationConfig, _filters: Record<string, FilterValue>, sorter: SorterResult<ImportJob> | SorterResult<ImportJob>[]) => {
const s = Array.isArray(sorter) ? sorter[0] : sorter const s = Array.isArray(sorter) ? sorter[0] : sorter
if (s.columnKey) setSortBy(String(s.columnKey)) if (s.order) {
setSortOrder(s.order || 'descend') setSortBy(String(s.columnKey || 'createdAt'))
setSortOrder(s.order)
} else {
setSortBy('createdAt')
setSortOrder('descend')
}
}} }}
/> />
<Drawer title={selectedJob?.sourceName || '导入详情'} open={drawerOpen} onClose={() => { setDrawerOpen(false); setSelectedJob(null) }} width={700}> <Drawer title={selectedJob?.sourceName || '导入详情'} open={drawerOpen} onClose={() => { setDrawerOpen(false); setSelectedJob(null) }} width={700}>

View File

@ -3,8 +3,6 @@ import { useQuery, useQueryClient } from '@tanstack/react-query'
import { Table, Tag, Typography, Button, App, Tabs, Input, Card, Space } from 'antd' import { Table, Tag, Typography, Button, App, Tabs, Input, Card, Space } from 'antd'
import { ReloadOutlined, SearchOutlined, ExperimentOutlined } from '@ant-design/icons' import { ReloadOutlined, SearchOutlined, ExperimentOutlined } from '@ant-design/icons'
import { knowledgeAPI, vectorAPI } from '@/api' import { knowledgeAPI, vectorAPI } from '@/api'
import { candidateStatusLabels } from '@/constants/labels'
import dayjs from 'dayjs'
const { Title, Text } = Typography const { Title, Text } = Typography
@ -14,11 +12,6 @@ export default function KnowledgeOpsPage() {
const [sourceId, setSourceId] = useState('') const [sourceId, setSourceId] = useState('')
const [debugQuery, setDebugQuery] = useState('') const [debugQuery, setDebugQuery] = useState('')
const { data: candidates } = useQuery({
queryKey: ['ops', 'candidates'],
queryFn: () => knowledgeAPI.candidates(),
})
const { data: chunks } = useQuery({ const { data: chunks } = useQuery({
queryKey: ['ops', 'chunks', sourceId], queryKey: ['ops', 'chunks', sourceId],
queryFn: () => knowledgeAPI.chunks(sourceId), queryFn: () => knowledgeAPI.chunks(sourceId),
@ -31,15 +24,6 @@ export default function KnowledgeOpsPage() {
message.info(JSON.stringify(res.data)) message.info(JSON.stringify(res.data))
} }
const candidateCols = [
{ title: '标题', dataIndex: 'title', width: 200, ellipsis: true },
{ title: '用户', dataIndex: 'userId', width: 120, ellipsis: true, render: (_: any, r: any) => r.userName || r.userId },
{ title: '状态', dataIndex: 'status', width: 80, render: (s: string) => <Tag color={s==='PENDING'?'gold':s==='ACCEPTED'?'green':'red'}>{candidateStatusLabels[s] || 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 = [ const chunkCols = [
{ title: '#', dataIndex: 'chunkIndex', width: 50 }, { title: '#', dataIndex: 'chunkIndex', width: 50 },
{ title: '内容', dataIndex: 'content', ellipsis: true, render: (v: string) => <Text style={{ fontSize: 12 }}>{v?.slice(0, 150)}</Text> }, { title: '内容', dataIndex: 'content', ellipsis: true, render: (v: string) => <Text style={{ fontSize: 12 }}>{v?.slice(0, 150)}</Text> },
@ -57,10 +41,6 @@ export default function KnowledgeOpsPage() {
</div> </div>
<Tabs items={[ <Tabs items={[
{
key: 'candidates', label: '候选巡检',
children: <Table dataSource={candidates || []} columns={candidateCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" />,
},
{ {
key: 'chunks', label: 'Chunk 管理', key: 'chunks', label: 'Chunk 管理',
children: ( children: (

View File

@ -22,7 +22,6 @@ export type {
ImportStepLog, ImportStepLog,
ImportJobDetail, ImportJobDetail,
KnowledgeChunk, KnowledgeChunk,
KnowledgeCandidate,
} from './knowledge' } from './knowledge'
export type { export type {
ServerInfo, ServerInfo,

View File

@ -9,7 +9,6 @@ export interface KnowledgeBase {
user?: { nickname?: string; email?: string } user?: { nickname?: string; email?: string }
sourceCount?: number sourceCount?: number
chunkCount?: number chunkCount?: number
candidateCount?: number
createdAt: string createdAt: string
updatedAt?: string updatedAt?: string
} }
@ -63,7 +62,6 @@ export type ImportJobStatus =
| 'CHUNKING' | 'CHUNKING'
| 'EMBEDDING' | 'EMBEDDING'
| 'INDEXING' | 'INDEXING'
| 'GENERATING_CANDIDATES'
| 'COMPLETED' | 'COMPLETED'
| 'FAILED' | 'FAILED'
| 'FAILED_RETRYABLE' | 'FAILED_RETRYABLE'
@ -95,16 +93,3 @@ export interface KnowledgeChunk {
tokenCount?: number tokenCount?: number
confidence?: number confidence?: number
} }
// ── 候选知识点 ──
export interface KnowledgeCandidate {
id: string
knowledgeBaseId: string
sourceId: string
importId?: string
title: string
content?: string
confidence?: number
status?: string
}