From 27aff59152852f3007fc48633cb1eb3069819bb9 Mon Sep 17 00:00:00 2001 From: Wenyan Date: Sun, 7 Dec 2025 23:12:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E7=9F=A5=E8=AF=86?= =?UTF-8?q?=E5=BA=93=E9=85=8D=E7=BD=AE=E7=AE=A1=E7=90=86=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增地区-知识库绑定管理功能,支持增删改查操作 - 添加 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 --- app/api/v3/dify/area-datasets.ts | 158 ++ app/components/dify-chat/index.tsx | 24 +- app/components/dify-chat/sidebar.tsx | 31 +- .../area-dataset-config.tsx | 662 ++++++++ app/hooks/use-area-dataset-config.ts | 308 ++++ app/routes/api.v3.dify.area-datasets.$id.tsx | 76 + .../api.v3.dify.area-datasets.areas.tsx | 42 + app/routes/api.v3.dify.area-datasets.my.tsx | 42 + app/routes/api.v3.dify.area-datasets.tsx | 100 ++ app/routes/chat-with-llm.dataset-manager.tsx | 44 +- .../area-dataset-config.css | 0 docs/new-dify/dify_api_doc.md | 875 +++++++++++ docs/new-dify/dify_frontend_integration.md | 1399 +++++++++++++++++ 13 files changed, 3740 insertions(+), 21 deletions(-) create mode 100644 app/api/v3/dify/area-datasets.ts create mode 100644 app/components/dify-dataset-manager/area-dataset-config.tsx create mode 100644 app/hooks/use-area-dataset-config.ts create mode 100644 app/routes/api.v3.dify.area-datasets.$id.tsx create mode 100644 app/routes/api.v3.dify.area-datasets.areas.tsx create mode 100644 app/routes/api.v3.dify.area-datasets.my.tsx create mode 100644 app/routes/api.v3.dify.area-datasets.tsx create mode 100644 app/styles/components/dify-dataset-manager/area-dataset-config.css create mode 100644 docs/new-dify/dify_api_doc.md create mode 100644 docs/new-dify/dify_frontend_integration.md diff --git a/app/api/v3/dify/area-datasets.ts b/app/api/v3/dify/area-datasets.ts new file mode 100644 index 0000000..087ee51 --- /dev/null +++ b/app/api/v3/dify/area-datasets.ts @@ -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 { + code: number; + message: string; + data: T; +} + +// ==================== API Functions ==================== + +const API_BASE = '/api/v3/dify/area-datasets'; + +/** + * 获取当前用户可访问的知识库列表 + * 权限: dify:dataset:read + */ +export async function getMyDatasets(): Promise { + 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 { + 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 { + const response = await request.get(`${API_BASE}/areas`); + return response.data.data; +} + +/** + * 创建知识库绑定(管理员) + * 权限: dify:dataset:manage + */ +export async function createDatasetBinding( + data: CreateDatasetRequest +): Promise> { + const response = await request.post(`${API_BASE}`, data); + return response.data; +} + +/** + * 更新知识库绑定(管理员) + * 权限: dify:dataset:manage + */ +export async function updateDatasetBinding( + id: number, + data: UpdateDatasetRequest +): Promise> { + const response = await request.put(`${API_BASE}/${id}`, data); + return response.data; +} + +/** + * 删除知识库绑定(管理员) + * 权限: dify:dataset:manage + */ +export async function deleteDatasetBinding(id: number): Promise> { + const response = await request.delete(`${API_BASE}/${id}`); + return response.data; +} diff --git a/app/components/dify-chat/index.tsx b/app/components/dify-chat/index.tsx index 1fa135b..4b1ccf4 100644 --- a/app/components/dify-chat/index.tsx +++ b/app/components/dify-chat/index.tsx @@ -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 { diff --git a/app/components/dify-chat/sidebar.tsx b/app/components/dify-chat/sidebar.tsx index bbbaf66..5ba0205 100644 --- a/app/components/dify-chat/sidebar.tsx +++ b/app/components/dify-chat/sidebar.tsx @@ -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(({ collapsed, onToggle, + chatApps, + loadingChatApps, + currentChatApp, + onChatAppChange, conversations, currentConversationId, onConversationSelect, @@ -281,6 +291,25 @@ const ChatSidebar = forwardRef(({ {/* 搜索框 */} + {/* 对话应用选择器 */} + {!collapsed && ( +
+ +
+ )} {!collapsed && ( ([]); + const [difyDatasetsLoading, setDifyDatasetsLoading] = useState(false); + const [difyDatasetsTotal, setDifyDatasetsTotal] = useState(0); + const [difyDatasetsPage, setDifyDatasetsPage] = useState(1); + const [isLoadingDifyDatasets, setIsLoadingDifyDatasets] = useState(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) => { + 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 = { + 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) => ( + {area} + ), + }, + { + title: '知识库名称', + dataIndex: 'dataset_name', + key: 'dataset_name', + width: 200, + ellipsis: true, + render: (text: string) => ( + + + {text} + + + ), + }, + { + title: '知识库ID', + dataIndex: 'dataset_id', + key: 'dataset_id', + width: 200, + ellipsis: true, + render: (text: string) => ( + + + {text.substring(0, 8)}...{text.substring(text.length - 4)} + + + ), + }, + { + title: '描述', + dataIndex: 'dataset_description', + key: 'dataset_description', + ellipsis: true, + render: (text: string) => + text ? ( + + + {text.length > 30 ? text.substring(0, 30) + '...' : text} + + + ) : ( + - + ), + }, + { + title: '标签', + key: 'tags', + width: 120, + render: (_: any, record: AreaDataset) => ( + + {record.is_public && ( + }> + 公共 + + )} + {record.is_default && ( + + 默认 + + )} + + ), + }, + { + 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) => ( + + {status === 1 ? '启用' : '禁用'} + + ), + }, + { + title: '创建时间', + dataIndex: 'created_at', + key: 'created_at', + width: 150, + render: (text: string) => ( + + {new Date(text).toLocaleString('zh-CN')} + + ), + }, + // 操作列(仅管理员可见) + ...(canManageDataset + ? [ + { + title: '操作', + key: 'actions', + width: 120, + fixed: 'right' as const, + render: (_: any, record: AreaDataset) => ( + + + handleDeleteClick(record.id)} + okText="确定" + cancelText="取消" + > + + + + ), + }, + ] + : []), + ]; + + return ( +
+ {/* 页面头部 */} + + +
+ + 知识库配置管理 + + + + 地区: {userArea || '-'} + + + 角色: {userRoleLabel} + + + 总数: {total} + + +
+ + {/* 仅管理员显示新增按钮 */} + {canManageDataset && ( + + )} +
+
+ + {/* 筛选区域(仅管理员可见) */} + {canManageDataset && ( + + + 地区筛选: + ({ + label: area, + value: area, + }))} + /> + + + {/* Dify知识库选择(仅新增时可选) */} + + + + + {/* 知识库描述 */} + + + + + {/* 高级设置折叠面板 */} +
+ + 高级设置 + + + + {/* 是否公开 */} + + + + + {/* 是否默认 */} + + + + + {/* 排序顺序 */} + + + + +
+ + +
+ ); +} diff --git a/app/hooks/use-area-dataset-config.ts b/app/hooks/use-area-dataset-config.ts new file mode 100644 index 0000000..5cd85bb --- /dev/null +++ b/app/hooks/use-area-dataset-config.ts @@ -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; + loadAreas: () => Promise; + handleCreate: (data: CreateDatasetRequest) => Promise; + handleUpdate: (id: number, data: UpdateDatasetRequest) => Promise; + handleDelete: (id: number) => Promise; + + // 权限 + canManageDataset: boolean; +} + +// ==================== Hook Implementation ==================== + +export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn { + // 权限控制 + const { canManageDataset, canViewDataset, userRole } = usePermission(); + + // 数据状态 + const [datasets, setDatasets] = useState([]); + const [loading, setLoading] = useState(false); + const [total, setTotal] = useState(0); + const [userArea, setUserArea] = useState(''); + const [userRoleState, setUserRoleState] = useState(''); + const [areas, setAreas] = useState([]); + const [areasLoading, setAreasLoading] = useState(false); + + // 筛选状态 + const [filterArea, setFilterArea] = useState(''); + const [page, setPage] = useState(1); + const pageSize = 20; + + // 表单状态 + const [modalVisible, setModalVisible] = useState(false); + const [editingId, setEditingId] = useState(null); + const [submitLoading, setSubmitLoading] = useState(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 => { + 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 => { + 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 => { + 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, + }; +} diff --git a/app/routes/api.v3.dify.area-datasets.$id.tsx b/app/routes/api.v3.dify.area-datasets.$id.tsx new file mode 100644 index 0000000..bba19a6 --- /dev/null +++ b/app/routes/api.v3.dify.area-datasets.$id.tsx @@ -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 } + ); + } +} diff --git a/app/routes/api.v3.dify.area-datasets.areas.tsx b/app/routes/api.v3.dify.area-datasets.areas.tsx new file mode 100644 index 0000000..3c7adcc --- /dev/null +++ b/app/routes/api.v3.dify.area-datasets.areas.tsx @@ -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 } + ); + } +} diff --git a/app/routes/api.v3.dify.area-datasets.my.tsx b/app/routes/api.v3.dify.area-datasets.my.tsx new file mode 100644 index 0000000..14a43c7 --- /dev/null +++ b/app/routes/api.v3.dify.area-datasets.my.tsx @@ -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 } + ); + } +} diff --git a/app/routes/api.v3.dify.area-datasets.tsx b/app/routes/api.v3.dify.area-datasets.tsx new file mode 100644 index 0000000..3452e5d --- /dev/null +++ b/app/routes/api.v3.dify.area-datasets.tsx @@ -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 } + ); + } +} diff --git a/app/routes/chat-with-llm.dataset-manager.tsx b/app/routes/chat-with-llm.dataset-manager.tsx index 16f641b..cc608d2 100644 --- a/app/routes/chat-with-llm.dataset-manager.tsx +++ b/app/routes/chat-with-llm.dataset-manager.tsx @@ -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: , + }, + { + key: 'area-config', + label: '知识库配置管理', + children: , + }, + ]; + + // 使用Tabs作为顶层导航,默认选中第一个 + const defaultActiveTab = 'dataset-list'; + return ( -
- +
+
); } diff --git a/app/styles/components/dify-dataset-manager/area-dataset-config.css b/app/styles/components/dify-dataset-manager/area-dataset-config.css new file mode 100644 index 0000000..e69de29 diff --git a/docs/new-dify/dify_api_doc.md b/docs/new-dify/dify_api_doc.md new file mode 100644 index 0000000..588dbc5 --- /dev/null +++ b/docs/new-dify/dify_api_doc.md @@ -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 " +``` + +**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 " +``` + +**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 代理接口 | + + diff --git a/docs/new-dify/dify_frontend_integration.md b/docs/new-dify/dify_frontend_integration.md new file mode 100644 index 0000000..71fdd0f --- /dev/null +++ b/docs/new-dify/dify_frontend_integration.md @@ -0,0 +1,1399 @@ +# Dify 模块前端对接文档 + +> 版本:1.0 +> 更新时间:2025-12-06 +> 适用前端:Vue 3 + TypeScript + +--- + +## 目录 + +1. [概述](#概述) +2. [权限体系](#权限体系) +3. [API 接口清单](#api-接口清单) +4. [大模型对话模块](#大模型对话模块) +5. [知识库管理模块](#知识库管理模块) +6. [权限控制实现](#权限控制实现) +7. [错误处理](#错误处理) +8. [完整代码示例](#完整代码示例) + +--- + +## 概述 + +### 模块结构 + +``` +AI法务助手 /chat-with-llm +├── 大模型对话 /chat-with-llm/chat +│ ├── 使用AI对话 (dify:chat:use) +│ ├── 查看对话历史 (dify:conversation:read) +│ ├── 删除对话 (dify:conversation:delete) +│ └── 消息反馈 (dify:message:feedback) +│ +└── 知识库管理 /chat-with-llm/dataset-manager + ├── 查看知识库 (dify:dataset:read) + ├── 编辑知识库内容 (dify:dataset:write) + ├── 管理知识库绑定 (dify:dataset:manage) + ├── 下载文件 (dify:file:read) + └── 上传文件 (dify:file:upload) +``` + +### 角色权限矩阵 + +| 功能 | common (普通员工) | admin (市级管理员) | provincial_admin (省级管理员) | +|------|------------------|-------------------|------------------------------| +| 大模型对话 | ✅ | ✅ | ✅ | +| 查看对话历史 | ✅ 仅自己 | ✅ 仅自己 | ✅ 仅自己 | +| 删除对话 | ✅ 仅自己 | ✅ 仅自己 | ✅ 仅自己 | +| 查看知识库 | ✅ 本地区+公共 | ✅ 本地区+公共 | ✅ 全部 | +| 编辑知识库内容 | ❌ | ✅ 仅本地区 | ✅ 全部 | +| 管理知识库绑定 | ❌ | ❌ | ✅ | +| 文件上传/下载 | ✅ | ✅ | ✅ | + +--- + +## 权限体系 + +### 权限标识 (permission_key) + +| 权限标识 | 显示名称 | HTTP方法 | API路径 | +|---------|---------|----------|---------| +| `dify:chat:use` | 使用AI对话 | POST | /api/dify/chat/chat-messages | +| `dify:conversation:read` | 查看对话历史 | GET | /api/dify/chat/conversations | +| `dify:conversation:delete` | 删除对话 | DELETE | /api/dify/chat/conversations/{conversation_id} | +| `dify:message:feedback` | 消息反馈 | POST | /api/dify/chat/messages/{message_id}/feedbacks | +| `dify:dataset:read` | 查看知识库 | GET | /api/v3/dify/area-datasets/my | +| `dify:dataset:write` | 编辑知识库内容 | POST | /api/dify/dataset/documents | +| `dify:dataset:manage` | 管理知识库绑定 | POST | /api/v3/dify/area-datasets | +| `dify:file:read` | 下载文件 | GET | /api/dify/file/{file_id}/file-preview | +| `dify:file:upload` | 上传文件 | POST | /api/dify/chat/files/upload | + +### 前端权限检查 + +```typescript +// stores/permission.ts +import { defineStore } from 'pinia' + +interface RoutePermission { + route_path: string + permissions: string[] +} + +export const usePermissionStore = defineStore('permission', { + state: () => ({ + routes: [] as RoutePermission[], + routesFlat: [] as RoutePermission[] + }), + + actions: { + // 检查是否有某个权限 + hasPermission(permissionKey: string): boolean { + return this.routesFlat.some(route => + route.permissions.includes(permissionKey) + ) + }, + + // 检查是否有某个路由下的权限 + hasRoutePermission(routePath: string, permissionKey: string): boolean { + const route = this.routesFlat.find(r => r.route_path === routePath) + return route?.permissions.includes(permissionKey) ?? false + } + } +}) + +// 使用示例 +const permissionStore = usePermissionStore() + +// 检查是否可以编辑知识库 +if (permissionStore.hasPermission('dify:dataset:write')) { + // 显示编辑按钮 +} + +// 检查是否可以管理知识库绑定 +if (permissionStore.hasPermission('dify:dataset:manage')) { + // 显示管理按钮 +} +``` + +--- + +## API 接口清单 + +### 基础配置 + +```typescript +// api/config.ts +const API_BASE = '/api' + +export const DIFY_API = { + // 对话相关 + CHAT_MESSAGES: `${API_BASE}/dify/chat/chat-messages`, + CONVERSATIONS: `${API_BASE}/dify/chat/conversations`, + CONVERSATION_MESSAGES: (id: string) => `${API_BASE}/dify/chat/conversations/${id}/messages`, + CONVERSATION_DELETE: (id: string) => `${API_BASE}/dify/chat/conversations/${id}`, + CONVERSATION_RENAME: (id: string) => `${API_BASE}/dify/chat/conversations/${id}/name`, + MESSAGE_FEEDBACK: (id: string) => `${API_BASE}/dify/chat/messages/${id}/feedbacks`, + SUGGESTED_QUESTIONS: (id: string) => `${API_BASE}/dify/chat/messages/${id}/suggested`, + + // 文件相关 + FILE_UPLOAD: `${API_BASE}/dify/chat/files/upload`, + FILE_PREVIEW: (id: string) => `${API_BASE}/dify/file/${id}/file-preview`, + + // 知识库相关 (新增) + AREA_DATASETS_MY: `${API_BASE}/v3/dify/area-datasets/my`, + AREA_DATASETS: `${API_BASE}/v3/dify/area-datasets`, + AREA_DATASETS_DETAIL: (id: number) => `${API_BASE}/v3/dify/area-datasets/${id}`, + AREA_DATASETS_AREAS: `${API_BASE}/v3/dify/area-datasets/areas`, + AREA_DATASETS_CHECK: (datasetId: string) => `${API_BASE}/v3/dify/area-datasets/check/${datasetId}`, +} +``` + +--- + +## 大模型对话模块 + +### 1. 发送对话消息 + +**权限要求**: `dify:chat:use` + +```typescript +// api/dify/chat.ts +interface ChatMessageRequest { + query: string // 用户输入的问题 + conversation_id?: string // 对话ID(首次对话不传) + response_mode?: 'streaming' | 'blocking' // 响应模式,默认 streaming + files?: Array<{ + type: 'image' | 'document' + transfer_method: 'local_file' + upload_file_id: string + }> +} + +interface ChatMessageResponse { + event: string + message_id: string + conversation_id: string + answer: string + created_at: number +} + +// 流式对话(推荐) +export async function sendChatMessageStream( + data: ChatMessageRequest, + onMessage: (chunk: string) => void, + onDone: (response: ChatMessageResponse) => void, + onError: (error: Error) => void +) { + const response = await fetch(DIFY_API.CHAT_MESSAGES, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${getToken()}`, + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + ...data, + response_mode: 'streaming' + }) + }) + + if (!response.ok) { + if (response.status === 403) { + onError(new Error('权限不足:您没有使用AI对话的权限')) + return + } + throw new Error(`HTTP error! status: ${response.status}`) + } + + const reader = response.body?.getReader() + const decoder = new TextDecoder() + + while (true) { + const { done, value } = await reader!.read() + if (done) break + + const chunk = decoder.decode(value) + const lines = chunk.split('\n') + + for (const line of lines) { + if (line.startsWith('data: ')) { + const data = JSON.parse(line.slice(6)) + + if (data.event === 'message') { + onMessage(data.answer) + } else if (data.event === 'message_end') { + onDone(data) + } else if (data.event === 'error') { + onError(new Error(data.message)) + } + } + } + } +} + +// 阻塞式对话 +export async function sendChatMessage(data: ChatMessageRequest): Promise { + const response = await request.post(DIFY_API.CHAT_MESSAGES, { + ...data, + response_mode: 'blocking' + }) + return response.data +} +``` + +**使用示例**: + +```vue + + + +``` + +### 2. 获取对话历史列表 + +**权限要求**: `dify:conversation:read` + +```typescript +interface Conversation { + id: string + name: string + created_at: number + updated_at: number +} + +interface ConversationListResponse { + data: Conversation[] + has_more: boolean + limit: number +} + +export async function getConversations( + limit: number = 20, + last_id?: string +): Promise { + const params = new URLSearchParams({ limit: limit.toString() }) + if (last_id) params.append('last_id', last_id) + + const response = await request.get(`${DIFY_API.CONVERSATIONS}?${params}`) + return response.data +} +``` + +### 3. 获取对话消息详情 + +**权限要求**: `dify:conversation:read` + +```typescript +interface Message { + id: string + conversation_id: string + query: string + answer: string + created_at: number + feedback?: { + rating: 'like' | 'dislike' + } +} + +interface MessageListResponse { + data: Message[] + has_more: boolean + limit: number +} + +export async function getConversationMessages( + conversationId: string, + limit: number = 20, + first_id?: string +): Promise { + const params = new URLSearchParams({ limit: limit.toString() }) + if (first_id) params.append('first_id', first_id) + + const response = await request.get( + `${DIFY_API.CONVERSATION_MESSAGES(conversationId)}?${params}` + ) + return response.data +} +``` + +### 4. 删除对话 + +**权限要求**: `dify:conversation:delete` + +```typescript +export async function deleteConversation(conversationId: string): Promise { + await request.delete(DIFY_API.CONVERSATION_DELETE(conversationId)) +} +``` + +**使用示例**: + +```vue + + + +``` + +### 5. 消息反馈(点赞/点踩) + +**权限要求**: `dify:message:feedback` + +```typescript +interface FeedbackRequest { + rating: 'like' | 'dislike' | null // null 表示取消反馈 + content?: string // 反馈内容(可选) +} + +export async function submitMessageFeedback( + messageId: string, + feedback: FeedbackRequest +): Promise { + await request.post(DIFY_API.MESSAGE_FEEDBACK(messageId), feedback) +} +``` + +### 6. 上传文件 + +**权限要求**: `dify:file:upload` + +```typescript +interface FileUploadResponse { + id: string + name: string + size: number + extension: string + mime_type: string + created_at: number +} + +export async function uploadFile(file: File): Promise { + const formData = new FormData() + formData.append('file', file) + + const response = await request.post(DIFY_API.FILE_UPLOAD, formData, { + headers: { + 'Content-Type': 'multipart/form-data' + } + }) + return response.data +} +``` + +--- + +## 知识库管理模块 + +### 1. 获取当前用户可访问的知识库列表 + +**权限要求**: `dify:dataset:read` + +**接口说明**: 根据用户角色自动返回对应数据范围 +- `common` / `admin`: 本地区 + 公共知识库 +- `provincial_admin`: 全部知识库 + +```typescript +interface AreaDataset { + id: number + area: string // 地区:梅州、云浮、揭阳、潮州、省级 + dataset_id: string // Dify 知识库 ID + dataset_name: string // 知识库名称 + dataset_description?: string // 知识库描述 + is_default: boolean // 是否为该地区默认知识库 + is_public: boolean // 是否公开(省级公共知识库) + sort_order: number // 排序顺序 + status: number // 状态:1=启用, 0=禁用 + created_at: string + updated_at: string +} + +interface MyDatasetsResponse { + data: AreaDataset[] + total: number + user_area: string // 当前用户所属地区 + user_role: string // 当前用户角色 +} + +export async function getMyDatasets(): Promise { + const response = await request.get(DIFY_API.AREA_DATASETS_MY) + return response.data +} +``` + +**使用示例**: + +```vue + + + +``` + +### 2. 获取所有知识库绑定列表(管理员) + +**权限要求**: `dify:dataset:manage` (仅省级管理员) + +```typescript +interface DatasetListResponse { + data: AreaDataset[] + total: number + page: number + page_size: number + has_more: boolean +} + +interface DatasetListParams { + area?: string // 筛选地区 + only_enabled?: boolean // 是否只返回启用的,默认 true + page?: number // 页码,默认 1 + page_size?: number // 每页数量,默认 20 +} + +export async function getAllDatasets( + params: DatasetListParams = {} +): Promise { + const response = await request.get(DIFY_API.AREA_DATASETS, { params }) + return response.data +} +``` + +### 3. 获取可用地区列表 + +**权限要求**: `dify:dataset:manage` + +```typescript +export async function getAvailableAreas(): Promise { + const response = await request.get(DIFY_API.AREA_DATASETS_AREAS) + return response.data.data +} +``` + +### 4. 创建知识库绑定 + +**权限要求**: `dify:dataset:manage` (仅省级管理员) + +```typescript +interface CreateDatasetRequest { + area: string // 地区名称 + dataset_id: string // Dify 知识库 ID + dataset_name: string // 知识库名称 + dataset_description?: string // 描述 + is_default?: boolean // 是否默认,默认 false + is_public?: boolean // 是否公开,默认 false + sort_order?: number // 排序,默认 0 +} + +export async function createDatasetBinding( + data: CreateDatasetRequest +): Promise { + const response = await request.post(DIFY_API.AREA_DATASETS, data) + return response.data.data +} +``` + +### 5. 更新知识库绑定 + +**权限要求**: `dify:dataset:manage` (仅省级管理员) + +```typescript +interface UpdateDatasetRequest { + dataset_name?: string + dataset_description?: string + is_default?: boolean + is_public?: boolean + sort_order?: number + status?: number // 1=启用, 0=禁用 +} + +export async function updateDatasetBinding( + id: number, + data: UpdateDatasetRequest +): Promise { + const response = await request.put(DIFY_API.AREA_DATASETS_DETAIL(id), data) + return response.data.data +} +``` + +### 6. 删除知识库绑定 + +**权限要求**: `dify:dataset:manage` (仅省级管理员) + +```typescript +export async function deleteDatasetBinding(id: number): Promise { + await request.delete(DIFY_API.AREA_DATASETS_DETAIL(id)) +} +``` + +### 7. 检查知识库访问权限 + +**权限要求**: `dify:dataset:read` + +```typescript +interface CheckAccessResponse { + has_access: boolean + user_area: string + dataset_id: string +} + +export async function checkDatasetAccess( + datasetId: string +): Promise { + const response = await request.get(DIFY_API.AREA_DATASETS_CHECK(datasetId)) + return response.data +} +``` + +**使用示例**: + +```typescript +// 在访问 Dify 知识库详情之前检查权限 +async function viewDatasetDetail(datasetId: string) { + try { + const { has_access } = await checkDatasetAccess(datasetId) + + if (!has_access) { + ElMessage.warning('您没有访问该知识库的权限') + return + } + + // 继续访问知识库详情... + router.push(`/dataset/${datasetId}`) + } catch (error) { + ElMessage.error('权限检查失败') + } +} +``` + +--- + +## 权限控制实现 + +### 1. 获取用户权限 + +登录后调用 `/user/routes` 获取用户权限: + +```typescript +// api/auth.ts +interface UserRoutesResponse { + user_id: number + username: string + routes: RouteInfo[] + routes_flat: RouteInfo[] +} + +export async function getUserRoutes(): Promise { + const response = await request.get('/user/routes') + return response.data +} + +// 登录后初始化权限 +async function initPermissions() { + const data = await getUserRoutes() + const permissionStore = usePermissionStore() + permissionStore.setRoutes(data.routes, data.routes_flat) +} +``` + +### 2. 路由守卫 + +```typescript +// router/guards.ts +import { usePermissionStore } from '@/stores/permission' + +router.beforeEach(async (to, from, next) => { + const permissionStore = usePermissionStore() + + // 检查是否有该路由的访问权限 + const hasAccess = permissionStore.routesFlat.some( + route => route.route_path === to.path + ) + + if (!hasAccess && to.path !== '/403') { + next('/403') + return + } + + next() +}) +``` + +### 3. 按钮级权限控制 + +```typescript +// directives/permission.ts +import { usePermissionStore } from '@/stores/permission' + +export const vPermission = { + mounted(el: HTMLElement, binding: { value: string }) { + const permissionStore = usePermissionStore() + const permissionKey = binding.value + + if (!permissionStore.hasPermission(permissionKey)) { + el.parentNode?.removeChild(el) + } + } +} + +// main.ts +app.directive('permission', vPermission) +``` + +**使用示例**: + +```vue + +``` + +### 4. 组合式权限 Hook + +```typescript +// composables/usePermission.ts +import { computed } from 'vue' +import { usePermissionStore } from '@/stores/permission' + +export function usePermission() { + const store = usePermissionStore() + + // Dify 对话权限 + const canChat = computed(() => store.hasPermission('dify:chat:use')) + const canViewHistory = computed(() => store.hasPermission('dify:conversation:read')) + const canDeleteConversation = computed(() => store.hasPermission('dify:conversation:delete')) + const canFeedback = computed(() => store.hasPermission('dify:message:feedback')) + + // Dify 知识库权限 + const canViewDataset = computed(() => store.hasPermission('dify:dataset:read')) + const canEditDataset = computed(() => store.hasPermission('dify:dataset:write')) + const canManageDataset = computed(() => store.hasPermission('dify:dataset:manage')) + + // Dify 文件权限 + const canDownloadFile = computed(() => store.hasPermission('dify:file:read')) + const canUploadFile = computed(() => store.hasPermission('dify:file:upload')) + + return { + // 检查任意权限 + hasPermission: (key: string) => store.hasPermission(key), + + // 对话权限 + canChat, + canViewHistory, + canDeleteConversation, + canFeedback, + + // 知识库权限 + canViewDataset, + canEditDataset, + canManageDataset, + + // 文件权限 + canDownloadFile, + canUploadFile + } +} +``` + +**使用示例**: + +```vue + + + +``` + +--- + +## 错误处理 + +### HTTP 状态码 + +| 状态码 | 说明 | 处理方式 | +|--------|------|---------| +| 200 | 成功 | 正常处理 | +| 400 | 请求参数错误 | 显示错误信息 | +| 401 | 未认证 | 跳转登录页 | +| 403 | 权限不足 | 显示权限提示 | +| 404 | 资源不存在 | 显示不存在提示 | +| 500 | 服务器错误 | 显示系统错误提示 | + +### 统一错误处理 + +```typescript +// utils/request.ts +import axios from 'axios' +import { ElMessage } from 'element-plus' +import router from '@/router' + +const request = axios.create({ + baseURL: import.meta.env.VITE_API_BASE, + timeout: 30000 +}) + +// 请求拦截器 +request.interceptors.request.use(config => { + const token = localStorage.getItem('token') + if (token) { + config.headers.Authorization = `Bearer ${token}` + } + return config +}) + +// 响应拦截器 +request.interceptors.response.use( + response => response, + error => { + const { response } = error + + if (response) { + switch (response.status) { + case 401: + ElMessage.error('登录已过期,请重新登录') + localStorage.removeItem('token') + router.push('/login') + break + + case 403: + // 权限不足 + const errorData = response.data + ElMessage.error(errorData?.detail || '权限不足,无法执行此操作') + break + + case 404: + ElMessage.error('请求的资源不存在') + break + + case 500: + ElMessage.error('服务器错误,请稍后重试') + break + + default: + ElMessage.error(response.data?.message || '请求失败') + } + } else { + ElMessage.error('网络错误,请检查网络连接') + } + + return Promise.reject(error) + } +) + +export default request +``` + +### 权限错误特殊处理 + +```typescript +// 403 错误的详细信息格式 +interface PermissionError { + error: string // 如 "权限不足: dify:dataset:manage" + detail: string // 如 "您没有执行此操作的权限,请联系管理员" +} + +// 解析权限错误 +function parsePermissionError(error: any): string { + if (error.response?.status === 403) { + const data = error.response.data as PermissionError + + // 提取权限标识 + const match = data.error?.match(/权限不足: (.+)/) + if (match) { + const permissionKey = match[1] + const permissionLabels: Record = { + 'dify:chat:use': '使用AI对话', + 'dify:conversation:read': '查看对话历史', + 'dify:conversation:delete': '删除对话', + 'dify:dataset:read': '查看知识库', + 'dify:dataset:write': '编辑知识库', + 'dify:dataset:manage': '管理知识库绑定', + 'dify:file:read': '下载文件', + 'dify:file:upload': '上传文件' + } + return `您没有"${permissionLabels[permissionKey] || permissionKey}"的权限` + } + + return data.detail || '权限不足' + } + + return '操作失败' +} +``` + +--- + +## 完整代码示例 + +### 知识库管理页面完整实现 + +```vue + + + + + + +``` + +--- + +## 更新日志 + +| 版本 | 日期 | 说明 | +|------|------|------| +| 1.0 | 2025-12-06 | 初始版本,包含完整的前端对接说明 |