feat: import monitor & knowledge ops enhancements
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 13s
All checks were successful
Deploy Admin Frontend / build-and-deploy (push) Successful in 13s
This commit is contained in:
parent
12ca018ead
commit
26f20cb826
1
.gitignore
vendored
1
.gitignore
vendored
@ -23,3 +23,4 @@ dist-ssr
|
||||
*.sln
|
||||
*.sw?
|
||||
.env
|
||||
uploads/
|
||||
|
||||
@ -6,4 +6,5 @@ export const importsAdminAPI = {
|
||||
detail: (id: string) => api.get<ImportJobDetail>(`/admin-api/imports/${id}`),
|
||||
retry: (id: string) => api.post(`/admin-api/imports/${id}/retry`),
|
||||
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}`),
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
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 = {
|
||||
list(page = 1, limit = 20) {
|
||||
@ -7,7 +7,6 @@ export const knowledgeAPI = {
|
||||
},
|
||||
getById(id: string) { return api.get<KnowledgeBase>(`/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`) },
|
||||
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`) },
|
||||
|
||||
@ -91,7 +91,6 @@ export const importStatusLabels: Record<string, string> = {
|
||||
CHUNKING: '分段中',
|
||||
EMBEDDING: '向量化中',
|
||||
INDEXING: '索引中',
|
||||
GENERATING_CANDIDATES: '生成候选中',
|
||||
COMPLETED: '已完成',
|
||||
FAILED: '失败',
|
||||
FAILED_RETRYABLE: '失败(可重试)',
|
||||
@ -109,7 +108,6 @@ export const importStepLabels: Record<string, string> = {
|
||||
CHUNKING: '文本分段',
|
||||
EMBEDDING: '向量化',
|
||||
INDEXING: '写入索引',
|
||||
GENERATING_CANDIDATES: 'AI 生成候选',
|
||||
COMPLETED: '已完成',
|
||||
FAILED: '失败',
|
||||
FAILED_RETRYABLE: '失败(可重试)',
|
||||
@ -130,11 +128,6 @@ export const userStatusLabels: Record<string, string> = {
|
||||
|
||||
// ── Knowledge ──
|
||||
export const kbStatusLabels: Record<string, string> = { active: '正常' };
|
||||
export const candidateStatusLabels: Record<string, string> = {
|
||||
PENDING: '待审核',
|
||||
ACCEPTED: '已通过',
|
||||
REJECTED: '已拒绝',
|
||||
};
|
||||
|
||||
// ── Hermes / Agent ──
|
||||
export const agentTaskStatusLabels: Record<string, string> = {
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { useState, useCallback, useRef } from 'react'
|
||||
import { useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
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 { importStatusLabels, importStepLabels } from '@/constants/labels'
|
||||
import type { TablePaginationConfig } from 'antd'
|
||||
@ -73,6 +73,17 @@ export default function ImportMonitorPage() {
|
||||
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) => {
|
||||
if (sourceDeleted) {
|
||||
message.warning('关联的资料来源已删除,无法重新解析。请删除此导入记录后重新上传文件。')
|
||||
@ -135,7 +146,7 @@ export default function ImportMonitorPage() {
|
||||
const statusColor: Record<string, string> = {
|
||||
COMPLETED: 'green', QUEUED: 'default', CLAIMED: 'blue',
|
||||
DOWNLOADING: 'processing', PARSING: 'processing', CHUNKING: 'processing',
|
||||
EMBEDDING: 'processing', INDEXING: 'processing', GENERATING_CANDIDATES: 'processing',
|
||||
EMBEDDING: 'processing', INDEXING: 'processing',
|
||||
FAILED: 'red', FAILED_RETRYABLE: 'orange', CANCELLED: 'default',
|
||||
}
|
||||
|
||||
@ -155,6 +166,9 @@ export default function ImportMonitorPage() {
|
||||
<Space size={0}>
|
||||
<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>
|
||||
<Popconfirm title="确认删除此导入记录?关联的 Source 和文件也将被删除。" onConfirm={() => handleDelete(r.id)}>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>删除</Button>
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
) },
|
||||
]
|
||||
@ -198,8 +212,13 @@ export default function ImportMonitorPage() {
|
||||
size="small"
|
||||
onChange={(_pagination: TablePaginationConfig, _filters: Record<string, FilterValue>, sorter: SorterResult<ImportJob> | SorterResult<ImportJob>[]) => {
|
||||
const s = Array.isArray(sorter) ? sorter[0] : sorter
|
||||
if (s.columnKey) setSortBy(String(s.columnKey))
|
||||
setSortOrder(s.order || 'descend')
|
||||
if (s.order) {
|
||||
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}>
|
||||
|
||||
@ -3,8 +3,6 @@ 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 { knowledgeAPI, vectorAPI } from '@/api'
|
||||
import { candidateStatusLabels } from '@/constants/labels'
|
||||
import dayjs from 'dayjs'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
@ -14,11 +12,6 @@ export default function KnowledgeOpsPage() {
|
||||
const [sourceId, setSourceId] = useState('')
|
||||
const [debugQuery, setDebugQuery] = useState('')
|
||||
|
||||
const { data: candidates } = useQuery({
|
||||
queryKey: ['ops', 'candidates'],
|
||||
queryFn: () => knowledgeAPI.candidates(),
|
||||
})
|
||||
|
||||
const { data: chunks } = useQuery({
|
||||
queryKey: ['ops', 'chunks', sourceId],
|
||||
queryFn: () => knowledgeAPI.chunks(sourceId),
|
||||
@ -31,15 +24,6 @@ export default function KnowledgeOpsPage() {
|
||||
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 = [
|
||||
{ title: '#', dataIndex: 'chunkIndex', width: 50 },
|
||||
{ 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>
|
||||
|
||||
<Tabs items={[
|
||||
{
|
||||
key: 'candidates', label: '候选巡检',
|
||||
children: <Table dataSource={candidates || []} columns={candidateCols} rowKey="id" pagination={{ pageSize: 20 }} size="small" />,
|
||||
},
|
||||
{
|
||||
key: 'chunks', label: 'Chunk 管理',
|
||||
children: (
|
||||
|
||||
@ -22,7 +22,6 @@ export type {
|
||||
ImportStepLog,
|
||||
ImportJobDetail,
|
||||
KnowledgeChunk,
|
||||
KnowledgeCandidate,
|
||||
} from './knowledge'
|
||||
export type {
|
||||
ServerInfo,
|
||||
|
||||
@ -9,7 +9,6 @@ export interface KnowledgeBase {
|
||||
user?: { nickname?: string; email?: string }
|
||||
sourceCount?: number
|
||||
chunkCount?: number
|
||||
candidateCount?: number
|
||||
createdAt: string
|
||||
updatedAt?: string
|
||||
}
|
||||
@ -63,7 +62,6 @@ export type ImportJobStatus =
|
||||
| 'CHUNKING'
|
||||
| 'EMBEDDING'
|
||||
| 'INDEXING'
|
||||
| 'GENERATING_CANDIDATES'
|
||||
| 'COMPLETED'
|
||||
| 'FAILED'
|
||||
| 'FAILED_RETRYABLE'
|
||||
@ -95,16 +93,3 @@ export interface KnowledgeChunk {
|
||||
tokenCount?: number
|
||||
confidence?: number
|
||||
}
|
||||
|
||||
// ── 候选知识点 ──
|
||||
|
||||
export interface KnowledgeCandidate {
|
||||
id: string
|
||||
knowledgeBaseId: string
|
||||
sourceId: string
|
||||
importId?: string
|
||||
title: string
|
||||
content?: string
|
||||
confidence?: number
|
||||
status?: string
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user