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,127 @@
|
||||
/**
|
||||
* Dify 对话应用管理 API 模块
|
||||
*
|
||||
* 提供浏览器端调用对话应用管理 API 的函数
|
||||
* 注意:这些 API 调用的是前端 Remix 路由(/api/...),不需要后端 baseURL
|
||||
*
|
||||
* @module api/dify-chat-apps/chatAppsApi
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChatApp,
|
||||
MyChatAppsResponse,
|
||||
DefaultChatAppResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* API 基础 URL(前端 Remix 路由)
|
||||
*/
|
||||
const API_URL = '/api/v3/dify/chat-apps';
|
||||
|
||||
/**
|
||||
* HTTP 状态码对应的友好错误信息
|
||||
*/
|
||||
const HTTP_ERROR_MESSAGES: Record<number, string> = {
|
||||
400: '请求参数错误',
|
||||
401: '登录已过期,请重新登录',
|
||||
403: '您没有权限执行此操作',
|
||||
404: '请求的资源不存在',
|
||||
409: '数据冲突,该记录可能已存在',
|
||||
500: '服务器内部错误,请稍后重试',
|
||||
502: '网关错误,请稍后重试',
|
||||
503: '服务暂时不可用,请稍后重试',
|
||||
};
|
||||
|
||||
/**
|
||||
* 封装 fetch 请求,自动处理 credentials
|
||||
*/
|
||||
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
credentials: 'include', // 包含 cookies
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
// 优先使用后端返回的错误信息,否则使用友好的默认信息
|
||||
const friendlyMessage = errorData.message
|
||||
|| errorData.error
|
||||
|| HTTP_ERROR_MESSAGES[response.status]
|
||||
|| '操作失败,请稍后重试';
|
||||
const error = new Error(friendlyMessage);
|
||||
(error as any).response = { status: response.status, data: errorData };
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前实例配置的对话应用列表
|
||||
*
|
||||
* 根据用户角色自动返回对应数据范围:
|
||||
* - provincial_admin: 全部可用应用
|
||||
* - admin/common: 本地区可用应用
|
||||
*
|
||||
* @returns 用户可访问的对话应用列表
|
||||
*/
|
||||
export async function getMyChatApps(): Promise<MyChatAppsResponse> {
|
||||
const response = await request<any>(`${API_URL}/my`);
|
||||
|
||||
// 兼容嵌套格式 { data: { data: [], total: ... } }
|
||||
if (response?.data?.data) {
|
||||
return {
|
||||
data: response.data.data,
|
||||
total: response.data.total || 0,
|
||||
page: response.data.page || 1,
|
||||
page_size: response.data.page_size || 10,
|
||||
};
|
||||
}
|
||||
// 格式 { data: [], total: ... }
|
||||
if (response?.data && Array.isArray(response.data)) {
|
||||
return response as MyChatAppsResponse;
|
||||
}
|
||||
// 直接返回
|
||||
if (Array.isArray(response?.data)) {
|
||||
return {
|
||||
data: response.data,
|
||||
total: response.data.length,
|
||||
page: 1,
|
||||
page_size: response.data.length,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn('[API] getMyChatApps: 无效的响应格式', response);
|
||||
return { data: [], total: 0, page: 1, page_size: 10 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认对话应用
|
||||
*
|
||||
* 返回配置文件中的第一个应用作为默认应用
|
||||
*
|
||||
* @returns 默认对话应用
|
||||
*/
|
||||
export async function getDefaultChatApp(): Promise<DefaultChatAppResponse> {
|
||||
const response = await request<any>(`${API_URL}/default`);
|
||||
|
||||
// 兼容嵌套格式 { data: { data: {...} } }
|
||||
if (response?.data?.data) {
|
||||
return {
|
||||
data: response.data.data,
|
||||
};
|
||||
}
|
||||
// 格式 { data: {...} }
|
||||
if (response?.data) {
|
||||
return {
|
||||
data: response.data,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn('[API] getDefaultChatApp: 无效的响应格式', response);
|
||||
throw new Error('获取默认对话应用失败');
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 对话应用类型定义
|
||||
*/
|
||||
export interface ChatApp {
|
||||
/** 应用ID */
|
||||
app_id: string;
|
||||
/** 应用名称 */
|
||||
app_name: string;
|
||||
/** 应用描述 */
|
||||
description: string;
|
||||
/** 是否默认应用 */
|
||||
is_default: boolean;
|
||||
/** 应用类型 */
|
||||
type: string;
|
||||
/** 创建时间 */
|
||||
created_at: string;
|
||||
/** 更新时间 */
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的对话应用列表响应
|
||||
*/
|
||||
export interface MyChatAppsResponse {
|
||||
/** 应用列表 */
|
||||
data: ChatApp[];
|
||||
/** 总数 */
|
||||
total: number;
|
||||
/** 分页页码 */
|
||||
page: number;
|
||||
/** 每页数量 */
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认对话应用响应
|
||||
*/
|
||||
export interface DefaultChatAppResponse {
|
||||
/** 默认应用 */
|
||||
data: ChatApp;
|
||||
}
|
||||
@@ -32,8 +32,11 @@ export const difyClient = {
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*
|
||||
* @param jwt - JWT 认证令牌
|
||||
* @param appId - 对话应用 ID(可选,用于获取特定应用的会话列表)
|
||||
*/
|
||||
async getConversations(jwt?: string): Promise<any> {
|
||||
async getConversations(jwt?: string, appId?: string): Promise<any> {
|
||||
const params = new URLSearchParams({
|
||||
limit: '100',
|
||||
first_id: '',
|
||||
@@ -41,6 +44,7 @@ export const difyClient = {
|
||||
|
||||
const response = await difyFetch(`conversations?${params}`, {
|
||||
method: 'GET',
|
||||
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
@@ -70,6 +74,7 @@ export const difyClient = {
|
||||
* @param conversationId - 会话 ID
|
||||
* @param files - 附件文件
|
||||
* @param jwt - JWT 认证令牌
|
||||
* @param appId - 对话应用 ID(可选,用于切换不同的 Dify 应用)
|
||||
* @returns 对于流式响应返回 Response 对象,否则返回 JSON
|
||||
*/
|
||||
async createChatMessage(
|
||||
@@ -78,7 +83,8 @@ export const difyClient = {
|
||||
responseMode: string = 'streaming',
|
||||
conversationId?: string,
|
||||
files?: any[],
|
||||
jwt?: string
|
||||
jwt?: string,
|
||||
appId?: string
|
||||
): Promise<Response | any> {
|
||||
const body = {
|
||||
inputs,
|
||||
@@ -90,6 +96,7 @@ export const difyClient = {
|
||||
const response = await difyFetch('chat-messages', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
|
||||
}, jwt);
|
||||
|
||||
// 对于流式响应,直接返回 Response 对象
|
||||
|
||||
@@ -27,6 +27,14 @@ const DIFY_CHAT_API_URL = `${API_BASE_URL}/dify_chat`;
|
||||
// 基础请求函数
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Dify Fetch 请求选项
|
||||
*/
|
||||
export interface DifyFetchOptions extends RequestInit {
|
||||
/** 对话应用 ID,用于切换不同的 Dify 应用 */
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dify Chat API 基础请求函数
|
||||
*
|
||||
@@ -34,20 +42,21 @@ const DIFY_CHAT_API_URL = `${API_BASE_URL}/dify_chat`;
|
||||
* FastAPI 后端会验证 JWT 并添加 Dify API_KEY
|
||||
*
|
||||
* @param endpoint - API 端点路径
|
||||
* @param options - fetch 请求选项
|
||||
* @param options - fetch 请求选项(可包含 appId)
|
||||
* @param jwt - 用户 JWT 认证令牌
|
||||
* @returns Response 对象
|
||||
*/
|
||||
export async function difyFetch(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
options: DifyFetchOptions = {},
|
||||
jwt?: string
|
||||
): Promise<Response> {
|
||||
const { appId, ...fetchOptions } = options;
|
||||
const url = `${DIFY_CHAT_API_URL}/${endpoint.replace(/^\//, '')}`;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
...fetchOptions.headers,
|
||||
};
|
||||
|
||||
if (jwt) {
|
||||
@@ -56,8 +65,14 @@ export async function difyFetch(
|
||||
console.warn('[Dify Chat] 没有提供 JWT,FastAPI 请求可能失败');
|
||||
}
|
||||
|
||||
// 如果指定了应用 ID,添加 X-Dify-App-Id 请求头
|
||||
if (appId) {
|
||||
(headers as Record<string, string>)['X-Dify-App-Id'] = appId;
|
||||
console.log('[Dify Chat] 使用应用 ID:', appId);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
...fetchOptions,
|
||||
headers,
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ const baseOptions: RequestInit = {
|
||||
/**
|
||||
* 获取用户的会话列表
|
||||
*
|
||||
* @param appId - 对话应用 ID(可选,用于获取特定应用的会话列表)
|
||||
* @returns 包含会话列表的响应对象
|
||||
* @throws {Error} 当获取会话列表失败时抛出错误
|
||||
*
|
||||
@@ -59,15 +60,23 @@ const baseOptions: RequestInit = {
|
||||
* const response = await fetchConversations();
|
||||
* const conversations = response.data;
|
||||
* console.log('会话数量:', conversations.length);
|
||||
*
|
||||
* // 获取指定应用的会话列表
|
||||
* const appConversations = await fetchConversations('app-123');
|
||||
* ```
|
||||
*/
|
||||
export async function fetchConversations(): Promise<ConversationsResponse> {
|
||||
export async function fetchConversations(appId?: string): Promise<ConversationsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
limit: '100',
|
||||
});
|
||||
|
||||
// 如果指定了 appId,添加到查询参数中
|
||||
if (appId) {
|
||||
params.append('app_id', appId);
|
||||
}
|
||||
|
||||
const url = `${API_URL}/conversations?${params}`;
|
||||
console.log('📋 [Dify Client] 获取会话列表:', { url });
|
||||
console.log('📋 [Dify Client] 获取会话列表:', { url, appId });
|
||||
|
||||
try {
|
||||
const response = await axios.get<ConversationsResponse>(url, {
|
||||
|
||||
@@ -521,6 +521,8 @@ export interface SendMessageParams {
|
||||
conversation_id?: string | null;
|
||||
files?: VisionFile[];
|
||||
response_mode?: 'streaming' | 'blocking';
|
||||
/** 对话应用 ID,用于切换不同的 Dify 应用 */
|
||||
app_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 提供地区-知识库绑定管理接口
|
||||
*/
|
||||
|
||||
import { request } from '~/api/axios-client';
|
||||
import { get, post, put, del } from '~/api/axios-client';
|
||||
|
||||
// ==================== Type Definitions ====================
|
||||
|
||||
@@ -87,8 +87,8 @@ 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;
|
||||
const response = await get<MyDatasetsResponse>(`${API_BASE}/my`);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,19 +101,15 @@ export async function getAllDatasets(params: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<AllDatasetsResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
const queryParams: Record<string, string | number | boolean | undefined> = {};
|
||||
|
||||
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));
|
||||
if (params.area) queryParams.area = params.area;
|
||||
if (params.only_enabled !== undefined) queryParams.only_enabled = params.only_enabled;
|
||||
if (params.page) queryParams.page = params.page;
|
||||
if (params.page_size) queryParams.page_size = params.page_size;
|
||||
|
||||
const response = await request.get(
|
||||
`${API_BASE}?${queryParams.toString()}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
const response = await get<AllDatasetsResponse>(API_BASE, queryParams);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,8 +117,8 @@ export async function getAllDatasets(params: {
|
||||
* 权限: dify:dataset:manage
|
||||
*/
|
||||
export async function getAvailableAreas(): Promise<string[]> {
|
||||
const response = await request.get(`${API_BASE}/areas`);
|
||||
return response.data.data;
|
||||
const response = await get<AreasResponse>(`${API_BASE}/areas`);
|
||||
return response.data?.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,8 +128,8 @@ export async function getAvailableAreas(): Promise<string[]> {
|
||||
export async function createDatasetBinding(
|
||||
data: CreateDatasetRequest
|
||||
): Promise<ApiResponse<{ data: AreaDataset }>> {
|
||||
const response = await request.post(`${API_BASE}`, data);
|
||||
return response.data;
|
||||
const response = await post<ApiResponse<{ data: AreaDataset }>>(API_BASE, data);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,8 +140,8 @@ export async function updateDatasetBinding(
|
||||
id: number,
|
||||
data: UpdateDatasetRequest
|
||||
): Promise<ApiResponse<{ data: AreaDataset }>> {
|
||||
const response = await request.put(`${API_BASE}/${id}`, data);
|
||||
return response.data;
|
||||
const response = await put<ApiResponse<{ data: AreaDataset }>>(`${API_BASE}/${id}`, data);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +149,6 @@ export async function updateDatasetBinding(
|
||||
* 权限: dify:dataset:manage
|
||||
*/
|
||||
export async function deleteDatasetBinding(id: number): Promise<ApiResponse<{ message: string }>> {
|
||||
const response = await request.delete(`${API_BASE}/${id}`);
|
||||
return response.data;
|
||||
const response = await del<ApiResponse<{ message: string }>>(`${API_BASE}/${id}`);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { fetchAppParams, fetchChatList, fetchConversations } from '~/api/dify-ch
|
||||
import { CHAT_CONFIG } from '../../config/chat';
|
||||
import useChatMessage from '../../hooks/use-chat-message';
|
||||
import useConversation from '../../hooks/use-conversation';
|
||||
import { useChatApps } from '../../hooks/dify-chat-apps/useChatApps';
|
||||
import '../../styles/components/chat-with-llm/index.css';
|
||||
|
||||
const { Content } = Layout;
|
||||
@@ -51,6 +52,14 @@ export default function Chat() {
|
||||
// 获取主题配置,避免SSR错误
|
||||
const { colorBgContainer, borderRadiusLG } = useChatTheme();
|
||||
|
||||
// 对话应用管理
|
||||
const {
|
||||
chatApps,
|
||||
loadingChatApps,
|
||||
currentChatApp,
|
||||
handleChatAppChange: originalHandleChatAppChange,
|
||||
} = useChatApps();
|
||||
|
||||
// 会话管理
|
||||
const {
|
||||
conversationList,
|
||||
@@ -335,7 +344,8 @@ export default function Chat() {
|
||||
message: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
|
||||
currConversationId,
|
||||
isNewConversation,
|
||||
willSendConversationId: isNewConversation ? null : currConversationId
|
||||
willSendConversationId: isNewConversation ? null : currConversationId,
|
||||
appId: currentChatApp?.app_id
|
||||
});
|
||||
|
||||
try {
|
||||
@@ -347,12 +357,13 @@ export default function Chat() {
|
||||
});
|
||||
}
|
||||
|
||||
// 使用 useChatMessage 钩子的 handleSend 方法
|
||||
// 使用 useChatMessage 钩子的 handleSend 方法,传递当前选中的应用 ID
|
||||
await handleSend(
|
||||
message,
|
||||
isNewConversation ? null : currConversationId,
|
||||
files,
|
||||
toServerInputs
|
||||
toServerInputs,
|
||||
currentChatApp?.app_id // 传递对话应用 ID
|
||||
);
|
||||
|
||||
} catch (error) {
|
||||
@@ -389,6 +400,50 @@ export default function Chat() {
|
||||
createNewChat();
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理对话应用切换
|
||||
* 切换应用后刷新加载对应的会话列表
|
||||
*/
|
||||
const handleChatAppChange = async (appId: string) => {
|
||||
console.log('🔄 [Chat] 切换对话应用:', appId);
|
||||
|
||||
// 调用原始的切换方法
|
||||
originalHandleChatAppChange(appId, async (app) => {
|
||||
console.log('🔄 [Chat] 应用已切换到:', app.app_name, '开始刷新会话列表...');
|
||||
|
||||
try {
|
||||
// 重新获取会话列表,传入新的应用ID获取该应用的会话
|
||||
const conversationData = await fetchConversations(app.app_id);
|
||||
const conversations = (conversationData as any).data || [];
|
||||
|
||||
console.log('📋 [Chat] 切换应用后获取到会话列表:', conversations.length, '条');
|
||||
|
||||
// 更新会话列表
|
||||
setConversationList(conversations);
|
||||
|
||||
// 清空当前聊天,创建新会话
|
||||
setChatList([]);
|
||||
setChatNotStarted();
|
||||
|
||||
// 如果有会话,选择第一个;否则创建新会话
|
||||
if (conversations.length > 0) {
|
||||
const firstConversation = conversations[0];
|
||||
setCurrConversationId(firstConversation.id, app.app_id, false);
|
||||
console.log('🎯 [Chat] 自动选择第一个会话:', firstConversation.id);
|
||||
} else {
|
||||
setCurrConversationId('-1', app.app_id, false);
|
||||
console.log('🆕 [Chat] 无会话,创建新会话');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('❌ [Chat] 切换应用后刷新会话列表失败:', error);
|
||||
// 即使刷新失败,也清空当前状态
|
||||
setConversationList([]);
|
||||
setChatList([]);
|
||||
setCurrConversationId('-1', appId, false);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理会话删除后的状态更新
|
||||
*/
|
||||
@@ -614,6 +669,10 @@ export default function Chat() {
|
||||
onNewConversation={handleNewConversation}
|
||||
onConversationDeleted={handleConversationDeleted}
|
||||
onConversationRenamed={handleConversationRenamed}
|
||||
chatApps={chatApps}
|
||||
loadingChatApps={loadingChatApps}
|
||||
currentChatApp={currentChatApp}
|
||||
onChatAppChange={handleChatAppChange}
|
||||
/>
|
||||
|
||||
{/* 主内容区域 */}
|
||||
|
||||
@@ -65,9 +65,9 @@ export default function AreaDatasetConfig() {
|
||||
areas,
|
||||
// areasLoading, // 地区列表已加载hook中
|
||||
|
||||
// 筛选
|
||||
filterArea,
|
||||
setFilterArea,
|
||||
// 筛选 - 多选
|
||||
filterAreas,
|
||||
setFilterAreas,
|
||||
page,
|
||||
setPage,
|
||||
pageSize,
|
||||
@@ -233,24 +233,17 @@ export default function AreaDatasetConfig() {
|
||||
};
|
||||
|
||||
/**
|
||||
* 处理地区筛选变化
|
||||
* 处理地区筛选变化 - 支持多选
|
||||
*/
|
||||
const handleAreaFilterChange = (value: string) => {
|
||||
setFilterArea(value);
|
||||
const handleAreaFilterChange = (values: string[]) => {
|
||||
setFilterAreas(values);
|
||||
setPage(1); // 重置到第一页
|
||||
};
|
||||
|
||||
// ==================== Render ====================
|
||||
|
||||
// 计算用户角色标签
|
||||
const userRoleLabel = (() => {
|
||||
const labels: Record<string, string> = {
|
||||
common: '普通用户',
|
||||
admin: '市级管理员',
|
||||
provincial_admin: '省级管理员',
|
||||
};
|
||||
return labels[userRole] || '未知角色';
|
||||
})();
|
||||
// 用户角色已经在 hook 中处理好了,直接使用 userRole
|
||||
const userRoleLabel = userRole || '未知角色';
|
||||
|
||||
// 表格列定义
|
||||
const columns = [
|
||||
@@ -456,16 +449,15 @@ export default function AreaDatasetConfig() {
|
||||
<Flex gap="16px" align="center">
|
||||
<Text style={{ color: colors.text }}>地区筛选:</Text>
|
||||
<Select
|
||||
style={{ width: '150px' }}
|
||||
placeholder="全部地区"
|
||||
mode="multiple"
|
||||
style={{ minWidth: '200px', maxWidth: '400px' }}
|
||||
placeholder="请选择地区(可多选)"
|
||||
allowClear
|
||||
value={filterArea || undefined}
|
||||
value={filterAreas}
|
||||
onChange={handleAreaFilterChange}
|
||||
options={[
|
||||
{ label: '全部', value: '' },
|
||||
{ label: '省级', value: '省级' },
|
||||
...areas.map((area) => ({ label: area, value: area })),
|
||||
]}
|
||||
maxTagCount={3}
|
||||
maxTagPlaceholder={(omittedValues) => `+${omittedValues.length}个`}
|
||||
options={Array.isArray(areas) ? areas.map((area) => ({ label: area, value: area })) : []}
|
||||
/>
|
||||
</Flex>
|
||||
</Card>
|
||||
@@ -538,10 +530,10 @@ export default function AreaDatasetConfig() {
|
||||
<Select
|
||||
placeholder="请选择地区"
|
||||
disabled={!!editingId} // 编辑时禁用
|
||||
options={areas.map((area) => ({
|
||||
options={Array.isArray(areas) ? areas.map((area) => ({
|
||||
label: area,
|
||||
value: area,
|
||||
}))}
|
||||
})) : []}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import DocumentList from './document-list';
|
||||
import DocumentDetail from './document-detail';
|
||||
import RetrieveTest from './retrieve-test';
|
||||
import DatasetSettings from './dataset-settings';
|
||||
import AreaDatasetConfig from './area-dataset-config';
|
||||
import { useDatasetManager } from '~/hooks/dify-dataset-manager';
|
||||
import '../../styles/components/dify-dataset-manager/index.css';
|
||||
|
||||
@@ -26,6 +27,10 @@ export default function DatasetManager() {
|
||||
activeTab,
|
||||
selectedDocument,
|
||||
|
||||
// 知识库列表(基于权限)
|
||||
availableDatasets,
|
||||
loadingAvailableDatasets,
|
||||
|
||||
// 方法
|
||||
handlePageChange,
|
||||
handleDocumentDeleted,
|
||||
@@ -35,6 +40,7 @@ export default function DatasetManager() {
|
||||
handleBackToDocuments,
|
||||
handleTabChange,
|
||||
handleDatasetUpdated,
|
||||
handleDatasetChange,
|
||||
} = useDatasetManager();
|
||||
|
||||
// 加载中状态
|
||||
@@ -101,6 +107,11 @@ export default function DatasetManager() {
|
||||
return <RetrieveTest datasetId={dataset?.id || ''} />;
|
||||
}
|
||||
|
||||
// 配置管理菜单
|
||||
if (activeTab === 'area-config') {
|
||||
return <AreaDatasetConfig />;
|
||||
}
|
||||
|
||||
// 设置菜单
|
||||
if (activeTab === 'settings') {
|
||||
return (
|
||||
@@ -122,6 +133,9 @@ export default function DatasetManager() {
|
||||
onTabChange={handleTabChange}
|
||||
showBackButton={activeTab === 'documents' && !!selectedDocument}
|
||||
onBack={handleBackToDocuments}
|
||||
availableDatasets={availableDatasets}
|
||||
loadingAvailableDatasets={loadingAvailableDatasets}
|
||||
onDatasetChange={handleDatasetChange}
|
||||
>
|
||||
{renderContent()}
|
||||
</DatasetLayout>
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Button, Tooltip } from 'antd';
|
||||
import { Button, Tooltip, Select, Spin } from 'antd';
|
||||
import {
|
||||
FileTextOutlined,
|
||||
SearchOutlined,
|
||||
SettingOutlined,
|
||||
ArrowLeftOutlined,
|
||||
DatabaseOutlined,
|
||||
AppstoreOutlined,
|
||||
SwapOutlined,
|
||||
} from '@ant-design/icons';
|
||||
import type { DatasetLayoutProps, MenuTab, MenuItem } from '~/types/dify-dataset-manager/layout';
|
||||
|
||||
@@ -19,28 +21,63 @@ export default function DatasetLayout({
|
||||
showBackButton = false,
|
||||
onBack,
|
||||
children,
|
||||
availableDatasets = [],
|
||||
loadingAvailableDatasets = false,
|
||||
onDatasetChange,
|
||||
}: DatasetLayoutProps) {
|
||||
const menuItems: MenuItem[] = [
|
||||
{ key: 'documents', icon: <FileTextOutlined />, label: '文档' },
|
||||
{ key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' },
|
||||
{ key: 'area-config', icon: <AppstoreOutlined />, label: '配置管理' },
|
||||
{ key: 'settings', icon: <SettingOutlined />, label: '设置' },
|
||||
];
|
||||
|
||||
// 是否显示知识库选择器(有多个知识库时显示)
|
||||
const showDatasetSelector = availableDatasets.length > 1;
|
||||
|
||||
return (
|
||||
<div className="dataset-layout">
|
||||
{/* 左侧侧边栏 */}
|
||||
<aside className="dataset-sidebar">
|
||||
{/* 知识库信息 */}
|
||||
{/* 知识库信息 / 选择器 */}
|
||||
<div className="sidebar-header">
|
||||
<div className="dataset-icon">
|
||||
<DatabaseOutlined />
|
||||
</div>
|
||||
{showDatasetSelector ? (
|
||||
/* 多个知识库时显示下拉选择器 */
|
||||
<div className="dataset-selector">
|
||||
<Select
|
||||
value={dataset?.id}
|
||||
onChange={onDatasetChange}
|
||||
loading={loadingAvailableDatasets}
|
||||
className="dataset-select"
|
||||
placeholder="选择知识库"
|
||||
suffixIcon={<SwapOutlined />}
|
||||
popupMatchSelectWidth={false}
|
||||
dropdownStyle={{ minWidth: 200 }}
|
||||
>
|
||||
{availableDatasets.map(ds => (
|
||||
<Select.Option key={ds.dataset_id} value={ds.dataset_id}>
|
||||
<div className="dataset-option">
|
||||
<span className="dataset-option-name">{ds.dataset_name}</span>
|
||||
{ds.is_default && <span className="dataset-option-tag">默认</span>}
|
||||
{ds.is_public && <span className="dataset-option-tag public">公共</span>}
|
||||
</div>
|
||||
</Select.Option>
|
||||
))}
|
||||
</Select>
|
||||
<span className="dataset-type">本地文档</span>
|
||||
</div>
|
||||
) : (
|
||||
/* 单个或无知识库时显示名称 */
|
||||
<div className="dataset-info">
|
||||
<Tooltip title={dataset?.name} placement="right">
|
||||
<h2 className="dataset-name">{dataset?.name || '知识库'}</h2>
|
||||
</Tooltip>
|
||||
<span className="dataset-type">本地文档</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 统计信息 */}
|
||||
|
||||
@@ -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,23 +119,41 @@ 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(datasetId, 1);
|
||||
} else {
|
||||
// 回退到原有逻辑:直接从 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);
|
||||
setError(err.message || '加载知识库失败');
|
||||
@@ -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]);
|
||||
|
||||
/**
|
||||
* 处理文档页码变化
|
||||
@@ -191,6 +276,10 @@ export function useDatasetManager() {
|
||||
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]);
|
||||
}, [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
|
||||
};
|
||||
|
||||
// 添加文件数据
|
||||
|
||||
+2
-2
@@ -288,8 +288,8 @@ export default function App() {
|
||||
|
||||
|
||||
return (
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<html lang="zh-CN" suppressHydrationWarning>
|
||||
<head suppressHydrationWarning>
|
||||
<meta charSet="utf-8" />
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
||||
<style dangerouslySetInnerHTML={{
|
||||
|
||||
@@ -91,6 +91,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
files,
|
||||
conversation_id: conversationId,
|
||||
response_mode: responseMode,
|
||||
app_id: appId, // 支持前端传递应用 ID
|
||||
} = body;
|
||||
|
||||
console.log('客戶端調用remix路由_Chat Messages API - 收到请求:', {
|
||||
@@ -98,6 +99,7 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
queryPreview: query?.substring(0, 100) + (query?.length > 100 ? '...' : ''),
|
||||
conversationId,
|
||||
responseMode,
|
||||
appId, // 记录应用 ID
|
||||
hasInputs: !!inputs,
|
||||
hasFiles: !!files && files.length > 0,
|
||||
filesCount: files?.length || 0,
|
||||
@@ -110,7 +112,8 @@ export async function action({ request }: ActionFunctionArgs) {
|
||||
responseMode,
|
||||
conversationId,
|
||||
files,
|
||||
frontendJWT // 传递 JWT
|
||||
frontendJWT, // 传递 JWT
|
||||
appId // 传递应用 ID
|
||||
);
|
||||
|
||||
// 对于流式响应,直接返回流
|
||||
|
||||
@@ -23,7 +23,13 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
);
|
||||
}
|
||||
|
||||
const data = await difyClient.getConversations(frontendJWT);
|
||||
// 从 URL 参数获取 app_id
|
||||
const url = new URL(request.url);
|
||||
const appId = url.searchParams.get('app_id') || undefined;
|
||||
|
||||
console.log('[API] Conversations - 获取会话列表:', { appId });
|
||||
|
||||
const data = await difyClient.getConversations(frontendJWT, appId);
|
||||
|
||||
return json(data, {
|
||||
headers: {
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
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 });
|
||||
@@ -35,11 +34,11 @@ export async function action({ request, params }: LoaderFunctionArgs) {
|
||||
console.log(`[API V3] Update Area Dataset: ${id}`, body);
|
||||
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`;
|
||||
const response = await backendRequest(apiUrl, {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'PUT',
|
||||
headers: {
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
@@ -52,10 +51,11 @@ export async function action({ request, params }: LoaderFunctionArgs) {
|
||||
console.log(`[API V3] Delete Area Dataset: ${id}`);
|
||||
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`;
|
||||
const response = await backendRequest(apiUrl, {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
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 {
|
||||
@@ -22,10 +21,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 转发请求到后端
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/areas`;
|
||||
const response = await backendRequest(apiUrl, {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
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 {
|
||||
@@ -22,10 +21,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 转发请求到后端
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/my`;
|
||||
const response = await backendRequest(apiUrl, {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
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 - 获取所有知识库绑定列表
|
||||
@@ -40,10 +39,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 转发请求到后端
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets?${params}`;
|
||||
const response = await backendRequest(apiUrl, {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -78,11 +78,11 @@ export async function action({ request }: LoaderFunctionArgs) {
|
||||
|
||||
// 转发创建请求到后端
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets`;
|
||||
const response = await backendRequest(apiUrl, {
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${frontendJWT}`,
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
/**
|
||||
* GET /api/v3/dify/chat-apps/default - 获取默认对话应用
|
||||
*
|
||||
* 转发请求到后端 API,后端从配置文件读取默认对话应用
|
||||
* 参考文档:docs/new-dify/dify_api_doc.md - 对话应用多实例支持
|
||||
*/
|
||||
|
||||
import { LoaderFunctionArgs, json } from '@remix-run/node';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (!frontendJWT) {
|
||||
return json(
|
||||
{ code: 401, message: 'JWT认证失败,请重新登录', data: null },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[API] Get Default Chat App - Forwarding to backend');
|
||||
|
||||
// 转发请求到后端
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/chat-apps/default`;
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[API] Get Default Chat App - Backend response:', data);
|
||||
|
||||
return json(data, { status: response.status });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API] Get Default Chat App - Error:', error.message);
|
||||
return json(
|
||||
{ code: 500, message: error.message || 'Failed to get default chat app', data: null },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* GET /api/v3/dify/chat-apps/my - 获取当前用户可访问的对话应用列表
|
||||
*
|
||||
* 转发请求到后端 API,后端从配置文件读取对话应用列表
|
||||
* 参考文档:docs/new-dify/dify_api_doc.md - 对话应用多实例支持
|
||||
*/
|
||||
|
||||
import { LoaderFunctionArgs, json } from '@remix-run/node';
|
||||
import { API_BASE_URL } from '~/config/api-config';
|
||||
import { getUserSession } from '~/api/login/auth.server';
|
||||
|
||||
export async function loader({ request }: LoaderFunctionArgs) {
|
||||
try {
|
||||
const { frontendJWT } = await getUserSession(request);
|
||||
|
||||
if (!frontendJWT) {
|
||||
return json(
|
||||
{ code: 401, message: 'JWT认证失败,请重新登录', data: { data: [], total: 0 } },
|
||||
{ status: 401 }
|
||||
);
|
||||
}
|
||||
|
||||
console.log('[API] Get My Chat Apps - Forwarding to backend');
|
||||
|
||||
// 转发请求到后端 - 使用正确的接口路径
|
||||
// 根据文档:GET /api/v3/dify/chat-apps
|
||||
const apiUrl = `${API_BASE_URL}/v3/dify/chat-apps`;
|
||||
const response = await fetch(apiUrl, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'Authorization': `Bearer ${frontendJWT}`,
|
||||
},
|
||||
});
|
||||
|
||||
const data = await response.json();
|
||||
console.log('[API] Get My Chat Apps - Backend response:', data);
|
||||
|
||||
return json(data, { status: response.status });
|
||||
|
||||
} catch (error: any) {
|
||||
console.error('[API] Get My Chat Apps - Error:', error.message);
|
||||
return json(
|
||||
{ code: 500, message: error.message || 'Failed to get chat apps', data: { data: [], total: 0 } },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,36 +1,42 @@
|
||||
import { Tabs } from 'antd';
|
||||
import DatasetManager from "~/components/dify-dataset-manager";
|
||||
import AreaDatasetConfig from "~/components/dify-dataset-manager/area-dataset-config";
|
||||
import { Spin } from 'antd';
|
||||
import { useState, useEffect } from 'react';
|
||||
|
||||
/**
|
||||
* 知识库管理首页 - 带标签页导航
|
||||
* 标签1: 知识库列表 - 进入单个知识库管理
|
||||
* 标签2: 知识库配置管理 - 地区-知识库绑定管理
|
||||
* 知识库管理页面
|
||||
* 动态加载 DatasetManager 组件避免 SSR 问题
|
||||
*/
|
||||
export default function DatasetManagerIndex() {
|
||||
const items = [
|
||||
{
|
||||
key: 'dataset-list',
|
||||
label: '知识库列表',
|
||||
children: <DatasetManager />,
|
||||
},
|
||||
{
|
||||
key: 'area-config',
|
||||
label: '知识库配置管理',
|
||||
children: <AreaDatasetConfig />,
|
||||
},
|
||||
];
|
||||
export default function DatasetManagerPage() {
|
||||
const [DatasetManager, setDatasetManager] = useState<React.ComponentType | null>(null);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
|
||||
// 使用Tabs作为顶层导航,默认选中第一个
|
||||
const defaultActiveTab = 'dataset-list';
|
||||
useEffect(() => {
|
||||
setMounted(true);
|
||||
// 只在客户端动态导入组件
|
||||
import("~/components/dify-dataset-manager").then((mod) => {
|
||||
setDatasetManager(() => mod.default);
|
||||
}).catch(err => {
|
||||
console.error('加载知识库管理组件失败:', err);
|
||||
});
|
||||
}, []);
|
||||
|
||||
// 服务端渲染时显示简单加载状态
|
||||
if (!mounted) {
|
||||
return (
|
||||
<div className="dataset-manager-container">
|
||||
<Tabs
|
||||
defaultActiveKey={defaultActiveTab}
|
||||
items={items}
|
||||
className="dataset-manager-tabs"
|
||||
/>
|
||||
<div style={{ padding: '40px', textAlign: 'center' }}>
|
||||
<div>加载中...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// 客户端加载中
|
||||
if (!DatasetManager) {
|
||||
return (
|
||||
<div style={{ padding: '40px', textAlign: 'center' }}>
|
||||
<Spin size="large" />
|
||||
<p style={{ marginTop: 16 }}>正在加载知识库管理...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return <DatasetManager />;
|
||||
}
|
||||
|
||||
@@ -95,6 +95,73 @@
|
||||
color: #999;
|
||||
}
|
||||
|
||||
/* 知识库选择器 */
|
||||
.dataset-selector {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.dataset-select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.dataset-select .ant-select-selector {
|
||||
border: 1px solid #e5e5e5 !important;
|
||||
border-radius: 6px !important;
|
||||
background: #fff !important;
|
||||
padding: 2px 8px !important;
|
||||
height: auto !important;
|
||||
min-height: 32px !important;
|
||||
}
|
||||
|
||||
.dataset-select .ant-select-selection-item {
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
color: #1a1a1a;
|
||||
}
|
||||
|
||||
.dataset-select:hover .ant-select-selector {
|
||||
border-color: rgb(0 104 74) !important;
|
||||
}
|
||||
|
||||
.dataset-select.ant-select-focused .ant-select-selector {
|
||||
border-color: rgb(0 104 74) !important;
|
||||
box-shadow: 0 0 0 2px rgba(0, 104, 74, 0.1) !important;
|
||||
}
|
||||
|
||||
/* 知识库下拉选项 */
|
||||
.dataset-option {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
}
|
||||
|
||||
.dataset-option-name {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dataset-option-tag {
|
||||
font-size: 10px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: rgba(0, 104, 74, 0.1);
|
||||
color: rgb(0 104 74);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.dataset-option-tag.public {
|
||||
background: rgba(24, 144, 255, 0.1);
|
||||
color: #1890ff;
|
||||
}
|
||||
|
||||
/* 统计信息 */
|
||||
.sidebar-stats {
|
||||
padding: 0 16px 16px;
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import type { ReactNode } from 'react';
|
||||
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes';
|
||||
import type { AreaDataset } from '~/api/v3/dify/area-datasets';
|
||||
|
||||
/**
|
||||
* 菜单项类型
|
||||
*/
|
||||
export type MenuTab = 'documents' | 'retrieve' | 'settings';
|
||||
export type MenuTab = 'documents' | 'retrieve' | 'area-config' | 'settings';
|
||||
|
||||
/**
|
||||
* 菜单项配置
|
||||
@@ -31,4 +32,10 @@ export interface DatasetLayoutProps {
|
||||
onBack?: () => void;
|
||||
/** 子组件 */
|
||||
children: ReactNode;
|
||||
/** 用户可访问的知识库列表(基于权限) */
|
||||
availableDatasets?: AreaDataset[];
|
||||
/** 加载知识库列表状态 */
|
||||
loadingAvailableDatasets?: boolean;
|
||||
/** 切换知识库回调 */
|
||||
onDatasetChange?: (datasetId: string) => void;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user