feat(admin-v2): add Knowledge base management page
- 4 tabs: Items (CRUD + ProTable), Categories (tree management), Search, Analytics - Knowledge service with full API integration - Nav item + breadcrumb + route registration - Analytics overview with 8 KPI statistics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -16,6 +16,7 @@ import {
|
||||
SunOutlined,
|
||||
MoonOutlined,
|
||||
ApiOutlined,
|
||||
BookOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { Avatar, Dropdown, Tooltip, Drawer } from 'antd'
|
||||
import { useAuthStore } from '@/stores/authStore'
|
||||
@@ -42,6 +43,7 @@ const navItems: NavItem[] = [
|
||||
{ path: '/api-keys', name: 'API 密钥', icon: <ApiOutlined />, permission: 'provider:manage', group: '资源管理' },
|
||||
{ path: '/usage', name: '用量统计', icon: <BarChartOutlined />, permission: 'admin:full', group: '运维' },
|
||||
{ path: '/relay', name: '中转任务', icon: <SwapOutlined />, permission: 'relay:use', group: '运维' },
|
||||
{ path: '/knowledge', name: '知识库', icon: <BookOutlined />, permission: 'knowledge:read', group: '资源管理' },
|
||||
{ path: '/logs', name: '操作日志', icon: <FileTextOutlined />, permission: 'admin:full', group: '运维' },
|
||||
{ path: '/config', name: '系统配置', icon: <SettingOutlined />, permission: 'config:read', group: '系统' },
|
||||
{ path: '/prompts', name: '提示词管理', icon: <MessageOutlined />, permission: 'prompt:read', group: '系统' },
|
||||
@@ -204,6 +206,7 @@ const breadcrumbMap: Record<string, string> = {
|
||||
'/agent-templates': 'Agent 模板',
|
||||
'/usage': '用量统计',
|
||||
'/relay': '中转任务',
|
||||
'/knowledge': '知识库',
|
||||
'/config': '系统配置',
|
||||
'/prompts': '提示词管理',
|
||||
'/logs': '操作日志',
|
||||
|
||||
503
admin-v2/src/pages/Knowledge.tsx
Normal file
503
admin-v2/src/pages/Knowledge.tsx
Normal file
@@ -0,0 +1,503 @@
|
||||
// ============================================================
|
||||
// 知识库管理
|
||||
// ============================================================
|
||||
|
||||
import { useState, useMemo } from 'react'
|
||||
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'
|
||||
import {
|
||||
Button, message, Tag, Modal, Form, Input, Select, Space, Popconfirm,
|
||||
Card, Statistic, Row, Col, Tabs, Tree, Typography, Empty, Spin, InputNumber,
|
||||
} from 'antd'
|
||||
import {
|
||||
PlusOutlined, SearchOutlined, BookOutlined, FolderOutlined,
|
||||
DeleteOutlined, EditOutlined, EyeOutlined, BarChartOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import type { ProColumns } from '@ant-design/pro-components'
|
||||
import { ProTable } from '@ant-design/pro-components'
|
||||
import { knowledgeService } from '@/services/knowledge'
|
||||
import type { CategoryResponse, KnowledgeItem, SearchResult } from '@/services/knowledge'
|
||||
|
||||
const { TextArea } = Input
|
||||
const { Text, Title } = Typography
|
||||
|
||||
// === 分类树 + 条目列表 Tab ===
|
||||
|
||||
function CategoriesPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const { data: categories = [], isLoading } = useQuery({
|
||||
queryKey: ['knowledge-categories'],
|
||||
queryFn: ({ signal }) => knowledgeService.listCategories(signal),
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Parameters<typeof knowledgeService.createCategory>[0]) =>
|
||||
knowledgeService.createCategory(data),
|
||||
onSuccess: () => {
|
||||
message.success('分类已创建')
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge-categories'] })
|
||||
setCreateOpen(false)
|
||||
form.resetFields()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => knowledgeService.deleteCategory(id),
|
||||
onSuccess: () => {
|
||||
message.success('分类已删除')
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge-categories'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '删除失败'),
|
||||
})
|
||||
|
||||
const treeData = useMemo(
|
||||
() => buildTreeData(categories, (id) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除',
|
||||
content: '删除后无法恢复,请确保分类下没有子分类和条目。',
|
||||
okType: 'danger',
|
||||
onOk: () => deleteMutation.mutate(id),
|
||||
})
|
||||
}),
|
||||
[categories, deleteMutation],
|
||||
)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<Title level={5} style={{ margin: 0 }}>分类管理</Title>
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
|
||||
新建分类
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoading ? (
|
||||
<div className="flex justify-center py-8"><Spin /></div>
|
||||
) : categories.length === 0 ? (
|
||||
<Empty description="暂无分类,请新建一个" />
|
||||
) : (
|
||||
<Tree
|
||||
treeData={treeData}
|
||||
defaultExpandAll
|
||||
showLine={{ showLeafIcon: false }}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
|
||||
<Modal
|
||||
title="新建分类"
|
||||
open={createOpen}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={createMutation.isPending}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={(v) => createMutation.mutate(v)}>
|
||||
<Form.Item name="name" label="分类名称" rules={[{ required: true, message: '请输入分类名称' }]}>
|
||||
<Input placeholder="例如:产品知识、技术文档" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="描述">
|
||||
<TextArea rows={2} placeholder="可选描述" />
|
||||
</Form.Item>
|
||||
<Form.Item name="parent_id" label="父分类">
|
||||
<Select placeholder="无(顶级分类)" allowClear>
|
||||
{flattenCategories(categories).map((c) => (
|
||||
<Select.Option key={c.id} value={c.id}>{c.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="icon" label="图标">
|
||||
<Input placeholder="可选,如 📚" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// === 条目列表 ===
|
||||
|
||||
function ItemsPanel() {
|
||||
const queryClient = useQueryClient()
|
||||
const [createOpen, setCreateOpen] = useState(false)
|
||||
const [detailItem, setDetailItem] = useState<string | null>(null)
|
||||
const [page, setPage] = useState(1)
|
||||
const [pageSize, setPageSize] = useState(20)
|
||||
const [filters, setFilters] = useState<{ category_id?: string; status?: string; keyword?: string }>({})
|
||||
const [form] = Form.useForm()
|
||||
|
||||
const { data: categories = [] } = useQuery({
|
||||
queryKey: ['knowledge-categories'],
|
||||
queryFn: ({ signal }) => knowledgeService.listCategories(signal),
|
||||
})
|
||||
|
||||
const { data, isLoading } = useQuery({
|
||||
queryKey: ['knowledge-items', page, pageSize, filters],
|
||||
queryFn: ({ signal }) =>
|
||||
knowledgeService.listItems({ page, page_size: pageSize, ...filters }, signal),
|
||||
})
|
||||
|
||||
const { data: detailData } = useQuery({
|
||||
queryKey: ['knowledge-item-detail', detailItem],
|
||||
queryFn: ({ signal }) => knowledgeService.getItem(detailItem!, signal),
|
||||
enabled: !!detailItem,
|
||||
})
|
||||
|
||||
const createMutation = useMutation({
|
||||
mutationFn: (data: Parameters<typeof knowledgeService.createItem>[0]) =>
|
||||
knowledgeService.createItem(data),
|
||||
onSuccess: () => {
|
||||
message.success('条目已创建')
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge-items'] })
|
||||
setCreateOpen(false)
|
||||
form.resetFields()
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '创建失败'),
|
||||
})
|
||||
|
||||
const deleteMutation = useMutation({
|
||||
mutationFn: (id: string) => knowledgeService.deleteItem(id),
|
||||
onSuccess: () => {
|
||||
message.success('已删除')
|
||||
queryClient.invalidateQueries({ queryKey: ['knowledge-items'] })
|
||||
},
|
||||
onError: (err: Error) => message.error(err.message || '删除失败'),
|
||||
})
|
||||
|
||||
const statusColors: Record<string, string> = { active: 'green', draft: 'orange', archived: 'default' }
|
||||
const statusLabels: Record<string, string> = { active: '活跃', draft: '草稿', archived: '已归档' }
|
||||
|
||||
const columns: ProColumns<KnowledgeItem>[] = [
|
||||
{
|
||||
title: '标题',
|
||||
dataIndex: 'title',
|
||||
width: 250,
|
||||
render: (_, r) => (
|
||||
<Button type="link" size="small" onClick={() => setDetailItem(r.id)}>
|
||||
{r.title}
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
{ title: '状态', dataIndex: 'status', width: 80, valueEnum: Object.fromEntries(Object.entries(statusLabels).map(([k, v]) => [k, { text: v, status: statusColors[k] === 'green' ? 'Success' : statusColors[k] === 'orange' ? 'Warning' : 'Default' }])) },
|
||||
{ title: '版本', dataIndex: 'version', width: 60, search: false },
|
||||
{ title: '优先级', dataIndex: 'priority', width: 70, search: false },
|
||||
{
|
||||
title: '标签',
|
||||
dataIndex: 'tags',
|
||||
width: 200,
|
||||
search: false,
|
||||
render: (_, r) => (
|
||||
<Space size={[4, 4]} wrap>
|
||||
{r.tags?.map((t) => <Tag key={t}>{t}</Tag>)}
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
{ title: '更新时间', dataIndex: 'updated_at', width: 160, valueType: 'dateTime', search: false },
|
||||
{
|
||||
title: '操作',
|
||||
width: 100,
|
||||
search: false,
|
||||
render: (_, r) => (
|
||||
<Space>
|
||||
<Button type="link" size="small" icon={<EyeOutlined />} onClick={() => setDetailItem(r.id)} />
|
||||
<Popconfirm title="确认删除?" onConfirm={() => deleteMutation.mutate(r.id)}>
|
||||
<Button type="link" size="small" danger icon={<DeleteOutlined />} />
|
||||
</Popconfirm>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
<ProTable<KnowledgeItem>
|
||||
columns={columns}
|
||||
dataSource={data?.items || []}
|
||||
loading={isLoading}
|
||||
rowKey="id"
|
||||
search={{
|
||||
onReset: () => setFilters({}),
|
||||
onSearch: (values) => setFilters(values),
|
||||
}}
|
||||
toolBarRender={() => [
|
||||
<Button key="create" type="primary" icon={<PlusOutlined />} onClick={() => setCreateOpen(true)}>
|
||||
新建条目
|
||||
</Button>,
|
||||
]}
|
||||
pagination={{
|
||||
current: page,
|
||||
pageSize,
|
||||
total: data?.total || 0,
|
||||
showSizeChanger: true,
|
||||
onChange: (p, ps) => { setPage(p); setPageSize(ps) },
|
||||
}}
|
||||
options={{ density: false, fullScreen: false, reload: () => queryClient.invalidateQueries({ queryKey: ['knowledge-items'] }) }}
|
||||
/>
|
||||
|
||||
{/* 创建弹窗 */}
|
||||
<Modal
|
||||
title="新建知识条目"
|
||||
open={createOpen}
|
||||
onCancel={() => setCreateOpen(false)}
|
||||
onOk={() => form.submit()}
|
||||
confirmLoading={createMutation.isPending}
|
||||
width={640}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={(v) => createMutation.mutate(v)}>
|
||||
<Form.Item name="category_id" label="分类" rules={[{ required: true, message: '请选择分类' }]}>
|
||||
<Select placeholder="选择分类">
|
||||
{flattenCategories(categories).map((c) => (
|
||||
<Select.Option key={c.id} value={c.id}>{c.name}</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item name="title" label="标题" rules={[{ required: true, message: '请输入标题' }]}>
|
||||
<Input placeholder="知识条目标题" />
|
||||
</Form.Item>
|
||||
<Form.Item name="content" label="内容" rules={[{ required: true, message: '请输入内容' }]}>
|
||||
<TextArea rows={8} placeholder="支持 Markdown 格式" />
|
||||
</Form.Item>
|
||||
<Row gutter={16}>
|
||||
<Col span={12}>
|
||||
<Form.Item name="keywords" label="关键词">
|
||||
<Select mode="tags" placeholder="输入后回车添加" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Form.Item name="tags" label="标签">
|
||||
<Select mode="tags" placeholder="输入后回车添加" />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
</Row>
|
||||
<Form.Item name="priority" label="优先级" initialValue={0}>
|
||||
<InputNumber min={0} max={100} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Modal>
|
||||
|
||||
{/* 详情弹窗 */}
|
||||
<Modal
|
||||
title={detailData?.title || '条目详情'}
|
||||
open={!!detailItem}
|
||||
onCancel={() => setDetailItem(null)}
|
||||
footer={null}
|
||||
width={720}
|
||||
>
|
||||
{detailData && (
|
||||
<div>
|
||||
<div className="mb-4 flex gap-2">
|
||||
<Tag color={statusColors[detailData.status]}>{statusLabels[detailData.status] || detailData.status}</Tag>
|
||||
<Tag>版本 {detailData.version}</Tag>
|
||||
<Tag>优先级 {detailData.priority}</Tag>
|
||||
</div>
|
||||
<div className="mb-4 whitespace-pre-wrap bg-neutral-50 dark:bg-neutral-900 p-4 rounded-lg max-h-96 overflow-y-auto text-sm">
|
||||
{detailData.content}
|
||||
</div>
|
||||
<div className="flex gap-2 flex-wrap">
|
||||
{detailData.tags?.map((t) => <Tag key={t} color="blue">{t}</Tag>)}
|
||||
{detailData.keywords?.map((k) => <Tag key={k} color="cyan">{k}</Tag>)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// === 搜索面板 ===
|
||||
|
||||
function SearchPanel() {
|
||||
const [query, setQuery] = useState('')
|
||||
const [results, setResults] = useState<SearchResult[]>([])
|
||||
const [searching, setSearching] = useState(false)
|
||||
|
||||
const handleSearch = async () => {
|
||||
if (!query.trim()) return
|
||||
setSearching(true)
|
||||
try {
|
||||
const data = await knowledgeService.search({ query: query.trim(), limit: 10 })
|
||||
setResults(data)
|
||||
} catch (err) {
|
||||
message.error('搜索失败')
|
||||
} finally {
|
||||
setSearching(false)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={5}>语义搜索</Title>
|
||||
<Space.Compact className="w-full mb-4">
|
||||
<Input
|
||||
size="large"
|
||||
placeholder="输入搜索关键词..."
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
onPressEnter={handleSearch}
|
||||
prefix={<SearchOutlined />}
|
||||
/>
|
||||
<Button size="large" type="primary" loading={searching} onClick={handleSearch}>
|
||||
搜索
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
|
||||
{results.length === 0 && !searching && query === '' && (
|
||||
<Empty description="输入关键词搜索知识库" />
|
||||
)}
|
||||
|
||||
<div className="space-y-3">
|
||||
{results.map((r) => (
|
||||
<Card key={r.chunk_id} size="small" hoverable>
|
||||
<div className="flex justify-between items-start mb-2">
|
||||
<Text strong>{r.item_title}</Text>
|
||||
<Tag>{r.category_name}</Tag>
|
||||
</div>
|
||||
<div className="text-sm text-neutral-600 dark:text-neutral-400 line-clamp-3 mb-2">
|
||||
{r.content}
|
||||
</div>
|
||||
<div className="flex gap-1 flex-wrap">
|
||||
{r.keywords?.slice(0, 5).map((k) => (
|
||||
<Tag key={k} color="cyan" style={{ fontSize: 11 }}>{k}</Tag>
|
||||
))}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// === 分析看板 ===
|
||||
|
||||
function AnalyticsPanel() {
|
||||
const { data: overview, isLoading } = useQuery({
|
||||
queryKey: ['knowledge-analytics'],
|
||||
queryFn: ({ signal }) => knowledgeService.getOverview(signal),
|
||||
})
|
||||
|
||||
if (isLoading) return <div className="flex justify-center py-8"><Spin /></div>
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Title level={5} className="mb-4">知识库概览</Title>
|
||||
<Row gutter={[16, 16]}>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="总条目数" value={overview?.total_items || 0} /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="活跃条目" value={overview?.active_items || 0} valueStyle={{ color: '#52c41a' }} /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="分类数" value={overview?.total_categories || 0} /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="本周新增" value={overview?.weekly_new_items || 0} valueStyle={{ color: '#1890ff' }} /></Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} className="mt-4">
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="总引用次数" value={overview?.total_references || 0} /></Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="注入率"
|
||||
value={((overview?.injection_rate || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card>
|
||||
<Statistic
|
||||
title="正面反馈率"
|
||||
value={((overview?.positive_feedback_rate || 0) * 100).toFixed(1)}
|
||||
suffix="%"
|
||||
valueStyle={{ color: '#52c41a' }}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={6}>
|
||||
<Card><Statistic title="过期条目" value={overview?.stale_items_count || 0} valueStyle={{ color: '#faad14' }} /></Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// === 主页面 ===
|
||||
|
||||
export default function Knowledge() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<Tabs
|
||||
defaultActiveKey="items"
|
||||
items={[
|
||||
{
|
||||
key: 'items',
|
||||
label: '知识条目',
|
||||
icon: <BookOutlined />,
|
||||
children: <ItemsPanel />,
|
||||
},
|
||||
{
|
||||
key: 'categories',
|
||||
label: '分类管理',
|
||||
icon: <FolderOutlined />,
|
||||
children: <CategoriesPanel />,
|
||||
},
|
||||
{
|
||||
key: 'search',
|
||||
label: '搜索',
|
||||
icon: <SearchOutlined />,
|
||||
children: <SearchPanel />,
|
||||
},
|
||||
{
|
||||
key: 'analytics',
|
||||
label: '分析看板',
|
||||
icon: <BarChartOutlined />,
|
||||
children: <AnalyticsPanel />,
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// === 辅助函数 ===
|
||||
|
||||
function flattenCategories(cats: CategoryResponse[]): { id: string; name: string }[] {
|
||||
const result: { id: string; name: string }[] = []
|
||||
for (const c of cats) {
|
||||
result.push({ id: c.id, name: c.name })
|
||||
if (c.children?.length) {
|
||||
result.push(...flattenCategories(c.children))
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
interface TreeNode {
|
||||
key: string
|
||||
title: React.ReactNode
|
||||
icon?: React.ReactNode
|
||||
children?: TreeNode[]
|
||||
}
|
||||
|
||||
function buildTreeData(cats: CategoryResponse[], onDelete: (id: string) => void): TreeNode[] {
|
||||
return cats.map((c) => ({
|
||||
key: c.id,
|
||||
title: (
|
||||
<div className="flex items-center gap-2">
|
||||
<span>{c.icon || '📁'} {c.name}</span>
|
||||
<Tag>{c.item_count}</Tag>
|
||||
<Button type="link" size="small" danger onClick={() => onDelete(c.id)}>
|
||||
<DeleteOutlined />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
children: c.children?.length ? buildTreeData(c.children, onDelete) : undefined,
|
||||
}))
|
||||
}
|
||||
@@ -28,6 +28,7 @@ export const router = createBrowserRouter([
|
||||
{ path: 'api-keys', lazy: () => import('@/pages/ModelServices').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'usage', lazy: () => import('@/pages/Usage').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'relay', lazy: () => import('@/pages/Relay').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'knowledge', lazy: () => import('@/pages/Knowledge').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'config', lazy: () => import('@/pages/Config').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'prompts', lazy: () => import('@/pages/Prompts').then((m) => ({ Component: m.default })) },
|
||||
{ path: 'logs', lazy: () => import('@/pages/Logs').then((m) => ({ Component: m.default })) },
|
||||
|
||||
128
admin-v2/src/services/knowledge.ts
Normal file
128
admin-v2/src/services/knowledge.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
import request, { withSignal } from './request'
|
||||
|
||||
// === Types ===
|
||||
|
||||
export interface CategoryResponse {
|
||||
id: string
|
||||
name: string
|
||||
description: string | null
|
||||
parent_id: string | null
|
||||
icon: string | null
|
||||
sort_order: number
|
||||
item_count: number
|
||||
children: CategoryResponse[]
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface KnowledgeItem {
|
||||
id: string
|
||||
category_id: string
|
||||
title: string
|
||||
content: string
|
||||
keywords: string[]
|
||||
related_questions: string[]
|
||||
priority: number
|
||||
status: string
|
||||
version: number
|
||||
source: string
|
||||
tags: string[]
|
||||
created_by: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface SearchResult {
|
||||
chunk_id: string
|
||||
item_id: string
|
||||
item_title: string
|
||||
category_name: string
|
||||
content: string
|
||||
score: number
|
||||
keywords: string[]
|
||||
}
|
||||
|
||||
export interface AnalyticsOverview {
|
||||
total_items: number
|
||||
active_items: number
|
||||
total_categories: number
|
||||
weekly_new_items: number
|
||||
total_references: number
|
||||
avg_reference_per_item: number
|
||||
hit_rate: number
|
||||
injection_rate: number
|
||||
positive_feedback_rate: number
|
||||
stale_items_count: number
|
||||
}
|
||||
|
||||
export interface ListItemsResponse {
|
||||
items: KnowledgeItem[]
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
}
|
||||
|
||||
// === Service ===
|
||||
|
||||
export const knowledgeService = {
|
||||
// 分类
|
||||
listCategories: (signal?: AbortSignal) =>
|
||||
request.get<CategoryResponse[]>('/knowledge/categories', withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
createCategory: (data: { name: string; description?: string; parent_id?: string; icon?: string }) =>
|
||||
request.post('/knowledge/categories', data).then((r) => r.data),
|
||||
|
||||
deleteCategory: (id: string) =>
|
||||
request.delete(`/knowledge/categories/${id}`).then((r) => r.data),
|
||||
|
||||
// 条目
|
||||
listItems: (params: { page?: number; page_size?: number; category_id?: string; status?: string; keyword?: string }, signal?: AbortSignal) =>
|
||||
request.get<ListItemsResponse>('/knowledge/items', withSignal({ params }, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
getItem: (id: string, signal?: AbortSignal) =>
|
||||
request.get<KnowledgeItem>(`/knowledge/items/${id}`, withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
createItem: (data: {
|
||||
category_id: string
|
||||
title: string
|
||||
content: string
|
||||
keywords?: string[]
|
||||
related_questions?: string[]
|
||||
priority?: number
|
||||
tags?: string[]
|
||||
}) => request.post('/knowledge/items', data).then((r) => r.data),
|
||||
|
||||
updateItem: (id: string, data: Record<string, unknown>) =>
|
||||
request.put(`/knowledge/items/${id}`, data).then((r) => r.data),
|
||||
|
||||
deleteItem: (id: string) =>
|
||||
request.delete(`/knowledge/items/${id}`).then((r) => r.data),
|
||||
|
||||
batchCreate: (items: Array<{
|
||||
category_id: string
|
||||
title: string
|
||||
content: string
|
||||
keywords?: string[]
|
||||
tags?: string[]
|
||||
}>) => request.post('/knowledge/items/batch', items).then((r) => r.data),
|
||||
|
||||
// 搜索
|
||||
search: (data: { query: string; category_id?: string; limit?: number }) =>
|
||||
request.post<SearchResult[]>('/knowledge/search', data).then((r) => r.data),
|
||||
|
||||
// 分析
|
||||
getOverview: (signal?: AbortSignal) =>
|
||||
request.get<AnalyticsOverview>('/knowledge/analytics/overview', withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
getTrends: (signal?: AbortSignal) =>
|
||||
request.get('/knowledge/analytics/trends', withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
|
||||
getTopItems: (signal?: AbortSignal) =>
|
||||
request.get('/knowledge/analytics/top-items', withSignal({}, signal))
|
||||
.then((r) => r.data),
|
||||
}
|
||||
Reference in New Issue
Block a user