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:
@@ -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,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的初始化逻辑
|
* 实现单页面应用模式,参考webapp-conversation的初始化逻辑
|
||||||
@@ -29,9 +47,9 @@ export default function Chat() {
|
|||||||
// 侧边栏状态
|
// 侧边栏状态
|
||||||
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
const [sidebarCollapsed, setSidebarCollapsed] = useState(false);
|
||||||
const [isMobile, setIsMobile] = useState(false);
|
const [isMobile, setIsMobile] = useState(false);
|
||||||
const {
|
|
||||||
token: { colorBgContainer, borderRadiusLG },
|
// 获取主题配置,避免SSR错误
|
||||||
} = theme.useToken();
|
const { colorBgContainer, borderRadiusLG } = useChatTheme();
|
||||||
|
|
||||||
// 会话管理
|
// 会话管理
|
||||||
const {
|
const {
|
||||||
|
|||||||
@@ -9,8 +9,9 @@ import {
|
|||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
SearchOutlined,
|
SearchOutlined,
|
||||||
} from '@ant-design/icons';
|
} 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 { forwardRef, useImperativeHandle, useState } from 'react';
|
||||||
|
import type { ChatApp } from '~/api/dify-chat-apps/types';
|
||||||
import type { ConversationItem } from '~/api/dify-chat';
|
import type { ConversationItem } from '~/api/dify-chat';
|
||||||
import { deleteConversation, renameConversation } from '~/api/dify-chat';
|
import { deleteConversation, renameConversation } from '~/api/dify-chat';
|
||||||
import '../../styles/components/chat-with-llm/sidebar.css';
|
import '../../styles/components/chat-with-llm/sidebar.css';
|
||||||
@@ -25,6 +26,11 @@ interface ChatSidebarProps {
|
|||||||
onConversationSelect: (conversationId: string) => void;
|
onConversationSelect: (conversationId: string) => void;
|
||||||
onNewConversation: () => void;
|
onNewConversation: () => void;
|
||||||
onConversationDeleted?: (conversationId: string) => void;
|
onConversationDeleted?: (conversationId: string) => void;
|
||||||
|
// 对话应用相关属性
|
||||||
|
chatApps: ChatApp[];
|
||||||
|
loadingChatApps: boolean;
|
||||||
|
currentChatApp: ChatApp | null;
|
||||||
|
onChatAppChange: (appId: string) => void;
|
||||||
onConversationRenamed?: (conversationId: string, newName: string) => void;
|
onConversationRenamed?: (conversationId: string, newName: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,6 +45,10 @@ export interface ChatSidebarRef {
|
|||||||
const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
||||||
collapsed,
|
collapsed,
|
||||||
onToggle,
|
onToggle,
|
||||||
|
chatApps,
|
||||||
|
loadingChatApps,
|
||||||
|
currentChatApp,
|
||||||
|
onChatAppChange,
|
||||||
conversations,
|
conversations,
|
||||||
currentConversationId,
|
currentConversationId,
|
||||||
onConversationSelect,
|
onConversationSelect,
|
||||||
@@ -281,6 +291,25 @@ const ChatSidebar = forwardRef<ChatSidebarRef, ChatSidebarProps>(({
|
|||||||
</div>
|
</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 && (
|
{!collapsed && (
|
||||||
<Input
|
<Input
|
||||||
placeholder="搜索对话..."
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,26 +1,36 @@
|
|||||||
|
import { Tabs } from 'antd';
|
||||||
import DatasetManager from "~/components/dify-dataset-manager";
|
import DatasetManager from "~/components/dify-dataset-manager";
|
||||||
import datasetManagerStyles from "~/styles/components/dify-dataset-manager/index.css?url";
|
import AreaDatasetConfig from "~/components/dify-dataset-manager/area-dataset-config";
|
||||||
// import sidebarStyles from "~/styles/components/dify-dataset-manager/sidebar.css?url";
|
|
||||||
// import documentListStyles from "~/styles/components/dify-dataset-manager/document-list.css?url";
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 注册样式
|
* 知识库管理首页 - 带标签页导航
|
||||||
*/
|
* 标签1: 知识库列表 - 进入单个知识库管理
|
||||||
export function links() {
|
* 标签2: 知识库配置管理 - 地区-知识库绑定管理
|
||||||
return [
|
|
||||||
{ rel: "stylesheet", href: datasetManagerStyles },
|
|
||||||
// { rel: "stylesheet", href: sidebarStyles },
|
|
||||||
// { rel: "stylesheet", href: documentListStyles },
|
|
||||||
];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 知识库管理首页
|
|
||||||
*/
|
*/
|
||||||
export default function DatasetManagerIndex() {
|
export default function DatasetManagerIndex() {
|
||||||
|
const items = [
|
||||||
|
{
|
||||||
|
key: 'dataset-list',
|
||||||
|
label: '知识库列表',
|
||||||
|
children: <DatasetManager />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'area-config',
|
||||||
|
label: '知识库配置管理',
|
||||||
|
children: <AreaDatasetConfig />,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 使用Tabs作为顶层导航,默认选中第一个
|
||||||
|
const defaultActiveTab = 'dataset-list';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dataset-manager-page" style={{ height: '90vh', padding: '16px' }}>
|
<div className="dataset-manager-container">
|
||||||
<DatasetManager />
|
<Tabs
|
||||||
|
defaultActiveKey={defaultActiveTab}
|
||||||
|
items={items}
|
||||||
|
className="dataset-manager-tabs"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,875 @@
|
|||||||
|
# Dify 模块 API 接口文档
|
||||||
|
|
||||||
|
> 版本:2.3
|
||||||
|
> 更新时间:2025-01-22
|
||||||
|
> 基础路径:`/api/v3`
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 目录
|
||||||
|
|
||||||
|
|
||||||
|
1. [概述](#概述)
|
||||||
|
2. [权限说明](#权限说明)
|
||||||
|
3. [安全设计](#安全设计)
|
||||||
|
4. [地区知识库管理接口](#地区知识库管理接口)
|
||||||
|
- [获取当前用户可访问的知识库](#1-获取当前用户可访问的知识库)
|
||||||
|
- [获取所有知识库绑定列表](#2-获取所有知识库绑定列表管理员)
|
||||||
|
- [获取可用地区列表](#3-获取可用地区列表)
|
||||||
|
- [获取知识库绑定详情](#4-获取知识库绑定详情)
|
||||||
|
- [创建知识库绑定](#5-创建知识库绑定)
|
||||||
|
|
||||||
|
- [更新知识库绑定](#6-更新知识库绑定)
|
||||||
|
- [删除知识库绑定](#7-删除知识库绑定)
|
||||||
|
- [检查用户知识库访问权限](#8- [对话应用管理接口](#9-检查用户知识库访问权限)
|
||||||
|
- [获取可用的对话应用列表](#9-获取可用的对话应用列表)
|
||||||
|
- [获取默认对话应用](#10-获取默认对话应用)
|
||||||
|
|
||||||
|
|
||||||
|
5. [对话应用多实例支持](#对话应用多实例支持)
|
||||||
|
- [配置方式](#配置方式)
|
||||||
|
- [前端切换应用](#前端切换应用)
|
||||||
|
- [端口多实例特性](#端口多实例特性)
|
||||||
|
6. [错误码说明](#错误码说明)
|
||||||
|
- [接口说明](#接口说明)
|
||||||
|
7. [数据字典](#数据字典)
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 概述
|
||||||
|
|
||||||
|
Dify 模块提供 AI 对话和知识库管理功能。本文档主要描述**地区-知识库绑定管理**相关接口。
|
||||||
|
|
||||||
|
### 核心概念
|
||||||
|
|
||||||
|
|概念 |说明 |
|
||||||
|
|---|---|
|
||||||
|
|**地区(area)** |用户所属地区,如:梅州、云浮、揭阳、潮州 |
|
||||||
|
|**知识库绑定** |将 Dify 平台的知识库与特定地区关联,控制用户可访问的知识库范围 |
|
||||||
|
|**公共知识库** |`is_public=true` 的知识库,所有地区用户均可访问(如省级知识库) |
|
||||||
|
|**默认知识库** |`is_default=true` 的知识库,该地区用户的首选知识库 |
|
||||||
|
|
||||||
|
### 业务规则
|
||||||
|
|
||||||
|
|
||||||
|
1. **普通用户(common)只能访问**本地区绑定的知识库 + **公共知识库**
|
||||||
|
2. **市级管理员(admin)只能访问**本地区绑定的知识库 + **公共知识库**,可编辑本地区知识库内容
|
||||||
|
3. **省级管理员(provincial_admin)可以访问和管理**所有地区的知识库
|
||||||
|
4. 只有**省级管理员**可以管理知识库绑定(增删改查绑定关系)
|
||||||
|
5. 同一地区不能重复绑定同一知识库
|
||||||
|
6. 删除为软删除,可恢复
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 权限说明
|
||||||
|
|
||||||
|
### 权限键定义(无通配符)
|
||||||
|
|
||||||
|
|权限键 |说明 |适用角色 |
|
||||||
|
|---|---|---|
|
||||||
|
|`dify:chat:use` |使用 AI 对话 |common, provincial_admin, admin |
|
||||||
|
|`dify:conversation:read` |查看对话历史(仅自己) |common, provincial_admin, admin |
|
||||||
|
|`dify:conversation:delete` |删除对话(仅自己) |common, provincial_admin, admin |
|
||||||
|
|`dify:message:feedback` |消息反馈 |common, provincial_admin, admin |
|
||||||
|
|`dify:dataset:read` |查看知识库列表 |common, admin, provincial_admin |
|
||||||
|
|`dify:dataset:write` |编辑知识库内容 |admin(仅本地区), provincial_admin(全部) |
|
||||||
|
|`dify:dataset:manage` |管理知识库绑定(增删改查) |provincial_admin(仅省级管理员) |
|
||||||
|
|`dify:file:read` |下载/预览文件 |common, provincial_admin, admin |
|
||||||
|
|`dify:file:upload` |上传文件 |common, provincial_admin, admin |
|
||||||
|
|
||||||
|
### 角色权限矩阵
|
||||||
|
|
||||||
|
|接口 |common |admin(市级管理员) |provincial_admin(省级管理员) |
|
||||||
|
|---|---|---|---|
|
||||||
|
|获取用户可访问的知识库 |✅ 本地区+公共 |✅ 本地区+公共 |✅ 全部 |
|
||||||
|
|编辑知识库内容 |❌ |✅ 仅本地区 |✅ 全部 |
|
||||||
|
|获取所有绑定列表(管理) |❌ |❌ |✅ |
|
||||||
|
|创建/更新/删除绑定 |❌ |❌ |✅ |
|
||||||
|
|检查访问权限 |✅ |✅ |✅ |
|
||||||
|
|
||||||
|
### 数据范围说明
|
||||||
|
|
||||||
|
|数据范围 |代码值 |说明 |
|
||||||
|
|---|---|---|
|
||||||
|
|全部 |`ALL` |可访问所有地区数据(仅 provincial_admin) |
|
||||||
|
|本地区 |`DEPT` |只能访问本地区数据 + 公共数据(common, admin) |
|
||||||
|
|仅自己 |`SELF` |只能操作自己的数据(对话历史) |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 安全设计
|
||||||
|
|
||||||
|
### 1. 对话数据越权防护
|
||||||
|
|
||||||
|
**问题**:用户 A 不能查看/删除用户 B 的对话
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
|
||||||
|
1. **Dify 端隔离**:请求时强制注入 `user` 参数为当前登录用户的 `username`,无法伪造
|
||||||
|
2. **本地数据库隔离**:`dify_conversation` 表的操作都带 `user_id` 条件
|
||||||
|
|
||||||
|
```sql
|
||||||
|
-- 删除对话时的 SQL
|
||||||
|
UPDATE dify_conversation
|
||||||
|
SET deleted_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE user_id = $1 AND conversation_id = $2 -- 必须是自己的
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 知识库数据范围控制
|
||||||
|
|
||||||
|
**问题**:普通用户和市级管理员不能访问其他地区的知识库
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 根据用户角色控制数据范围
|
||||||
|
if user_role == "provincial_admin":
|
||||||
|
# 仅省级管理员返回所有知识库
|
||||||
|
else:
|
||||||
|
# 市级管理员和普通用户:只返回本地区 + 公共知识库
|
||||||
|
WHERE area = {user_area} OR is_public = true
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. 请求伪造防护
|
||||||
|
|
||||||
|
**问题**:用户伪造请求参数访问他人数据
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
|
||||||
|
- 所有用户身份信息从 JWT Token 解析,不信任请求参数
|
||||||
|
- `user` 参数强制覆盖,不允许前端传入
|
||||||
|
|
||||||
|
```python
|
||||||
|
# process_request_body 中
|
||||||
|
body_data['user'] = username # 强制使用当前用户
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 地区知识库管理接口
|
||||||
|
|
||||||
|
### 1. 获取当前用户可访问的知识库
|
||||||
|
|
||||||
|
根据当前登录用户的地区和角色,返回可访问的知识库列表。
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v3/dify/area-datasets/my
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求头**
|
||||||
|
|
||||||
|
|参数 |类型 |必填 |说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
|Authorization |string |是 |Bearer {token} |
|
||||||
|
|
||||||
|
**数据范围控制**
|
||||||
|
|
||||||
|
|角色 |返回数据 |
|
||||||
|
|---|---|
|
||||||
|
|common(普通员工) |用户所属地区的知识库 + 公共知识库 |
|
||||||
|
|admin(市级管理员) |用户所属地区的知识库 + 公共知识库 |
|
||||||
|
|provincial_admin(省级管理员) |所有地区的知识库 |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 1,
|
||||||
|
"area": "省级",
|
||||||
|
"dataset_id": "fd680642-4493-416b-b592-972c69b3f595",
|
||||||
|
"dataset_name": "省级法务知识库",
|
||||||
|
"dataset_description": "全省通用法务知识库",
|
||||||
|
"is_default": false,
|
||||||
|
"is_public": true,
|
||||||
|
"sort_order": 0,
|
||||||
|
"status": 1,
|
||||||
|
"created_at": "2025-01-22T10:00:00",
|
||||||
|
"updated_at": "2025-01-22T10:00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"area": "梅州",
|
||||||
|
"dataset_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||||
|
"dataset_name": "梅州法务知识库",
|
||||||
|
"dataset_description": "梅州地区专用知识库",
|
||||||
|
"is_default": true,
|
||||||
|
"is_public": false,
|
||||||
|
"sort_order": 10,
|
||||||
|
"status": 1,
|
||||||
|
"created_at": "2025-01-22T10:30:00",
|
||||||
|
"updated_at": "2025-01-22T10:30:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 2,
|
||||||
|
"user_area": "梅州",
|
||||||
|
"user_role": "common"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应字段说明**
|
||||||
|
|
||||||
|
|字段 |类型 |说明 |
|
||||||
|
|---|---|---|
|
||||||
|
|data.data |array |知识库列表 |
|
||||||
|
|data.total |int |知识库总数 |
|
||||||
|
|data.user_area |string |当前用户所属地区 |
|
||||||
|
|data.user_role |string |当前用户角色 |
|
||||||
|
|
||||||
|
**错误响应**
|
||||||
|
|
||||||
|
|HTTP 状态码 |错误信息 |说明 |
|
||||||
|
|---|---|---|
|
||||||
|
|400 |用户未设置地区,无法获取知识库列表 |用户 area 字段为空(非管理员) |
|
||||||
|
|401 |JWT 认证失败 |Token 无效或过期 |
|
||||||
|
|403 |Permission denied |无 `dify:dataset:read` 权限 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. 获取所有知识库绑定列表(管理员)
|
||||||
|
|
||||||
|
获取系统中所有地区的知识库绑定记录,支持按地区筛选和分页。
|
||||||
|
|
||||||
|
**权限要求**:`dify:dataset:manage`(仅 provincial_admin)
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v3/dify/area-datasets
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求头**
|
||||||
|
|
||||||
|
|参数 |类型 |必填 |说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
|Authorization |string |是 |Bearer {token} |
|
||||||
|
|
||||||
|
**查询参数**
|
||||||
|
|
||||||
|
|参数 |类型 |必填 |默认值 |说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
|area |string |否 |- |筛选地区,如:梅州 |
|
||||||
|
|only_enabled |boolean |否 |true |是否只返回启用的记录 |
|
||||||
|
|page |int |否 |1 |页码,从 1 开始 |
|
||||||
|
|page_size |int |否 |20 |每页数量,范围 1-100 |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/v3/dify/area-datasets?area=梅州&page=1&page_size=20" \
|
||||||
|
-H "Authorization: Bearer {token}"
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"id": 2,
|
||||||
|
"area": "梅州",
|
||||||
|
"dataset_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||||
|
"dataset_name": "梅州法务知识库",
|
||||||
|
"dataset_description": "梅州地区专用知识库",
|
||||||
|
"is_default": true,
|
||||||
|
"is_public": false,
|
||||||
|
"sort_order": 10,
|
||||||
|
"status": 1,
|
||||||
|
"created_by": 1,
|
||||||
|
"created_at": "2025-01-22T10:30:00",
|
||||||
|
"updated_by": null,
|
||||||
|
"updated_at": "2025-01-22T10:30:00"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 1,
|
||||||
|
"page": 1,
|
||||||
|
"page_size": 20,
|
||||||
|
"has_more": false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. 获取可用地区列表
|
||||||
|
|
||||||
|
获取系统中所有可用的地区列表,用于管理员创建绑定时选择地区。
|
||||||
|
|
||||||
|
**权限要求**:`dify:dataset:manage`
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v3/dify/area-datasets/areas
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"data": ["云浮", "揭阳", "梅州", "潮州"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. 获取知识库绑定详情
|
||||||
|
|
||||||
|
根据绑定记录 ID 获取详情。
|
||||||
|
|
||||||
|
**权限要求**:`dify:dataset:manage`
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v3/dify/area-datasets/{dataset_bind_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
|参数 |类型 |必填 |说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
|dataset_bind_id |int |是 |绑定记录 ID |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"data": {
|
||||||
|
"id": 2,
|
||||||
|
"area": "梅州",
|
||||||
|
"dataset_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||||
|
"dataset_name": "梅州法务知识库",
|
||||||
|
"dataset_description": "梅州地区专用知识库",
|
||||||
|
"is_default": true,
|
||||||
|
"is_public": false,
|
||||||
|
"sort_order": 10,
|
||||||
|
"status": 1,
|
||||||
|
"created_by": 1,
|
||||||
|
"created_at": "2025-01-22T10:30:00",
|
||||||
|
"updated_by": null,
|
||||||
|
"updated_at": "2025-01-22T10:30:00"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. 创建知识库绑定
|
||||||
|
|
||||||
|
将 Dify 知识库绑定到指定地区。
|
||||||
|
|
||||||
|
**权限要求**:`dify:dataset:manage`
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
POST /api/v3/dify/area-datasets
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体**
|
||||||
|
|
||||||
|
|参数 |类型 |必填 |默认值 |说明 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
|area |string |是 |- |地区名称(1-50字符) |
|
||||||
|
|dataset_id |string |是 |- |Dify 知识库 ID(1-100字符) |
|
||||||
|
|dataset_name |string |是 |- |知识库名称(1-255字符) |
|
||||||
|
|dataset_description |string |否 |null |知识库描述 |
|
||||||
|
|is_default |boolean |否 |false |是否为该地区默认知识库 |
|
||||||
|
|is_public |boolean |否 |false |是否对所有地区公开 |
|
||||||
|
|sort_order |int |否 |0 |排序顺序,越小越靠前 |
|
||||||
|
|
||||||
|
**请求示例**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X POST "http://localhost:8000/api/v3/dify/area-datasets" \
|
||||||
|
-H "Authorization: Bearer {token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"area": "梅州",
|
||||||
|
"dataset_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||||
|
"dataset_name": "梅州法务知识库",
|
||||||
|
"dataset_description": "梅州地区专用知识库",
|
||||||
|
"is_default": true,
|
||||||
|
"is_public": false,
|
||||||
|
"sort_order": 10
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"data": {
|
||||||
|
"id": 2,
|
||||||
|
"area": "梅州",
|
||||||
|
"dataset_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||||
|
"dataset_name": "梅州法务知识库",
|
||||||
|
"dataset_description": "梅州地区专用知识库",
|
||||||
|
"is_default": true,
|
||||||
|
"is_public": false,
|
||||||
|
"sort_order": 10,
|
||||||
|
"status": 1,
|
||||||
|
"created_at": "2025-01-22T10:30:00"
|
||||||
|
},
|
||||||
|
"message": "创建成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**错误响应**
|
||||||
|
|
||||||
|
|HTTP 状态码 |错误信息 |说明 |
|
||||||
|
|---|---|---|
|
||||||
|
|400 |地区 '梅州' 已绑定知识库 'xxx' |重复绑定 |
|
||||||
|
|422 |Validation Error |参数校验失败 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. 更新知识库绑定
|
||||||
|
|
||||||
|
更新知识库绑定信息,支持部分更新。
|
||||||
|
|
||||||
|
**权限要求**:`dify:dataset:manage`
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
PUT /api/v3/dify/area-datasets/{dataset_bind_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**请求体**
|
||||||
|
|
||||||
|
|参数 |类型 |必填 |说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
|dataset_name |string |否 |知识库名称 |
|
||||||
|
|dataset_description |string |否 |知识库描述 |
|
||||||
|
|is_default |boolean |否 |是否默认 |
|
||||||
|
|is_public |boolean |否 |是否公开 |
|
||||||
|
|sort_order |int |否 |排序顺序 |
|
||||||
|
|status |int |否 |状态:1=启用, 0=禁用 |
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"data": {
|
||||||
|
"id": 2,
|
||||||
|
"area": "梅州",
|
||||||
|
"dataset_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab",
|
||||||
|
"dataset_name": "梅州法务知识库(更新)",
|
||||||
|
"is_default": true,
|
||||||
|
"sort_order": 5,
|
||||||
|
"status": 1,
|
||||||
|
"updated_at": "2025-01-22T11:00:00"
|
||||||
|
},
|
||||||
|
"message": "更新成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. 删除知识库绑定
|
||||||
|
|
||||||
|
软删除知识库绑定记录。
|
||||||
|
|
||||||
|
**权限要求**:`dify:dataset:manage`
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
DELETE /api/v3/dify/area-datasets/{dataset_bind_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"message": "删除成功"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. 检查用户知识库访问权限
|
||||||
|
|
||||||
|
检查当前用户是否有权访问指定的 Dify 知识库。
|
||||||
|
|
||||||
|
**权限要求**:`dify:dataset:read`
|
||||||
|
|
||||||
|
**请求**
|
||||||
|
|
||||||
|
```
|
||||||
|
GET /api/v3/dify/area-datasets/check/{dataset_id}
|
||||||
|
```
|
||||||
|
|
||||||
|
**路径参数**
|
||||||
|
|
||||||
|
|参数 |类型 |必填 |说明 |
|
||||||
|
|---|---|---|---|
|
||||||
|
|dataset_id |string |是 |Dify 知识库 ID |
|
||||||
|
|
||||||
|
**响应(有权限)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"has_access": true,
|
||||||
|
"user_area": "梅州",
|
||||||
|
"dataset_id": "a1b2c3d4-5678-90ab-cdef-1234567890ab"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**响应(无权限)**
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 0,
|
||||||
|
"message": "success",
|
||||||
|
"data": {
|
||||||
|
"has_access": false,
|
||||||
|
"user_area": "梅州",
|
||||||
|
"dataset_id": "xyz-not-allowed"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 错误码说明
|
||||||
|
|
||||||
|
### HTTP 状态码
|
||||||
|
|
||||||
|
|状态码 |说明 |
|
||||||
|
|---|---|
|
||||||
|
|200 |请求成功 |
|
||||||
|
|400 |请求参数错误 |
|
||||||
|
|401 |未认证或 Token 无效 |
|
||||||
|
|403 |权限不足 |
|
||||||
|
|404 |资源不存在 |
|
||||||
|
|422 |参数校验失败 |
|
||||||
|
|500 |服务器内部错误 |
|
||||||
|
|
||||||
|
### 业务错误码
|
||||||
|
|
||||||
|
|code |说明 |
|
||||||
|
|---|---|
|
||||||
|
|0 |成功 |
|
||||||
|
|-1 |通用错误 |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据字典
|
||||||
|
|
||||||
|
### dify_area_dataset 表
|
||||||
|
|
||||||
|
|字段 |类型 |说明 |
|
||||||
|
|---|---|---|
|
||||||
|
|id |int |主键 ID |
|
||||||
|
|area |varchar(50) |地区名称 |
|
||||||
|
|dataset_id |varchar(100) |Dify 知识库 ID |
|
||||||
|
|dataset_name |varchar(255) |知识库名称 |
|
||||||
|
|dataset_description |text |知识库描述 |
|
||||||
|
|is_default |boolean |是否为该地区默认知识库 |
|
||||||
|
|is_public |boolean |是否对所有地区公开 |
|
||||||
|
|sort_order |int |排序顺序,越小越靠前 |
|
||||||
|
|status |smallint |状态:1=启用, 0=禁用 |
|
||||||
|
|created_by |int |创建人 ID |
|
||||||
|
|created_at |timestamp |创建时间 |
|
||||||
|
|updated_by |int |更新人 ID |
|
||||||
|
|updated_at |timestamp |更新时间 |
|
||||||
|
|deleted_at |timestamp |删除时间(软删除) |
|
||||||
|
|
||||||
|
### 地区枚举值
|
||||||
|
|
||||||
|
|值 |说明 |
|
||||||
|
|---|---|
|
||||||
|
|梅州 |梅州地区 |
|
||||||
|
|云浮 |云浮地区 |
|
||||||
|
|揭阳 |揭阳地区 |
|
||||||
|
|潮州 |潮州地区 |
|
||||||
|
|省级 |省级(用于公共知识库) |
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 使用示例
|
||||||
|
|
||||||
|
### 场景1:管理员配置知识库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 1. 创建省级公共知识库(所有用户可见)
|
||||||
|
curl -X POST "/api/v3/dify/area-datasets" \
|
||||||
|
-H "Authorization: Bearer {admin_token}" \
|
||||||
|
-d '{
|
||||||
|
"area": "省级",
|
||||||
|
"dataset_id": "省级知识库ID",
|
||||||
|
"dataset_name": "省级法务知识库",
|
||||||
|
"is_public": true,
|
||||||
|
"sort_order": 0
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 2. 为各地区创建专属知识库(仅该地区用户可见)
|
||||||
|
curl -X POST "/api/v3/dify/area-datasets" \
|
||||||
|
-d '{"area": "梅州", "dataset_id": "梅州知识库ID", "dataset_name": "梅州法务知识库", "is_default": true}'
|
||||||
|
|
||||||
|
curl -X POST "/api/v3/dify/area-datasets" \
|
||||||
|
-d '{"area": "云浮", "dataset_id": "云浮知识库ID", "dataset_name": "云浮法务知识库", "is_default": true}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景2:普通用户获取可访问的知识库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 梅州用户(common 角色)
|
||||||
|
curl -X GET "/api/v3/dify/area-datasets/my" \
|
||||||
|
-H "Authorization: Bearer {meizhou_user_token}"
|
||||||
|
|
||||||
|
# 返回:省级公共知识库 + 梅州专属知识库(2条)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景3:省级管理员获取所有知识库
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 省级管理员(provincial_admin 角色)
|
||||||
|
curl -X GET "/api/v3/dify/area-datasets/my" \
|
||||||
|
-H "Authorization: Bearer {provincial_admin_token}"
|
||||||
|
|
||||||
|
# 返回:所有地区的知识库(包括梅州、云浮、揭阳、潮州、省级)
|
||||||
|
```
|
||||||
|
|
||||||
|
### 场景4:前端调用 Dify API 前检查权限
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 检查用户是否有权访问某个知识库
|
||||||
|
curl -X GET "/api/v3/dify/area-datasets/check/某知识库ID" \
|
||||||
|
-H "Authorization: Bearer {token}"
|
||||||
|
|
||||||
|
# 根据 has_access 字段决定是否允许调用 Dify API
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 对话应用管理接口
|
||||||
|
|
||||||
|
### 9.1 获取可用的对话应用列表
|
||||||
|
|
||||||
|
**接口**: `GET /api/v3/dify/chat-apps`
|
||||||
|
|
||||||
|
**权限**: `dify:chat:use`
|
||||||
|
|
||||||
|
**说明**: 获取当前实例配置的所有对话应用列表,供前端切换使用。
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/v3/dify/chat-apps" \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body**: `success (data)`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "OK",
|
||||||
|
"data": {
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"app_id": "app-xxx",
|
||||||
|
"app_name": "AI法务助手",
|
||||||
|
"description": "通用法律咨询助手",
|
||||||
|
"is_default": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"app_id": "app-yyy",
|
||||||
|
"app_name": "合同审查助手",
|
||||||
|
"description": "专业合同分析",
|
||||||
|
"is_default": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"total": 2
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**字段说明**:
|
||||||
|
|
||||||
|
|字段 |类型 |说明 |
|
||||||
|
|---|---|---|
|
||||||
|
|`app_id` |string |对话应用 ID |
|
||||||
|
|`app_name` |string |对话应用名称 |
|
||||||
|
|`description` |string |应用描述 |
|
||||||
|
|`is_default` |boolean |是否为默认应用 |
|
||||||
|
|
||||||
|
### 9.2 获取默认对话应用
|
||||||
|
|
||||||
|
**接口**: `GET /api/v3/dify/chat-apps/default`
|
||||||
|
|
||||||
|
**权限**: `dify:chat:use`
|
||||||
|
|
||||||
|
**说明**: 获取当前配置的默认对话应用。
|
||||||
|
|
||||||
|
**请求示例**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -X GET "http://localhost:8000/api/v3/dify/chat-apps/default" \
|
||||||
|
-H "Authorization: Bearer <token>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Response Body**: `success (data)`
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"code": 200,
|
||||||
|
"msg": "OK",
|
||||||
|
"data": {
|
||||||
|
"data": {
|
||||||
|
"app_id": "app-xxx",
|
||||||
|
"app_name": "AI法务助手",
|
||||||
|
"description": "通用法律咨询助手",
|
||||||
|
"is_default": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 对话应用多实例支持
|
||||||
|
|
||||||
|
### 10.1 配置方式
|
||||||
|
|
||||||
|
在 `config/env.{port}` 配置文件中添加 `DIFY_CHAT_APPS` 配置项:
|
||||||
|
|
||||||
|
**格式**:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
DIFY_CHAT_APPS=app_id:api_key:app_name:description|app_id2:api_key2:app_name2:description
|
||||||
|
|
||||||
|
**说明**:
|
||||||
|
- 多个应用用 `|` 分隔
|
||||||
|
- 每个应用格式:`app_id`:`api_key`:`app_name`:`description`
|
||||||
|
- 第一个应用为默认应用
|
||||||
|
|
||||||
|
**示例**:
|
||||||
|
```ini
|
||||||
|
DIFY_CHAT_APPS=app-dmYZISz60ZGQHlJAPHmngoZZ:app-dmYZISz60ZGQHlJAPHmngoZZ:AI法务助手:通用法律咨询助手|app-xxx:app-xxx:合同审查助手:专业合同分析
|
||||||
|
```
|
||||||
|
|
||||||
|
### 10.2 前端切换应用
|
||||||
|
|
||||||
|
发送对话消息时,可通过以下方式指定应用:
|
||||||
|
|
||||||
|
**方式 1:请求头(推荐)**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
axios.post('/api/dify/chat/chat-messages', data, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer {token}',
|
||||||
|
'X-Dify-App-Id': 'app-xxx' // 指定应用 ID
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式 2:查询参数**
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
axios.post('/api/dify/chat/chat-messages?app_id=app-xxx', data, {
|
||||||
|
headers: {
|
||||||
|
'Authorization': 'Bearer {token}'
|
||||||
|
}
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
**使用默认应用**:
|
||||||
|
|
||||||
|
不指定 `X-Dify-App-Id` 或 `app_id` 时,使用配置文件中的第一个应用作为默认应用。
|
||||||
|
|
||||||
|
### 10.3 端口多实例特性
|
||||||
|
|
||||||
|
本系统的特点是**端口多实例**部署架构,每个端口读取对应的配置文件:
|
||||||
|
|
||||||
|
|端口 |配置文件 |默认对话应用 |
|
||||||
|
|---|---|---|
|
||||||
|
|8073 |`config/env.8073` |梅州实例的对话应用 |
|
||||||
|
|8000 |`config/env.8000` |云浮实例的对话应用 |
|
||||||
|
|8001 |`config/env.8001` |揭阳实例的对话应用 |
|
||||||
|
|8002 |`config/env.8002` |潮州实例的对话应用 |
|
||||||
|
|
||||||
|
因此,前端请求不同端口时,会获得不同的对话应用列表:
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
// 请求梅州实例
|
||||||
|
GET http://172.16.0.55:8073/api/v3/dify/chat-apps
|
||||||
|
// 返回梅州配置: [梅州法务助手, 梅州合同审查]
|
||||||
|
|
||||||
|
// 请求云浮实例
|
||||||
|
GET http://172.16.0.55:8000/api/v3/dify/chat-apps
|
||||||
|
// 返回云浮配置: [云浮法务助手, 云浮合同审查]
|
||||||
|
```
|
||||||
|
|
||||||
|
这种设计实现了区域间的业务隔离,无需后端代码判断,完全由配置驱动。
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. 更新日志
|
||||||
|
|
||||||
|
|版本 |日期 |说明 |
|
||||||
|
|---|---|---|
|
||||||
|
|2.3 |2025-01-22 |新增对话应用多实例支持,支持配置多个对话应用供前端切换 |
|
||||||
|
|2.2 |2025-01-22 |修正角色权限:市级管理员(admin)只能访问本地区,省级管理员(provincial_admin)可访问全部;知识库绑定管理仅限省级管理员 |
|
||||||
|
|2.1 |2025-01-22 |修正权限角色(去掉 area_admin),添加数据范围控制和越权防护说明 |
|
||||||
|
|2.0 |2025-01-22 |新增地区-知识库绑定管理接口 |
|
||||||
|
|1.0 |2025-01-20 |初始版本,Dify 代理接口 |
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user