feat: 添加知识库配置管理功能

新增地区-知识库绑定管理功能,支持增删改查操作
- 添加 V3 API 路由层:area-datasets 相关接口
- 添加 API 客户端:area-datasets.ts
- 添加自定义 Hook:use-area-dataset-config.ts
- 添加管理组件:area-dataset-config.tsx
- 修复路由冲突问题,删除重复的 .ts 路由文件
- 更新 dataset-manager 页面,添加 Tabs 导航

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
2025-12-07 23:12:21 +08:00
parent 21bcb1310b
commit 27aff59152
13 changed files with 3740 additions and 21 deletions
+158
View File
@@ -0,0 +1,158 @@
/**
* V3 Dify Area Dataset API 模块
*
* 提供地区-知识库绑定管理接口
*/
import { request } from '~/api/axios-client';
// ==================== Type Definitions ====================
export interface AreaDataset {
id: number;
area: string;
dataset_id: string;
dataset_name: string;
dataset_description?: string;
is_default: boolean;
is_public: boolean;
sort_order: number;
status: number;
created_at: string;
updated_at: string;
}
export interface MyDatasetsResponse {
code: number;
message: string;
data: {
data: AreaDataset[];
total: number;
user_area: string;
user_role: string;
};
}
export interface AllDatasetsResponse {
code: number;
message: string;
data: {
data: AreaDataset[];
total: number;
page: number;
page_size: number;
has_more: boolean;
};
}
export interface AreasResponse {
code: number;
message: string;
data: {
data: string[];
};
}
export interface CreateDatasetRequest {
area: string;
dataset_id: string;
dataset_name: string;
dataset_description?: string;
is_default?: boolean;
is_public?: boolean;
sort_order?: number;
}
export interface UpdateDatasetRequest {
dataset_name?: string;
dataset_description?: string;
is_default?: boolean;
is_public?: boolean;
sort_order?: number;
status?: number;
}
export interface ApiResponse<T> {
code: number;
message: string;
data: T;
}
// ==================== API Functions ====================
const API_BASE = '/api/v3/dify/area-datasets';
/**
* 获取当前用户可访问的知识库列表
* 权限: dify:dataset:read
*/
export async function getMyDatasets(): Promise<MyDatasetsResponse> {
const response = await request.get(`${API_BASE}/my`);
return response.data;
}
/**
* 获取所有知识库绑定列表(管理员)
* 权限: dify:dataset:manage
*/
export async function getAllDatasets(params: {
area?: string;
only_enabled?: boolean;
page?: number;
page_size?: number;
}): Promise<AllDatasetsResponse> {
const queryParams = new URLSearchParams();
if (params.area) queryParams.append('area', params.area);
if (params.only_enabled !== undefined)
queryParams.append('only_enabled', String(params.only_enabled));
if (params.page) queryParams.append('page', String(params.page));
if (params.page_size) queryParams.append('page_size', String(params.page_size));
const response = await request.get(
`${API_BASE}?${queryParams.toString()}`
);
return response.data;
}
/**
* 获取可用地区列表(管理员)
* 权限: dify:dataset:manage
*/
export async function getAvailableAreas(): Promise<string[]> {
const response = await request.get(`${API_BASE}/areas`);
return response.data.data;
}
/**
* 创建知识库绑定(管理员)
* 权限: dify:dataset:manage
*/
export async function createDatasetBinding(
data: CreateDatasetRequest
): Promise<ApiResponse<{ data: AreaDataset }>> {
const response = await request.post(`${API_BASE}`, data);
return response.data;
}
/**
* 更新知识库绑定(管理员)
* 权限: dify:dataset:manage
*/
export async function updateDatasetBinding(
id: number,
data: UpdateDatasetRequest
): Promise<ApiResponse<{ data: AreaDataset }>> {
const response = await request.put(`${API_BASE}/${id}`, data);
return response.data;
}
/**
* 删除知识库绑定(管理员)
* 权限: dify:dataset:manage
*/
export async function deleteDatasetBinding(id: number): Promise<ApiResponse<{ message: string }>> {
const response = await request.delete(`${API_BASE}/${id}`);
return response.data;
}
+21 -3
View File
@@ -21,6 +21,24 @@ declare global {
}
}
interface ChatTheme {
colorBgContainer: string;
borderRadiusLG: number;
}
/**
* 获取主题token - 避免在SSR环境中调用
*/
function useChatTheme(): ChatTheme {
// Ant Design的theme.useToken()必须在组件顶层调用,不能放在useEffect中
const antdToken = typeof window !== 'undefined' ? theme.useToken().token : null;
return {
colorBgContainer: antdToken?.colorBgContainer || '#ffffff',
borderRadiusLG: antdToken?.borderRadiusLG || 8,
};
}
/**
* 主聊天组件
* 实现单页面应用模式,参考webapp-conversation的初始化逻辑
@@ -29,9 +47,9 @@ export default function Chat() {
// 侧边栏状态
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
const [isMobile, setIsMobile] = useState(false);
const {
token: { colorBgContainer, borderRadiusLG },
} = theme.useToken();
// 获取主题配置,避免SSR错误
const { colorBgContainer, borderRadiusLG } = useChatTheme();
// 会话管理
const {
+30 -1
View File
@@ -9,8 +9,9 @@ import {
PlusOutlined,
SearchOutlined,
} from '@ant-design/icons';
import { Button, Dropdown, Input, Layout, Menu, Modal, Tooltip, message, theme } from 'antd';
import { Button, Dropdown, Input, Layout, Menu, Modal, Tooltip, message, theme, Select } from 'antd';
import { forwardRef, useImperativeHandle, useState } from 'react';
import type { ChatApp } from '~/api/dify-chat-apps/types';
import type { ConversationItem } from '~/api/dify-chat';
import { deleteConversation, renameConversation } from '~/api/dify-chat';
import '../../styles/components/chat-with-llm/sidebar.css';
@@ -25,6 +26,11 @@ interface ChatSidebarProps {
onConversationSelect: (conversationId: string) => void;
onNewConversation: () => void;
onConversationDeleted?: (conversationId: string) => void;
// 对话应用相关属性
chatApps: ChatApp[];
loadingChatApps: boolean;
currentChatApp: ChatApp | null;
onChatAppChange: (appId: string) => void;
onConversationRenamed?: (conversationId: string, newName: string) => void;
}
@@ -39,6 +45,10 @@ export interface ChatSidebarRef {
const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
collapsed,
onToggle,
chatApps,
loadingChatApps,
currentChatApp,
onChatAppChange,
conversations,
currentConversationId,
onConversationSelect,
@@ -281,6 +291,25 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
</div>
{/* 搜索框 */}
{/* 对话应用选择器 */}
{!collapsed && (
<div className="mb-3">
<Select
value={currentChatApp?.app_id}
onChange={onChatAppChange}
loading={loadingChatApps}
className="w-full"
placeholder="选择对话应用"
size="small"
>
{(chatApps || []).map(app => (
<Select.Option key={app.app_id} value={app.app_id}>
{app.app_name}
</Select.Option>
))}
</Select>
</div>
)}
{!collapsed && (
<Input
placeholder="搜索对话..."
@@ -0,0 +1,662 @@
/**
* 知识库配置管理组件
*
* 提供地区-知识库绑定管理功能,包括增删改查
*
* @author 开发团队
* @version 1.0.0
*/
import { useState, useEffect } from 'react';
import {
Card,
Table,
Button,
Tag,
Space,
Modal,
Form,
Input,
Select,
Switch,
InputNumber,
message,
Flex,
Typography,
Popconfirm,
Spin,
Tooltip,
} from 'antd';
import {
PlusOutlined,
EditOutlined,
DeleteOutlined,
InfoCircleOutlined,
CheckCircleOutlined,
} from '@ant-design/icons';
import { useAreaDatasetConfig } from '~/hooks/use-area-dataset-config';
import { fetchDatasets } from '~/api/dify-dataset/api/datasetApi';
import type { Dataset as DifyDataset } from '~/api/dify-dataset/type';
import type { AreaDataset } from '~/api/v3/dify/area-datasets';
const { Title, Text } = Typography;
// 颜色常量
const colors = {
bgLayout: '#f5f5f5',
border: '#e8e8e8',
text: '#262626',
textSecondary: '#8c8c8c',
primary: '#00684a',
};
/**
* 知识库配置管理组件
*/
export default function AreaDatasetConfig() {
// 使用自定义hook获取数据和操作方法
const {
// 数据
datasets,
loading,
total,
userArea,
userRole,
areas,
// areasLoading, // 地区列表已加载hook中
// 筛选
filterArea,
setFilterArea,
page,
setPage,
pageSize,
// 表单状态
modalVisible,
setModalVisible,
editingId,
setEditingId,
submitLoading,
// 操作方法
loadDatasets,
loadAreas,
handleCreate,
handleUpdate,
handleDelete,
// 权限
canManageDataset,
} = useAreaDatasetConfig();
// 内部状态
const [form] = Form.useForm();
const [difyDatasets, setDifyDatasets] = useState<DifyDataset[]>([]);
const [difyDatasetsLoading, setDifyDatasetsLoading] = useState<boolean>(false);
const [difyDatasetsTotal, setDifyDatasetsTotal] = useState<number>(0);
const [difyDatasetsPage, setDifyDatasetsPage] = useState<number>(1);
const [isLoadingDifyDatasets, setIsLoadingDifyDatasets] = useState<boolean>(false);
// ==================== Effects ====================
// 当编辑的ID变化时,加载表单数据
useEffect(() => {
if (editingId && modalVisible) {
const record = datasets.find((item) => item.id === editingId);
if (record) {
form.setFieldsValue({
area: record.area,
dataset_id: record.dataset_id,
dataset_name: record.dataset_name,
dataset_description: record.dataset_description,
is_public: record.is_public,
is_default: record.is_default,
sort_order: record.sort_order,
});
}
} else if (!editingId && modalVisible) {
// 新增时重置表单
form.resetFields();
loadDifyDatasets(); // 加载Dify知识库列表
}
}, [editingId, modalVisible, datasets, form]);
// ==================== Dify Datasets Loading ====================
/**
* 从Dify API加载知识库列表
*/
const loadDifyDatasets = async (pageNum: number = 1) => {
if (isLoadingDifyDatasets) return;
setIsLoadingDifyDatasets(true);
try {
const response = await fetchDatasets(pageNum, 20);
setDifyDatasets(response.data);
setDifyDatasetsTotal(response.total);
setDifyDatasetsPage(pageNum);
setDifyDatasetsLoading(false);
} catch (error: any) {
console.error('加载Dify知识库列表失败:', error);
message.error('加载Dify知识库列表失败');
setDifyDatasetsLoading(false);
} finally {
setIsLoadingDifyDatasets(false);
}
};
/**
* Dify数据集选择器滚动加载
*/
const handleDatasetSelectScroll = (e: React.UIEvent<HTMLDivElement>) => {
const { target } = e;
const { scrollTop, scrollHeight, clientHeight } = target as HTMLDivElement;
// 滚动到底部且还有更多数据时加载下一页
if (scrollHeight - scrollTop === clientHeight &&
difyDatasets.length < difyDatasetsTotal &&
!difyDatasetsLoading) {
loadDifyDatasets(difyDatasetsPage + 1);
}
};
// ==================== Event Handlers ====================
/**
* 处理新建按钮点击
*/
const handleCreateClick = () => {
if (!canManageDataset) {
message.error('您没有创建知识库绑定的权限');
return;
}
setEditingId(null);
setModalVisible(true);
form.resetFields();
loadDifyDatasets();
};
/**
* 处理编辑按钮点击
*/
const handleEditClick = (record: AreaDataset) => {
if (!canManageDataset) {
message.error('您没有编辑知识库绑定的权限');
return;
}
setEditingId(record.id);
setModalVisible(true);
};
/**
* 处理删除按钮点击
*/
const handleDeleteClick = async (id: number) => {
if (!canManageDataset) {
message.error('您没有删除知识库绑定的权限');
return;
}
const success = await handleDelete(id);
if (success) {
message.success('删除成功');
}
};
/**
* 处理表单提交
*/
const handleFormSubmit = async (values: any) => {
let success = false;
if (editingId) {
success = await handleUpdate(editingId, values);
} else {
success = await handleCreate(values);
}
if (success) {
setModalVisible(false);
form.resetFields();
setEditingId(null);
}
};
/**
* 处理表单取消
*/
const handleFormCancel = () => {
setModalVisible(false);
form.resetFields();
setEditingId(null);
};
/**
* 处理地区筛选变化
*/
const handleAreaFilterChange = (value: string) => {
setFilterArea(value);
setPage(1); // 重置到第一页
};
// ==================== Render ====================
// 计算用户角色标签
const userRoleLabel = (() => {
const labels: Record<string, string> = {
common: '普通用户',
admin: '市级管理员',
provincial_admin: '省级管理员',
};
return labels[userRole] || '未知角色';
})();
// 表格列定义
const columns = [
{
title: '序号',
key: 'index',
width: 60,
render: (_: any, __: any, index: number) =>
(page - 1) * pageSize + index + 1,
},
{
title: '地区',
dataIndex: 'area',
key: 'area',
width: 100,
render: (area: string) => (
<Tag color={area === '省级' ? 'gold' : 'blue'}>{area}</Tag>
),
},
{
title: '知识库名称',
dataIndex: 'dataset_name',
key: 'dataset_name',
width: 200,
ellipsis: true,
render: (text: string) => (
<Tooltip title={text}>
<Text style={{ color: colors.text }} strong>
{text}
</Text>
</Tooltip>
),
},
{
title: '知识库ID',
dataIndex: 'dataset_id',
key: 'dataset_id',
width: 200,
ellipsis: true,
render: (text: string) => (
<Tooltip title={text}>
<Text type="secondary" style={{ fontSize: '12px' }}>
{text.substring(0, 8)}...{text.substring(text.length - 4)}
</Text>
</Tooltip>
),
},
{
title: '描述',
dataIndex: 'dataset_description',
key: 'dataset_description',
ellipsis: true,
render: (text: string) =>
text ? (
<Tooltip title={text}>
<Text style={{ color: colors.text }}>
{text.length > 30 ? text.substring(0, 30) + '...' : text}
</Text>
</Tooltip>
) : (
<Text type="secondary">-</Text>
),
},
{
title: '标签',
key: 'tags',
width: 120,
render: (_: any, record: AreaDataset) => (
<Space size="small">
{record.is_public && (
<Tag color="success" icon={<CheckCircleOutlined />}>
</Tag>
)}
{record.is_default && (
<Tag color={colors.primary} style={{ color: '#fff' }}>
</Tag>
)}
</Space>
),
},
{
title: '排序',
dataIndex: 'sort_order',
key: 'sort_order',
width: 70,
align: 'center' as const,
sorter: (a: AreaDataset, b: AreaDataset) => a.sort_order - b.sort_order,
},
{
title: '状态',
dataIndex: 'status',
key: 'status',
width: 80,
render: (status: number) => (
<Tag color={status === 1 ? 'success' : 'default'}>
{status === 1 ? '启用' : '禁用'}
</Tag>
),
},
{
title: '创建时间',
dataIndex: 'created_at',
key: 'created_at',
width: 150,
render: (text: string) => (
<Text style={{ color: colors.textSecondary, fontSize: '12px' }}>
{new Date(text).toLocaleString('zh-CN')}
</Text>
),
},
// 操作列(仅管理员可见)
...(canManageDataset
? [
{
title: '操作',
key: 'actions',
width: 120,
fixed: 'right' as const,
render: (_: any, record: AreaDataset) => (
<Space size="small">
<Button
type="link"
size="small"
icon={<EditOutlined />}
onClick={() => handleEditClick(record)}
>
</Button>
<Popconfirm
title="确定删除?"
description="删除后该地区的用户将无法访问此知识库"
onConfirm={() => handleDeleteClick(record.id)}
okText="确定"
cancelText="取消"
>
<Button type="link" danger size="small" icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
),
},
]
: []),
];
return (
<div
style={{
height: '100%',
background: colors.bgLayout,
padding: '16px',
}}
>
{/* 页面头部 */}
<Card
style={{
marginBottom: '16px',
borderRadius: '8px',
}}
>
<Flex justify="space-between" align="center">
<div>
<Title level={4} style={{ margin: 0, marginBottom: '8px', color: colors.text }}>
</Title>
<Flex gap="16px" align="center">
<Text type="secondary">
: <Text strong>{userArea || '-'}</Text>
</Text>
<Text type="secondary">
: <Text strong>{userRoleLabel}</Text>
</Text>
<Text type="secondary">
: <Text strong>{total}</Text>
</Text>
</Flex>
</div>
{/* 仅管理员显示新增按钮 */}
{canManageDataset && (
<Button
type="primary"
icon={<PlusOutlined />}
onClick={handleCreateClick}
>
</Button>
)}
</Flex>
</Card>
{/* 筛选区域(仅管理员可见) */}
{canManageDataset && (
<Card
style={{
marginBottom: '16px',
borderRadius: '8px',
}}
>
<Flex gap="16px" align="center">
<Text style={{ color: colors.text }}>:</Text>
<Select
style={{ width: '150px' }}
placeholder="全部地区"
allowClear
value={filterArea || undefined}
onChange={handleAreaFilterChange}
options={[
{ label: '全部', value: '' },
{ label: '省级', value: '省级' },
...areas.map((area) => ({ label: area, value: area })),
]}
/>
</Flex>
</Card>
)}
{/* 数据表格 */}
<Card
style={{
borderRadius: '8px',
}}
>
<Table
columns={columns}
dataSource={datasets}
loading={loading}
rowKey="id"
pagination={
canManageDataset
? {
current: page,
pageSize: pageSize,
total: total,
onChange: (newPage) => setPage(newPage),
showSizeChanger: false,
showQuickJumper: true,
showTotal: (total) => `${total}`,
}
: false
}
scroll={{ x: 'max-content' }}
locale={{
emptyText: (
<Flex vertical align="center" justify="center" style={{ padding: '48px' }}>
<InfoCircleOutlined style={{ fontSize: '48px', color: colors.textSecondary }} />
<Text style={{ marginTop: '16px', color: colors.textSecondary }}>
</Text>
</Flex>
),
}}
/>
</Card>
{/* 新增/编辑对话框 */}
<Modal
title={editingId ? '编辑知识库绑定' : '新增知识库绑定'}
open={modalVisible}
onOk={() => form.submit()}
onCancel={handleFormCancel}
confirmLoading={submitLoading}
width={600}
destroyOnHidden
>
<Form
form={form}
layout="vertical"
onFinish={handleFormSubmit}
initialValues={{
is_public: false,
is_default: false,
sort_order: 0,
}}
>
{/* 地区选择(仅新增时可选) */}
<Form.Item
name="area"
label="地区"
rules={[{ required: true, message: '请选择地区' }]}
>
<Select
placeholder="请选择地区"
disabled={!!editingId} // 编辑时禁用
options={areas.map((area) => ({
label: area,
value: area,
}))}
/>
</Form.Item>
{/* Dify知识库选择(仅新增时可选) */}
<Form.Item
name="dataset_id"
label="Dify知识库"
rules={[{ required: true, message: '请选择Dify知识库' }]}
>
<Select
placeholder="请选择或输入知识库ID"
disabled={!!editingId}
loading={difyDatasetsLoading}
onPopupScroll={handleDatasetSelectScroll}
dropdownRender={(menu) => (
<div>
{menu}
{difyDatasets.length < difyDatasetsTotal && (
<div style={{ textAlign: 'center', padding: '8px' }}>
<Spin size="small" />
<Text style={{ marginLeft: '8px' }} type="secondary">
...
</Text>
</div>
)}
</div>
)}
onDropdownVisibleChange={(open) => {
if (open && !editingId) {
loadDifyDatasets();
}
}}
options={difyDatasets.map((ds) => ({
label: (
<Flex vertical>
<Text strong>{ds.name}</Text>
<Text type="secondary" style={{ fontSize: '12px' }}>
ID: {ds.id}
</Text>
</Flex>
),
value: ds.id,
}))}
dropdownStyle={{ maxHeight: '300px' }}
/>
</Form.Item>
{/* 知识库名称 */}
<Form.Item
name="dataset_name"
label="知识库名称"
rules={[
{ required: true, message: '请输入知识库名称' },
{ max: 255, message: '最多255个字符' },
]}
>
<Input placeholder="请输入知识库名称" />
</Form.Item>
{/* 知识库描述 */}
<Form.Item
name="dataset_description"
label="知识库描述"
>
<Input.TextArea
placeholder="请输入知识库描述(可选)"
rows={3}
maxLength={500}
/>
</Form.Item>
{/* 高级设置折叠面板 */}
<div style={{ marginTop: '24px' }}>
<Text strong style={{ color: colors.text, display: 'block', marginBottom: '16px' }}>
</Text>
<Flex gap="24px">
{/* 是否公开 */}
<Form.Item
name="is_public"
label="公开知识库"
valuePropName="checked"
tooltip="公开后所有地区的用户都可以访问此知识库"
>
<Switch />
</Form.Item>
{/* 是否默认 */}
<Form.Item
name="is_default"
label="默认知识库"
valuePropName="checked"
tooltip="设置为该地区的默认知识库,用户优先使用"
>
<Switch />
</Form.Item>
{/* 排序顺序 */}
<Form.Item
name="sort_order"
label="排序顺序"
tooltip="数值越小越靠前,范围0-100"
>
<InputNumber
min={0}
max={100}
style={{ width: '100%' }}
placeholder="0"
/>
</Form.Item>
</Flex>
</div>
</Form>
</Modal>
</div>
);
}
+308
View File
@@ -0,0 +1,308 @@
/**
* 知识库配置管理 Hook
*
* 提供地区-知识库绑定管理功能
*/
import { useState, useEffect, useCallback } from 'react';
import {
getMyDatasets,
getAllDatasets,
getAvailableAreas,
createDatasetBinding,
updateDatasetBinding,
deleteDatasetBinding,
type AreaDataset,
type CreateDatasetRequest,
type UpdateDatasetRequest,
} from '~/api/v3/dify/area-datasets';
import { message } from 'antd';
import { usePermission } from '~/hooks/usePermission';
// ==================== Type Definitions ====================
export interface UseAreaDatasetConfigReturn {
// 数据
datasets: AreaDataset[];
loading: boolean;
total: number;
userArea: string;
userRole: string;
areas: string[];
areasLoading: boolean;
// 筛选
filterArea: string;
setFilterArea: (area: string) => void;
page: number;
setPage: (page: number) => void;
pageSize: number;
// 表单状态
modalVisible: boolean;
setModalVisible: (visible: boolean) => void;
editingId: number | null;
setEditingId: (id: number | null) => void;
submitLoading: boolean;
// 操作方法
loadDatasets: () => Promise<void>;
loadAreas: () => Promise<void>;
handleCreate: (data: CreateDatasetRequest) => Promise<boolean>;
handleUpdate: (id: number, data: UpdateDatasetRequest) => Promise<boolean>;
handleDelete: (id: number) => Promise<boolean>;
// 权限
canManageDataset: boolean;
}
// ==================== Hook Implementation ====================
export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
// 权限控制
const { canManageDataset, canViewDataset, userRole } = usePermission();
// 数据状态
const [datasets, setDatasets] = useState<AreaDataset[]>([]);
const [loading, setLoading] = useState<boolean>(false);
const [total, setTotal] = useState<number>(0);
const [userArea, setUserArea] = useState<string>('');
const [userRoleState, setUserRoleState] = useState<string>('');
const [areas, setAreas] = useState<string[]>([]);
const [areasLoading, setAreasLoading] = useState<boolean>(false);
// 筛选状态
const [filterArea, setFilterArea] = useState<string>('');
const [page, setPage] = useState<number>(1);
const pageSize = 20;
// 表单状态
const [modalVisible, setModalVisible] = useState<boolean>(false);
const [editingId, setEditingId] = useState<number | null>(null);
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
// ==================== Data Loading ====================
/**
* 加载知识库列表
*/
const loadDatasets = useCallback(async () => {
if (!canViewDataset) {
message.warning('您没有查看知识库的权限');
return;
}
setLoading(true);
try {
let response;
if (canManageDataset) {
// 省级管理员:获取所有知识库
response = await getAllDatasets({
area: filterArea || undefined,
only_enabled: true,
page,
page_size: pageSize,
});
} else {
// 普通用户/市级管理员:获取我的知识库
response = await getMyDatasets();
}
if (response.code === 0) {
setDatasets(response.data.data);
setTotal(response.data.total);
// 如果是 my 接口,保存用户信息
if ('user_area' in response.data) {
setUserArea((response.data as any).user_area);
setUserRoleState((response.data as any).user_role);
}
} else {
message.error(`加载失败: ${response.message}`);
}
} catch (error: any) {
console.error('加载知识库失败:', error);
message.error('加载知识库失败,请稍后重试');
} finally {
setLoading(false);
}
}, [canManageDataset, canViewDataset, filterArea, page, pageSize]);
/**
* 加载地区列表(仅省级管理员)
*/
const loadAreas = useCallback(async () => {
if (!canManageDataset) return;
setAreasLoading(true);
try {
const areasList = await getAvailableAreas();
setAreas(areasList);
} catch (error: any) {
console.error('加载地区列表失败:', error);
message.error('加载地区列表失败');
} finally {
setAreasLoading(false);
}
}, [canManageDataset]);
// ==================== Operations ====================
/**
* 创建知识库绑定
*/
const handleCreate = useCallback(
async (data: CreateDatasetRequest): Promise<boolean> => {
if (!canManageDataset) {
message.error('您没有创建知识库绑定的权限');
return false;
}
setSubmitLoading(true);
try {
const response = await createDatasetBinding(data);
if (response.code === 0) {
message.success('创建成功');
await loadDatasets();
return true;
} else {
message.error(`创建失败: ${response.message}`);
return false;
}
} catch (error: any) {
console.error('创建知识库绑定失败:', error);
message.error('创建失败,请稍后重试');
return false;
} finally {
setSubmitLoading(false);
}
},
[canManageDataset, loadDatasets]
);
/**
* 更新知识库绑定
*/
const handleUpdate = useCallback(
async (id: number, data: UpdateDatasetRequest): Promise<boolean> => {
if (!canManageDataset) {
message.error('您没有编辑知识库绑定的权限');
return false;
}
setSubmitLoading(true);
try {
const response = await updateDatasetBinding(id, data);
if (response.code === 0) {
message.success('更新成功');
await loadDatasets();
return true;
} else {
message.error(`更新失败: ${response.message}`);
return false;
}
} catch (error: any) {
console.error('更新知识库绑定失败:', error);
message.error('更新失败,请稍后重试');
return false;
} finally {
setSubmitLoading(false);
}
},
[canManageDataset, loadDatasets]
);
/**
* 删除知识库绑定
*/
const handleDelete = useCallback(
async (id: number): Promise<boolean> => {
if (!canManageDataset) {
message.error('您没有删除知识库绑定的权限');
return false;
}
try {
const response = await deleteDatasetBinding(id);
if (response.code === 0) {
message.success('删除成功');
await loadDatasets();
return true;
} else {
message.error(`删除失败: ${response.message}`);
return false;
}
} catch (error: any) {
console.error('删除知识库绑定失败:', error);
message.error('删除失败,请稍后重试');
return false;
}
},
[canManageDataset, loadDatasets]
);
// ==================== Effects ====================
// 加载数据
useEffect(() => {
loadDatasets();
}, [loadDatasets]);
useEffect(() => {
loadAreas();
}, [loadAreas]);
// 监听筛选条件变化
useEffect(() => {
if (canManageDataset) {
setPage(1); // 筛选条件变化时重置到第一页
loadDatasets();
}
}, [filterArea, canManageDataset, loadDatasets]);
// 监听页码变化
useEffect(() => {
if (canManageDataset) {
loadDatasets();
}
}, [page, canManageDataset, loadDatasets]);
return {
// 数据
datasets,
loading,
total,
userArea,
userRole: userRoleState,
areas,
areasLoading,
// 筛选
filterArea,
setFilterArea,
page,
setPage,
pageSize,
// 表单状态
modalVisible,
setModalVisible,
editingId,
setEditingId,
submitLoading,
// 操作方法
loadDatasets,
loadAreas,
handleCreate,
handleUpdate,
handleDelete,
// 权限
canManageDataset,
};
}
@@ -0,0 +1,76 @@
/**
* PUT /api/v3/dify/area-datasets/{id} - 更新知识库绑定
* DELETE /api/v3/dify/area-datasets/{id} - 删除知识库绑定
*/
import { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
export async function loader({ request, params }: LoaderFunctionArgs) {
return json({ error: 'Method not allowed' }, { status: 405 });
}
/**
* 更新知识库绑定
*/
export async function action({ request, params }: LoaderFunctionArgs) {
try {
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const { id } = params;
const method = request.method;
if (method === 'PUT') {
// 更新知识库绑定
const body = await request.json();
console.log(`[API V3] Update Area Dataset: ${id}`, body);
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`;
const response = await backendRequest(apiUrl, {
method: 'PUT',
headers: {
Authorization: `Bearer ${frontendJWT}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await response.json();
return json(data, { status: response.status });
} else if (method === 'DELETE') {
// 删除知识库绑定
console.log(`[API V3] Delete Area Dataset: ${id}`);
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`;
const response = await backendRequest(apiUrl, {
method: 'DELETE',
headers: {
Authorization: `Bearer ${frontendJWT}`,
},
});
const data = await response.json();
return json(data, { status: response.status });
} else {
return json({ error: 'Method not allowed' }, { status: 405 });
}
} catch (error: any) {
console.error('[API V3] Area Dataset Action - Error:', error.message);
return json(
{ error: error.message || 'Operation failed' },
{ status: 500 }
);
}
}
@@ -0,0 +1,42 @@
/**
* GET /api/v3/dify/area-datasets/areas - 获取可用地区列表(管理员)
*/
import { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
export async function loader({ request }: LoaderFunctionArgs) {
try {
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
console.log('[API V3] Get Available Areas');
// 转发请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/areas`;
const response = await backendRequest(apiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${frontendJWT}`,
},
});
const data = await response.json();
return json(data, { status: response.status });
} catch (error: any) {
console.error('[API V3] Get Available Areas - Error:', error.message);
return json(
{ error: error.message || 'Failed to get available areas' },
{ status: 500 }
);
}
}
@@ -0,0 +1,42 @@
/**
* GET /api/v3/dify/area-datasets/my - 获取当前用户可访问的知识库列表
*/
import { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
export async function loader({ request }: LoaderFunctionArgs) {
try {
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
console.log('[API V3] Get My Area Datasets');
// 转发请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/my`;
const response = await backendRequest(apiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${frontendJWT}`,
},
});
const data = await response.json();
return json(data, { status: response.status });
} catch (error: any) {
console.error('[API V3] Get My Area Datasets - Error:', error.message);
return json(
{ error: error.message || 'Failed to get area datasets' },
{ status: 500 }
);
}
}
+100
View File
@@ -0,0 +1,100 @@
/**
* GET /api/v3/dify/area-datasets - 获取所有知识库绑定列表(管理员)
* POST /api/v3/dify/area-datasets - 创建知识库绑定
*/
import { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
/**
* GET - 获取所有知识库绑定列表
*/
export async function loader({ request }: LoaderFunctionArgs) {
try {
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
// 获取查询参数
const url = new URL(request.url);
const area = url.searchParams.get('area');
const only_enabled = url.searchParams.get('only_enabled');
const page = url.searchParams.get('page') || '1';
const page_size = url.searchParams.get('page_size') || '20';
// 构建查询参数
const params = new URLSearchParams();
if (area) params.append('area', area);
if (only_enabled !== null) params.append('only_enabled', only_enabled);
params.append('page', page);
params.append('page_size', page_size);
console.log('[API V3] Get All Area Datasets', { area, only_enabled, page, page_size });
// 转发请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets?${params}`;
const response = await backendRequest(apiUrl, {
method: 'GET',
headers: {
Authorization: `Bearer ${frontendJWT}`,
},
});
const data = await response.json();
return json(data, { status: response.status });
} catch (error: any) {
console.error('[API V3] Get All Area Datasets - Error:', error.message);
return json(
{ error: error.message || 'Failed to get all area datasets' },
{ status: 500 }
);
}
}
/**
* POST - 创建知识库绑定
*/
export async function action({ request }: LoaderFunctionArgs) {
try {
const { frontendJWT } = await getUserSession(request);
if (!frontendJWT) {
return new Response(
JSON.stringify({ error: 'JWT认证失败,请重新登录' }),
{ status: 401, headers: { 'Content-Type': 'application/json' } }
);
}
const body = await request.json();
console.log('[API V3] Create Area Dataset', body);
// 转发创建请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets`;
const response = await backendRequest(apiUrl, {
method: 'POST',
headers: {
Authorization: `Bearer ${frontendJWT}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(body),
});
const data = await response.json();
return json(data, { status: response.status });
} catch (error: any) {
console.error('[API V3] Create Area Dataset - Error:', error.message);
return json(
{ error: error.message || 'Failed to create area dataset' },
{ status: 500 }
);
}
}
+27 -17
View File
@@ -1,26 +1,36 @@
import { Tabs } from 'antd';
import DatasetManager from "~/components/dify-dataset-manager";
import datasetManagerStyles from "~/styles/components/dify-dataset-manager/index.css?url";
// import sidebarStyles from "~/styles/components/dify-dataset-manager/sidebar.css?url";
// import documentListStyles from "~/styles/components/dify-dataset-manager/document-list.css?url";
import AreaDatasetConfig from "~/components/dify-dataset-manager/area-dataset-config";
/**
* 注册样式
*/
export function links() {
return [
{ rel: "stylesheet", href: datasetManagerStyles },
// { rel: "stylesheet", href: sidebarStyles },
// { rel: "stylesheet", href: documentListStyles },
];
}
/**
* 知识库管理首页
* 知识库管理首页 - 带标签页导航
* 标签1: 知识库列表 - 进入单个知识库管理
* 标签2: 知识库配置管理 - 地区-知识库绑定管理
*/
export default function DatasetManagerIndex() {
const items = [
{
key: 'dataset-list',
label: '知识库列表',
children: <DatasetManager />,
},
{
key: 'area-config',
label: '知识库配置管理',
children: <AreaDatasetConfig />,
},
];
// 使用Tabs作为顶层导航,默认选中第一个
const defaultActiveTab = 'dataset-list';
return (
<div className="dataset-manager-page" style={{ height: '90vh', padding: '16px' }}>
<DatasetManager />
<div className="dataset-manager-container">
<Tabs
defaultActiveKey={defaultActiveTab}
items={items}
className="dataset-manager-tabs"
/>
</div>
);
}