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
|
*.sln
|
||||||
*.sw?
|
*.sw?
|
||||||
.env
|
.env
|
||||||
|
uploads/
|
||||||
|
|||||||
@ -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}`),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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`) },
|
||||||
|
|||||||
@ -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> = {
|
||||||
|
|||||||
@ -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}>
|
||||||
|
|||||||
@ -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: (
|
||||||
|
|||||||
@ -22,7 +22,6 @@ export type {
|
|||||||
ImportStepLog,
|
ImportStepLog,
|
||||||
ImportJobDetail,
|
ImportJobDetail,
|
||||||
KnowledgeChunk,
|
KnowledgeChunk,
|
||||||
KnowledgeCandidate,
|
|
||||||
} from './knowledge'
|
} from './knowledge'
|
||||||
export type {
|
export type {
|
||||||
ServerInfo,
|
ServerInfo,
|
||||||
|
|||||||
@ -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
|
|
||||||
}
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user