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:
2025-12-08 01:44:34 +08:00
parent 27aff59152
commit 3f5c23123b
27 changed files with 925 additions and 167 deletions
+127
View File
@@ -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('获取默认对话应用失败');
}
+41
View File
@@ -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;
}
+9 -2
View File
@@ -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 对象
+19 -4
View File
@@ -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] 没有提供 JWTFastAPI 请求可能失败');
}
// 如果指定了应用 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,
});
+11 -2
View File
@@ -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, {
+2
View File
@@ -521,6 +521,8 @@ export interface SendMessageParams {
conversation_id?: string | null;
files?: VisionFile[];
response_mode?: 'streaming' | 'blocking';
/** 对话应用 ID,用于切换不同的 Dify 应用 */
app_id?: string;
}
/**
+18 -22
View File
@@ -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!;
}
+62 -3
View File
@@ -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>
+39 -2
View File
@@ -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>
{/* 统计信息 */}
+138
View File
@@ -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,
};
}
+98 -8
View File
@@ -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,
};
}
+77 -37
View File
@@ -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,
+7
View File
@@ -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
View File
@@ -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={{
+4 -1
View File
@@ -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
);
// 对于流式响应,直接返回流
+7 -1
View File
@@ -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: {
+5 -5
View File
@@ -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}`,
},
});
+3 -3
View File
@@ -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}`,
},
});
+5 -5
View File
@@ -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 }
);
}
}
+48
View File
@@ -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 }
);
}
}
+33 -27
View File
@@ -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;
+8 -1
View File
@@ -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;
}