feat: 添加对话应用选择和知识库切换功能
- 新增对话应用管理模块(dify-chat-apps),支持获取和切换对话应用 - 优化对话应用切换后自动刷新会话列表功能 - 知识库管理页面新增下拉选择器,支持切换不同知识库 - API 层支持 app_id 参数传递,实现多应用会话隔离 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,138 @@
|
||||
/**
|
||||
* 对话应用管理钩子
|
||||
*
|
||||
* 提供对话应用的加载、切换和状态管理功能
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useEffect } from 'react';
|
||||
import { getMyChatApps, getDefaultChatApp } from '~/api/dify-chat-apps/chatAppsApi';
|
||||
import type { ChatApp } from '~/api/dify-chat-apps/types';
|
||||
|
||||
export function useChatApps() {
|
||||
// 对话应用列表
|
||||
const [chatApps, setChatApps] = useState<ChatApp[]>([]);
|
||||
// 加载状态
|
||||
const [loadingChatApps, setLoadingChatApps] = useState(true);
|
||||
// 加载默认应用状态
|
||||
const [loadingDefault, setLoadingDefault] = useState(true);
|
||||
// 当前选中的应用
|
||||
const [currentChatApp, setCurrentChatApp] = useState<ChatApp | null>(null);
|
||||
// 错误信息
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
// 初始化完成状态
|
||||
const [inited, setInited] = useState(false);
|
||||
|
||||
/**
|
||||
* 加载我的对话应用列表
|
||||
*/
|
||||
const loadChatApps = useCallback(async () => {
|
||||
setLoadingChatApps(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await getMyChatApps();
|
||||
setChatApps(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
console.error('[useChatApps] 加载对话应用列表失败:', err);
|
||||
setChatApps([]);
|
||||
return [];
|
||||
} finally {
|
||||
setLoadingChatApps(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 加载默认对话应用
|
||||
*/
|
||||
const loadDefaultChatApp = useCallback(async () => {
|
||||
setLoadingDefault(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const response = await getDefaultChatApp();
|
||||
// 如果加载所有应用失败,但成功加载了默认应用,将默认应用添加到chatApps数组中
|
||||
setChatApps(prev => [...prev, response.data]);
|
||||
setCurrentChatApp(response.data);
|
||||
return response.data;
|
||||
} catch (err: any) {
|
||||
console.error('[useChatApps] 加载默认对话应用失败:', err);
|
||||
setError(err.message || '加载默认对话应用失败');
|
||||
return null;
|
||||
} finally {
|
||||
setLoadingDefault(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 切换对话应用
|
||||
* @param appId 应用ID
|
||||
* @param onAppChanged 切换完成后的回调函数
|
||||
*/
|
||||
const handleChatAppChange = useCallback((appId: string, onAppChanged?: (app: ChatApp) => void) => {
|
||||
const app = chatApps.find(chatApp => chatApp.app_id === appId);
|
||||
if (app) {
|
||||
console.log('[useChatApps] 切换对话应用:', app.app_name, app.app_id);
|
||||
setCurrentChatApp(app);
|
||||
// 切换应用后,调用回调函数
|
||||
if (onAppChanged) {
|
||||
onAppChanged(app);
|
||||
}
|
||||
}
|
||||
}, [chatApps]);
|
||||
|
||||
/**
|
||||
* 初始化对话应用
|
||||
*/
|
||||
const initializeChatApps = useCallback(async () => {
|
||||
setLoadingChatApps(true);
|
||||
setLoadingDefault(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
try {
|
||||
// 尝试加载可用应用列表
|
||||
const apps = await loadChatApps();
|
||||
|
||||
if (apps.length > 0) {
|
||||
// 查找默认应用
|
||||
const defaultApp = apps.find((item) => item.is_default) || apps[0];
|
||||
setCurrentChatApp(defaultApp);
|
||||
} else {
|
||||
// 如果没有配置应用,尝试获取默认应用
|
||||
await loadDefaultChatApp();
|
||||
}
|
||||
} catch (err) {
|
||||
// 加载应用列表失败,尝试获取默认应用
|
||||
console.warn('[useChatApps] 加载应用列表失败,尝试获取默认应用:', err);
|
||||
await loadDefaultChatApp();
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[useChatApps] 初始化失败:', err);
|
||||
setError(err.message || '加载对话应用失败');
|
||||
} finally {
|
||||
setLoadingChatApps(false);
|
||||
setLoadingDefault(false);
|
||||
setInited(true);
|
||||
}
|
||||
}, [loadChatApps, loadDefaultChatApp]);
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
initializeChatApps();
|
||||
}, [initializeChatApps]);
|
||||
|
||||
return {
|
||||
// 状态
|
||||
chatApps,
|
||||
loadingChatApps,
|
||||
currentChatApp,
|
||||
error,
|
||||
inited,
|
||||
|
||||
// 方法
|
||||
loadChatApps,
|
||||
loadDefaultChatApp,
|
||||
handleChatAppChange,
|
||||
};
|
||||
}
|
||||
@@ -4,6 +4,7 @@ import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
|
||||
import type { Document } from '~/api/dify-dataset/type/documentTypes';
|
||||
import { fetchDatasets, fetchDataset } from '~/api/dify-dataset/api/datasetApi';
|
||||
import { fetchDocuments } from '~/api/dify-dataset/api/documentApi';
|
||||
import { getMyDatasets, type AreaDataset } from '~/api/v3/dify/area-datasets';
|
||||
import type { MenuTab } from '~/types/dify-dataset-manager/layout';
|
||||
import { DEFAULT_DOCUMENT_PAGE_SIZE } from '~/types/dify-dataset-manager/index';
|
||||
|
||||
@@ -15,6 +16,10 @@ export function useDatasetManager() {
|
||||
const [dataset, setDataset] = useState<Dataset | null>(null);
|
||||
const [loadingDataset, setLoadingDataset] = useState(true);
|
||||
|
||||
// 用户可访问的知识库列表(基于权限)
|
||||
const [availableDatasets, setAvailableDatasets] = useState<AreaDataset[]>([]);
|
||||
const [loadingAvailableDatasets, setLoadingAvailableDatasets] = useState(true);
|
||||
|
||||
// 文档状态
|
||||
const [documents, setDocuments] = useState<Document[]>([]);
|
||||
const [loadingDocuments, setLoadingDocuments] = useState(false);
|
||||
@@ -57,6 +62,56 @@ export function useDatasetManager() {
|
||||
}
|
||||
}, [documentPageSize]);
|
||||
|
||||
/**
|
||||
* 加载用户可访问的知识库列表(基于权限)
|
||||
*/
|
||||
const loadAvailableDatasets = useCallback(async () => {
|
||||
setLoadingAvailableDatasets(true);
|
||||
try {
|
||||
console.log('[DatasetManager] 加载用户可访问的知识库列表...');
|
||||
const response = await getMyDatasets();
|
||||
console.log('[DatasetManager] 用户知识库列表响应:', response);
|
||||
|
||||
if (response && response.code === 0 && response.data) {
|
||||
const dataList = Array.isArray(response.data.data) ? response.data.data : [];
|
||||
setAvailableDatasets(dataList);
|
||||
return dataList;
|
||||
} else {
|
||||
console.error('[DatasetManager] 获取用户知识库列表失败:', response);
|
||||
setAvailableDatasets([]);
|
||||
return [];
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[DatasetManager] 加载用户知识库列表失败:', err);
|
||||
setAvailableDatasets([]);
|
||||
return [];
|
||||
} finally {
|
||||
setLoadingAvailableDatasets(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 根据 dataset_id 加载知识库详情
|
||||
*/
|
||||
const loadDatasetById = useCallback(async (datasetId: string) => {
|
||||
setLoadingDataset(true);
|
||||
try {
|
||||
console.log('[DatasetManager] 加载知识库详情:', datasetId);
|
||||
const fullDataset = await fetchDataset(datasetId);
|
||||
console.log('[DatasetManager] 知识库详情响应:', fullDataset);
|
||||
|
||||
setDataset(fullDataset);
|
||||
// 立即加载文档
|
||||
await loadDocuments(datasetId, 1);
|
||||
} catch (err: any) {
|
||||
console.error('[DatasetManager] 加载知识库详情失败:', err);
|
||||
setError(err.message || '加载知识库失败');
|
||||
message.error('加载知识库失败');
|
||||
} finally {
|
||||
setLoadingDataset(false);
|
||||
}
|
||||
}, [loadDocuments]);
|
||||
|
||||
/**
|
||||
* 加载知识库(获取第一个知识库,再获取详情以包含 retrieval_model)
|
||||
*/
|
||||
@@ -64,22 +119,40 @@ export function useDatasetManager() {
|
||||
setLoadingDataset(true);
|
||||
try {
|
||||
console.log('[DatasetManager] 加载知识库...');
|
||||
// 先获取列表,找到第一个知识库的 ID
|
||||
const response = await fetchDatasets(1, 1);
|
||||
console.log('[DatasetManager] 知识库列表响应:', response);
|
||||
|
||||
if (response && response.data && response.data.length > 0) {
|
||||
const firstDatasetId = response.data[0].id;
|
||||
// 先加载用户可访问的知识库列表
|
||||
const userDatasets = await loadAvailableDatasets();
|
||||
|
||||
// 再获取详情,包含完整的 retrieval_model 等字段
|
||||
const fullDataset = await fetchDataset(firstDatasetId);
|
||||
if (userDatasets.length > 0) {
|
||||
// 找到默认知识库或第一个知识库
|
||||
const defaultDataset = userDatasets.find(ds => ds.is_default) || userDatasets[0];
|
||||
const datasetId = defaultDataset.dataset_id;
|
||||
|
||||
console.log('[DatasetManager] 使用知识库:', defaultDataset.dataset_name, datasetId);
|
||||
|
||||
// 获取知识库详情
|
||||
const fullDataset = await fetchDataset(datasetId);
|
||||
console.log('[DatasetManager] 知识库详情响应:', fullDataset);
|
||||
|
||||
setDataset(fullDataset);
|
||||
// 立即加载文档
|
||||
await loadDocuments(firstDatasetId, 1);
|
||||
await loadDocuments(datasetId, 1);
|
||||
} else {
|
||||
setError('未找到知识库,请先在Dify中创建知识库');
|
||||
// 回退到原有逻辑:直接从 Dify 获取
|
||||
console.log('[DatasetManager] 用户无绑定知识库,使用默认逻辑...');
|
||||
const response = await fetchDatasets(1, 1);
|
||||
console.log('[DatasetManager] Dify知识库列表响应:', response);
|
||||
|
||||
if (response && response.data && response.data.length > 0) {
|
||||
const firstDatasetId = response.data[0].id;
|
||||
const fullDataset = await fetchDataset(firstDatasetId);
|
||||
console.log('[DatasetManager] 知识库详情响应:', fullDataset);
|
||||
|
||||
setDataset(fullDataset);
|
||||
await loadDocuments(firstDatasetId, 1);
|
||||
} else {
|
||||
setError('未找到知识库,请先在Dify中创建知识库');
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[DatasetManager] 加载知识库失败:', err);
|
||||
@@ -89,7 +162,19 @@ export function useDatasetManager() {
|
||||
setLoadingDataset(false);
|
||||
setInited(true);
|
||||
}
|
||||
}, [loadDocuments]);
|
||||
}, [loadDocuments, loadAvailableDatasets]);
|
||||
|
||||
/**
|
||||
* 切换知识库
|
||||
*/
|
||||
const handleDatasetChange = useCallback(async (datasetId: string) => {
|
||||
console.log('[DatasetManager] 切换知识库:', datasetId);
|
||||
// 重置状态
|
||||
setSelectedDocument(null);
|
||||
setActiveTab('documents');
|
||||
// 加载新知识库
|
||||
await loadDatasetById(datasetId);
|
||||
}, [loadDatasetById]);
|
||||
|
||||
/**
|
||||
* 处理文档页码变化
|
||||
@@ -190,7 +275,11 @@ export function useDatasetManager() {
|
||||
error,
|
||||
activeTab,
|
||||
selectedDocument,
|
||||
|
||||
|
||||
// 知识库列表(基于权限)
|
||||
availableDatasets,
|
||||
loadingAvailableDatasets,
|
||||
|
||||
// 方法
|
||||
loadDataset,
|
||||
loadDocuments,
|
||||
@@ -202,6 +291,7 @@ export function useDatasetManager() {
|
||||
handleBackToDocuments,
|
||||
handleTabChange,
|
||||
handleDatasetUpdated,
|
||||
handleDatasetChange,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 提供地区-知识库绑定管理功能
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react';
|
||||
import { useState, useEffect, useCallback, useMemo } from 'react';
|
||||
import {
|
||||
getMyDatasets,
|
||||
getAllDatasets,
|
||||
@@ -31,9 +31,9 @@ export interface UseAreaDatasetConfigReturn {
|
||||
areas: string[];
|
||||
areasLoading: boolean;
|
||||
|
||||
// 筛选
|
||||
filterArea: string;
|
||||
setFilterArea: (area: string) => void;
|
||||
// 筛选 - 支持多选
|
||||
filterAreas: string[];
|
||||
setFilterAreas: (areas: string[]) => void;
|
||||
page: number;
|
||||
setPage: (page: number) => void;
|
||||
pageSize: number;
|
||||
@@ -56,23 +56,36 @@ export interface UseAreaDatasetConfigReturn {
|
||||
canManageDataset: boolean;
|
||||
}
|
||||
|
||||
// 角色名称映射
|
||||
const ROLE_LABELS: Record<string, string> = {
|
||||
common: '普通用户',
|
||||
admin: '市级管理员',
|
||||
provincial_admin: '省级管理员',
|
||||
};
|
||||
|
||||
// ==================== Hook Implementation ====================
|
||||
|
||||
export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
// 权限控制
|
||||
const { canManageDataset, canViewDataset, userRole } = usePermission();
|
||||
const { userRole: permissionUserRole } = usePermission();
|
||||
|
||||
// 根据 userRole 判断权限
|
||||
// provincial_admin 可以管理所有知识库配置
|
||||
// 其他角色只能查看自己地区的配置
|
||||
const canManageDataset = permissionUserRole === 'provincial_admin' || permissionUserRole.toLowerCase().includes('provin');
|
||||
const canViewDataset = true; // 所有登录用户都可以查看
|
||||
|
||||
// 数据状态
|
||||
const [datasets, setDatasets] = useState<AreaDataset[]>([]);
|
||||
const [allDatasets, setAllDatasets] = 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 [apiAreas, setApiAreas] = useState<string[]>([]); // API 返回的地区列表
|
||||
const [areasLoading, setAreasLoading] = useState<boolean>(false);
|
||||
|
||||
// 筛选状态
|
||||
const [filterArea, setFilterArea] = useState<string>('');
|
||||
// 筛选状态 - 支持多选
|
||||
const [filterAreas, setFilterAreas] = useState<string[]>([]);
|
||||
const [page, setPage] = useState<number>(1);
|
||||
const pageSize = 20;
|
||||
|
||||
@@ -81,6 +94,26 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
const [editingId, setEditingId] = useState<number | null>(null);
|
||||
const [submitLoading, setSubmitLoading] = useState<boolean>(false);
|
||||
|
||||
// 从数据集中提取地区列表
|
||||
const extractedAreas = useMemo(() => {
|
||||
const areaSet = new Set<string>();
|
||||
allDatasets.forEach(ds => {
|
||||
if (ds.area) {
|
||||
areaSet.add(ds.area);
|
||||
}
|
||||
});
|
||||
return Array.from(areaSet).sort();
|
||||
}, [allDatasets]);
|
||||
|
||||
// 合并 API 返回的地区和从数据中提取的地区
|
||||
const areas = useMemo(() => {
|
||||
const areaSet = new Set<string>([...apiAreas, ...extractedAreas]);
|
||||
return Array.from(areaSet).sort();
|
||||
}, [apiAreas, extractedAreas]);
|
||||
|
||||
// 获取角色显示名称
|
||||
const userRoleLabel = ROLE_LABELS[permissionUserRole] || permissionUserRole || '未知角色';
|
||||
|
||||
// ==================== Data Loading ====================
|
||||
|
||||
/**
|
||||
@@ -98,9 +131,11 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
|
||||
if (canManageDataset) {
|
||||
// 省级管理员:获取所有知识库
|
||||
// 如果有多个地区筛选,用逗号分隔传递
|
||||
const areaFilter = filterAreas.length > 0 ? filterAreas.join(',') : undefined;
|
||||
response = await getAllDatasets({
|
||||
area: filterArea || undefined,
|
||||
only_enabled: true,
|
||||
area: areaFilter,
|
||||
only_enabled: false, // 管理员可以看到所有状态
|
||||
page,
|
||||
page_size: pageSize,
|
||||
});
|
||||
@@ -109,17 +144,25 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
response = await getMyDatasets();
|
||||
}
|
||||
|
||||
if (response.code === 0) {
|
||||
setDatasets(response.data.data);
|
||||
setTotal(response.data.total);
|
||||
console.log('[AreaDatasetConfig] API响应:', response);
|
||||
|
||||
if (response && response.code === 0 && response.data) {
|
||||
const dataList = Array.isArray(response.data.data) ? response.data.data : [];
|
||||
setDatasets(dataList);
|
||||
setTotal(response.data.total || dataList.length);
|
||||
|
||||
// 如果没有筛选,保存所有数据用于提取地区
|
||||
if (filterAreas.length === 0) {
|
||||
setAllDatasets(dataList);
|
||||
}
|
||||
|
||||
// 如果是 my 接口,保存用户信息
|
||||
if ('user_area' in response.data) {
|
||||
setUserArea((response.data as any).user_area);
|
||||
setUserRoleState((response.data as any).user_role);
|
||||
setUserArea((response.data as any).user_area || '');
|
||||
}
|
||||
} else {
|
||||
message.error(`加载失败: ${response.message}`);
|
||||
console.error('[AreaDatasetConfig] API响应格式错误:', response);
|
||||
message.error(`加载失败: ${response?.message || '未知错误'}`);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载知识库失败:', error);
|
||||
@@ -127,7 +170,7 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, [canManageDataset, canViewDataset, filterArea, page, pageSize]);
|
||||
}, [canManageDataset, canViewDataset, filterAreas, page, pageSize]);
|
||||
|
||||
/**
|
||||
* 加载地区列表(仅省级管理员)
|
||||
@@ -138,10 +181,16 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
setAreasLoading(true);
|
||||
try {
|
||||
const areasList = await getAvailableAreas();
|
||||
setAreas(areasList);
|
||||
console.log('[AreaDatasetConfig] 地区列表响应:', areasList);
|
||||
if (Array.isArray(areasList)) {
|
||||
setApiAreas(areasList);
|
||||
} else {
|
||||
console.warn('[AreaDatasetConfig] 地区列表不是数组:', areasList);
|
||||
setApiAreas([]);
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error('加载地区列表失败:', error);
|
||||
message.error('加载地区列表失败');
|
||||
// 不显示错误提示,因为可以从数据中提取地区
|
||||
} finally {
|
||||
setAreasLoading(false);
|
||||
}
|
||||
@@ -247,29 +296,20 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
|
||||
// ==================== Effects ====================
|
||||
|
||||
// 加载数据
|
||||
// 初始加载数据
|
||||
useEffect(() => {
|
||||
loadDatasets();
|
||||
}, [loadDatasets]);
|
||||
}, []); // 只在挂载时加载一次
|
||||
|
||||
// 加载地区列表
|
||||
useEffect(() => {
|
||||
loadAreas();
|
||||
}, [loadAreas]);
|
||||
|
||||
// 监听筛选条件变化
|
||||
// 监听筛选条件和页码变化
|
||||
useEffect(() => {
|
||||
if (canManageDataset) {
|
||||
setPage(1); // 筛选条件变化时重置到第一页
|
||||
loadDatasets();
|
||||
}
|
||||
}, [filterArea, canManageDataset, loadDatasets]);
|
||||
|
||||
// 监听页码变化
|
||||
useEffect(() => {
|
||||
if (canManageDataset) {
|
||||
loadDatasets();
|
||||
}
|
||||
}, [page, canManageDataset, loadDatasets]);
|
||||
loadDatasets();
|
||||
}, [filterAreas, page]);
|
||||
|
||||
return {
|
||||
// 数据
|
||||
@@ -277,13 +317,13 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
|
||||
loading,
|
||||
total,
|
||||
userArea,
|
||||
userRole: userRoleState,
|
||||
userRole: userRoleLabel,
|
||||
areas,
|
||||
areasLoading,
|
||||
|
||||
// 筛选
|
||||
filterArea,
|
||||
setFilterArea,
|
||||
filterAreas,
|
||||
setFilterAreas,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
|
||||
@@ -143,12 +143,18 @@ export default function useChatMessage({
|
||||
|
||||
/**
|
||||
* 发送消息
|
||||
* @param message - 消息内容
|
||||
* @param conversationId - 会话 ID
|
||||
* @param files - 附件文件
|
||||
* @param inputs - 输入参数
|
||||
* @param appId - 对话应用 ID(可选,用于切换不同的 Dify 应用)
|
||||
*/
|
||||
const handleSend = useCallback(async (
|
||||
message: string,
|
||||
conversationId: string | null,
|
||||
files?: VisionFile[],
|
||||
inputs?: Record<string, any>,
|
||||
appId?: string,
|
||||
) => {
|
||||
if (!checkCanSend() || !message.trim()) {
|
||||
return;
|
||||
@@ -186,6 +192,7 @@ export default function useChatMessage({
|
||||
inputs: toServerInputs,
|
||||
query: message,
|
||||
conversation_id: conversationId === '-1' ? null : conversationId,
|
||||
app_id: appId, // 添加对话应用 ID
|
||||
};
|
||||
|
||||
// 添加文件数据
|
||||
|
||||
Reference in New Issue
Block a user