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({ const params = new URLSearchParams({
limit: '100', limit: '100',
first_id: '', first_id: '',
@@ -41,6 +44,7 @@ export const difyClient = {
const response = await difyFetch(`conversations?${params}`, { const response = await difyFetch(`conversations?${params}`, {
method: 'GET', method: 'GET',
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
}, jwt); }, jwt);
return response.json(); return response.json();
}, },
@@ -70,6 +74,7 @@ export const difyClient = {
* @param conversationId - 会话 ID * @param conversationId - 会话 ID
* @param files - 附件文件 * @param files - 附件文件
* @param jwt - JWT 认证令牌 * @param jwt - JWT 认证令牌
* @param appId - 对话应用 ID(可选,用于切换不同的 Dify 应用)
* @returns 对于流式响应返回 Response 对象,否则返回 JSON * @returns 对于流式响应返回 Response 对象,否则返回 JSON
*/ */
async createChatMessage( async createChatMessage(
@@ -78,7 +83,8 @@ export const difyClient = {
responseMode: string = 'streaming', responseMode: string = 'streaming',
conversationId?: string, conversationId?: string,
files?: any[], files?: any[],
jwt?: string jwt?: string,
appId?: string
): Promise<Response | any> { ): Promise<Response | any> {
const body = { const body = {
inputs, inputs,
@@ -90,6 +96,7 @@ export const difyClient = {
const response = await difyFetch('chat-messages', { const response = await difyFetch('chat-messages', {
method: 'POST', method: 'POST',
body: JSON.stringify(body), body: JSON.stringify(body),
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
}, jwt); }, jwt);
// 对于流式响应,直接返回 Response 对象 // 对于流式响应,直接返回 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 基础请求函数 * Dify Chat API 基础请求函数
* *
@@ -34,20 +42,21 @@ const DIFY_CHAT_API_URL = `${API_BASE_URL}/dify_chat`;
* FastAPI 后端会验证 JWT 并添加 Dify API_KEY * FastAPI 后端会验证 JWT 并添加 Dify API_KEY
* *
* @param endpoint - API 端点路径 * @param endpoint - API 端点路径
* @param options - fetch 请求选项 * @param options - fetch 请求选项(可包含 appId
* @param jwt - 用户 JWT 认证令牌 * @param jwt - 用户 JWT 认证令牌
* @returns Response 对象 * @returns Response 对象
*/ */
export async function difyFetch( export async function difyFetch(
endpoint: string, endpoint: string,
options: RequestInit = {}, options: DifyFetchOptions = {},
jwt?: string jwt?: string
): Promise<Response> { ): Promise<Response> {
const { appId, ...fetchOptions } = options;
const url = `${DIFY_CHAT_API_URL}/${endpoint.replace(/^\//, '')}`; const url = `${DIFY_CHAT_API_URL}/${endpoint.replace(/^\//, '')}`;
const headers: HeadersInit = { const headers: HeadersInit = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
...options.headers, ...fetchOptions.headers,
}; };
if (jwt) { if (jwt) {
@@ -56,8 +65,14 @@ export async function difyFetch(
console.warn('[Dify Chat] 没有提供 JWTFastAPI 请求可能失败'); 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, { const response = await fetch(url, {
...options, ...fetchOptions,
headers, headers,
}); });
+11 -2
View File
@@ -51,6 +51,7 @@ const baseOptions: RequestInit = {
/** /**
* 获取用户的会话列表 * 获取用户的会话列表
* *
* @param appId - 对话应用 ID(可选,用于获取特定应用的会话列表)
* @returns 包含会话列表的响应对象 * @returns 包含会话列表的响应对象
* @throws {Error} 当获取会话列表失败时抛出错误 * @throws {Error} 当获取会话列表失败时抛出错误
* *
@@ -59,15 +60,23 @@ const baseOptions: RequestInit = {
* const response = await fetchConversations(); * const response = await fetchConversations();
* const conversations = response.data; * const conversations = response.data;
* console.log('会话数量:', conversations.length); * 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({ const params = new URLSearchParams({
limit: '100', limit: '100',
}); });
// 如果指定了 appId,添加到查询参数中
if (appId) {
params.append('app_id', appId);
}
const url = `${API_URL}/conversations?${params}`; const url = `${API_URL}/conversations?${params}`;
console.log('📋 [Dify Client] 获取会话列表:', { url }); console.log('📋 [Dify Client] 获取会话列表:', { url, appId });
try { try {
const response = await axios.get<ConversationsResponse>(url, { const response = await axios.get<ConversationsResponse>(url, {
+2
View File
@@ -521,6 +521,8 @@ export interface SendMessageParams {
conversation_id?: string | null; conversation_id?: string | null;
files?: VisionFile[]; files?: VisionFile[];
response_mode?: 'streaming' | 'blocking'; 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 ==================== // ==================== Type Definitions ====================
@@ -87,8 +87,8 @@ const API_BASE = '/api/v3/dify/area-datasets';
* 权限: dify:dataset:read * 权限: dify:dataset:read
*/ */
export async function getMyDatasets(): Promise<MyDatasetsResponse> { export async function getMyDatasets(): Promise<MyDatasetsResponse> {
const response = await request.get(`${API_BASE}/my`); const response = await get<MyDatasetsResponse>(`${API_BASE}/my`);
return response.data; return response.data!;
} }
/** /**
@@ -101,19 +101,15 @@ export async function getAllDatasets(params: {
page?: number; page?: number;
page_size?: number; page_size?: number;
}): Promise<AllDatasetsResponse> { }): Promise<AllDatasetsResponse> {
const queryParams = new URLSearchParams(); const queryParams: Record<string, string | number | boolean | undefined> = {};
if (params.area) queryParams.append('area', params.area); if (params.area) queryParams.area = params.area;
if (params.only_enabled !== undefined) if (params.only_enabled !== undefined) queryParams.only_enabled = params.only_enabled;
queryParams.append('only_enabled', String(params.only_enabled)); if (params.page) queryParams.page = params.page;
if (params.page) queryParams.append('page', String(params.page)); if (params.page_size) queryParams.page_size = params.page_size;
if (params.page_size) queryParams.append('page_size', String(params.page_size));
const response = await request.get( const response = await get<AllDatasetsResponse>(API_BASE, queryParams);
`${API_BASE}?${queryParams.toString()}` return response.data!;
);
return response.data;
} }
/** /**
@@ -121,8 +117,8 @@ export async function getAllDatasets(params: {
* 权限: dify:dataset:manage * 权限: dify:dataset:manage
*/ */
export async function getAvailableAreas(): Promise<string[]> { export async function getAvailableAreas(): Promise<string[]> {
const response = await request.get(`${API_BASE}/areas`); const response = await get<AreasResponse>(`${API_BASE}/areas`);
return response.data.data; return response.data?.data || [];
} }
/** /**
@@ -132,8 +128,8 @@ export async function getAvailableAreas(): Promise<string[]> {
export async function createDatasetBinding( export async function createDatasetBinding(
data: CreateDatasetRequest data: CreateDatasetRequest
): Promise<ApiResponse<{ data: AreaDataset }>> { ): Promise<ApiResponse<{ data: AreaDataset }>> {
const response = await request.post(`${API_BASE}`, data); const response = await post<ApiResponse<{ data: AreaDataset }>>(API_BASE, data);
return response.data; return response.data!;
} }
/** /**
@@ -144,8 +140,8 @@ export async function updateDatasetBinding(
id: number, id: number,
data: UpdateDatasetRequest data: UpdateDatasetRequest
): Promise<ApiResponse<{ data: AreaDataset }>> { ): Promise<ApiResponse<{ data: AreaDataset }>> {
const response = await request.put(`${API_BASE}/${id}`, data); const response = await put<ApiResponse<{ data: AreaDataset }>>(`${API_BASE}/${id}`, data);
return response.data; return response.data!;
} }
/** /**
@@ -153,6 +149,6 @@ export async function updateDatasetBinding(
* 权限: dify:dataset:manage * 权限: dify:dataset:manage
*/ */
export async function deleteDatasetBinding(id: number): Promise<ApiResponse<{ message: string }>> { export async function deleteDatasetBinding(id: number): Promise<ApiResponse<{ message: string }>> {
const response = await request.delete(`${API_BASE}/${id}`); const response = await del<ApiResponse<{ message: string }>>(`${API_BASE}/${id}`);
return response.data; 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 { CHAT_CONFIG } from '../../config/chat';
import useChatMessage from '../../hooks/use-chat-message'; import useChatMessage from '../../hooks/use-chat-message';
import useConversation from '../../hooks/use-conversation'; import useConversation from '../../hooks/use-conversation';
import { useChatApps } from '../../hooks/dify-chat-apps/useChatApps';
import '../../styles/components/chat-with-llm/index.css'; import '../../styles/components/chat-with-llm/index.css';
const { Content } = Layout; const { Content } = Layout;
@@ -51,6 +52,14 @@ export default function Chat() {
// 获取主题配置,避免SSR错误 // 获取主题配置,避免SSR错误
const { colorBgContainer, borderRadiusLG } = useChatTheme(); const { colorBgContainer, borderRadiusLG } = useChatTheme();
// 对话应用管理
const {
chatApps,
loadingChatApps,
currentChatApp,
handleChatAppChange: originalHandleChatAppChange,
} = useChatApps();
// 会话管理 // 会话管理
const { const {
conversationList, conversationList,
@@ -335,7 +344,8 @@ export default function Chat() {
message: message.substring(0, 50) + (message.length > 50 ? '...' : ''), message: message.substring(0, 50) + (message.length > 50 ? '...' : ''),
currConversationId, currConversationId,
isNewConversation, isNewConversation,
willSendConversationId: isNewConversation ? null : currConversationId willSendConversationId: isNewConversation ? null : currConversationId,
appId: currentChatApp?.app_id
}); });
try { try {
@@ -347,12 +357,13 @@ export default function Chat() {
}); });
} }
// 使用 useChatMessage 钩子的 handleSend 方法 // 使用 useChatMessage 钩子的 handleSend 方法,传递当前选中的应用 ID
await handleSend( await handleSend(
message, message,
isNewConversation ? null : currConversationId, isNewConversation ? null : currConversationId,
files, files,
toServerInputs toServerInputs,
currentChatApp?.app_id // 传递对话应用 ID
); );
} catch (error) { } catch (error) {
@@ -389,6 +400,50 @@ export default function Chat() {
createNewChat(); 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} onNewConversation={handleNewConversation}
onConversationDeleted={handleConversationDeleted} onConversationDeleted={handleConversationDeleted}
onConversationRenamed={handleConversationRenamed} onConversationRenamed={handleConversationRenamed}
chatApps={chatApps}
loadingChatApps={loadingChatApps}
currentChatApp={currentChatApp}
onChatAppChange={handleChatAppChange}
/> />
{/* 主内容区域 */} {/* 主内容区域 */}
@@ -65,9 +65,9 @@ export default function AreaDatasetConfig() {
areas, areas,
// areasLoading, // 地区列表已加载hook中 // areasLoading, // 地区列表已加载hook中
// 筛选 // 筛选 - 多选
filterArea, filterAreas,
setFilterArea, setFilterAreas,
page, page,
setPage, setPage,
pageSize, pageSize,
@@ -233,24 +233,17 @@ export default function AreaDatasetConfig() {
}; };
/** /**
* 处理地区筛选变化 * 处理地区筛选变化 - 支持多选
*/ */
const handleAreaFilterChange = (value: string) => { const handleAreaFilterChange = (values: string[]) => {
setFilterArea(value); setFilterAreas(values);
setPage(1); // 重置到第一页 setPage(1); // 重置到第一页
}; };
// ==================== Render ==================== // ==================== Render ====================
// 计算用户角色标签 // 用户角色已经在 hook 中处理好了,直接使用 userRole
const userRoleLabel = (() => { const userRoleLabel = userRole || '未知角色';
const labels: Record<string, string> = {
common: '普通用户',
admin: '市级管理员',
provincial_admin: '省级管理员',
};
return labels[userRole] || '未知角色';
})();
// 表格列定义 // 表格列定义
const columns = [ const columns = [
@@ -456,16 +449,15 @@ export default function AreaDatasetConfig() {
<Flex gap="16px" align="center"> <Flex gap="16px" align="center">
<Text style={{ color: colors.text }}>:</Text> <Text style={{ color: colors.text }}>:</Text>
<Select <Select
style={{ width: '150px' }} mode="multiple"
placeholder="全部地区" style={{ minWidth: '200px', maxWidth: '400px' }}
placeholder="请选择地区(可多选)"
allowClear allowClear
value={filterArea || undefined} value={filterAreas}
onChange={handleAreaFilterChange} onChange={handleAreaFilterChange}
options={[ maxTagCount={3}
{ label: '全部', value: '' }, maxTagPlaceholder={(omittedValues) => `+${omittedValues.length}`}
{ label: '省级', value: '省级' }, options={Array.isArray(areas) ? areas.map((area) => ({ label: area, value: area })) : []}
...areas.map((area) => ({ label: area, value: area })),
]}
/> />
</Flex> </Flex>
</Card> </Card>
@@ -538,10 +530,10 @@ export default function AreaDatasetConfig() {
<Select <Select
placeholder="请选择地区" placeholder="请选择地区"
disabled={!!editingId} // 编辑时禁用 disabled={!!editingId} // 编辑时禁用
options={areas.map((area) => ({ options={Array.isArray(areas) ? areas.map((area) => ({
label: area, label: area,
value: area, value: area,
}))} })) : []}
/> />
</Form.Item> </Form.Item>
@@ -4,6 +4,7 @@ import DocumentList from './document-list';
import DocumentDetail from './document-detail'; import DocumentDetail from './document-detail';
import RetrieveTest from './retrieve-test'; import RetrieveTest from './retrieve-test';
import DatasetSettings from './dataset-settings'; import DatasetSettings from './dataset-settings';
import AreaDatasetConfig from './area-dataset-config';
import { useDatasetManager } from '~/hooks/dify-dataset-manager'; import { useDatasetManager } from '~/hooks/dify-dataset-manager';
import '../../styles/components/dify-dataset-manager/index.css'; import '../../styles/components/dify-dataset-manager/index.css';
@@ -26,6 +27,10 @@ export default function DatasetManager() {
activeTab, activeTab,
selectedDocument, selectedDocument,
// 知识库列表(基于权限)
availableDatasets,
loadingAvailableDatasets,
// 方法 // 方法
handlePageChange, handlePageChange,
handleDocumentDeleted, handleDocumentDeleted,
@@ -35,6 +40,7 @@ export default function DatasetManager() {
handleBackToDocuments, handleBackToDocuments,
handleTabChange, handleTabChange,
handleDatasetUpdated, handleDatasetUpdated,
handleDatasetChange,
} = useDatasetManager(); } = useDatasetManager();
// 加载中状态 // 加载中状态
@@ -101,6 +107,11 @@ export default function DatasetManager() {
return <RetrieveTest datasetId={dataset?.id || ''} />; return <RetrieveTest datasetId={dataset?.id || ''} />;
} }
// 配置管理菜单
if (activeTab === 'area-config') {
return <AreaDatasetConfig />;
}
// 设置菜单 // 设置菜单
if (activeTab === 'settings') { if (activeTab === 'settings') {
return ( return (
@@ -122,6 +133,9 @@ export default function DatasetManager() {
onTabChange={handleTabChange} onTabChange={handleTabChange}
showBackButton={activeTab === 'documents' && !!selectedDocument} showBackButton={activeTab === 'documents' && !!selectedDocument}
onBack={handleBackToDocuments} onBack={handleBackToDocuments}
availableDatasets={availableDatasets}
loadingAvailableDatasets={loadingAvailableDatasets}
onDatasetChange={handleDatasetChange}
> >
{renderContent()} {renderContent()}
</DatasetLayout> </DatasetLayout>
+39 -2
View File
@@ -1,10 +1,12 @@
import { Button, Tooltip } from 'antd'; import { Button, Tooltip, Select, Spin } from 'antd';
import { import {
FileTextOutlined, FileTextOutlined,
SearchOutlined, SearchOutlined,
SettingOutlined, SettingOutlined,
ArrowLeftOutlined, ArrowLeftOutlined,
DatabaseOutlined, DatabaseOutlined,
AppstoreOutlined,
SwapOutlined,
} from '@ant-design/icons'; } from '@ant-design/icons';
import type { DatasetLayoutProps, MenuTab, MenuItem } from '~/types/dify-dataset-manager/layout'; import type { DatasetLayoutProps, MenuTab, MenuItem } from '~/types/dify-dataset-manager/layout';
@@ -19,28 +21,63 @@ export default function DatasetLayout({
showBackButton = false, showBackButton = false,
onBack, onBack,
children, children,
availableDatasets = [],
loadingAvailableDatasets = false,
onDatasetChange,
}: DatasetLayoutProps) { }: DatasetLayoutProps) {
const menuItems: MenuItem[] = [ const menuItems: MenuItem[] = [
{ key: 'documents', icon: <FileTextOutlined />, label: '文档' }, { key: 'documents', icon: <FileTextOutlined />, label: '文档' },
{ key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' }, { key: 'retrieve', icon: <SearchOutlined />, label: '召回测试' },
{ key: 'area-config', icon: <AppstoreOutlined />, label: '配置管理' },
{ key: 'settings', icon: <SettingOutlined />, label: '设置' }, { key: 'settings', icon: <SettingOutlined />, label: '设置' },
]; ];
// 是否显示知识库选择器(有多个知识库时显示)
const showDatasetSelector = availableDatasets.length > 1;
return ( return (
<div className="dataset-layout"> <div className="dataset-layout">
{/* 左侧侧边栏 */} {/* 左侧侧边栏 */}
<aside className="dataset-sidebar"> <aside className="dataset-sidebar">
{/* 知识库信息 */} {/* 知识库信息 / 选择器 */}
<div className="sidebar-header"> <div className="sidebar-header">
<div className="dataset-icon"> <div className="dataset-icon">
<DatabaseOutlined /> <DatabaseOutlined />
</div> </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"> <div className="dataset-info">
<Tooltip title={dataset?.name} placement="right"> <Tooltip title={dataset?.name} placement="right">
<h2 className="dataset-name">{dataset?.name || '知识库'}</h2> <h2 className="dataset-name">{dataset?.name || '知识库'}</h2>
</Tooltip> </Tooltip>
<span className="dataset-type"></span> <span className="dataset-type"></span>
</div> </div>
)}
</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 type { Document } from '~/api/dify-dataset/type/documentTypes';
import { fetchDatasets, fetchDataset } from '~/api/dify-dataset/api/datasetApi'; import { fetchDatasets, fetchDataset } from '~/api/dify-dataset/api/datasetApi';
import { fetchDocuments } from '~/api/dify-dataset/api/documentApi'; 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 type { MenuTab } from '~/types/dify-dataset-manager/layout';
import { DEFAULT_DOCUMENT_PAGE_SIZE } from '~/types/dify-dataset-manager/index'; 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 [dataset, setDataset] = useState<Dataset | null>(null);
const [loadingDataset, setLoadingDataset] = useState(true); const [loadingDataset, setLoadingDataset] = useState(true);
// 用户可访问的知识库列表(基于权限)
const [availableDatasets, setAvailableDatasets] = useState<AreaDataset[]>([]);
const [loadingAvailableDatasets, setLoadingAvailableDatasets] = useState(true);
// 文档状态 // 文档状态
const [documents, setDocuments] = useState<Document[]>([]); const [documents, setDocuments] = useState<Document[]>([]);
const [loadingDocuments, setLoadingDocuments] = useState(false); const [loadingDocuments, setLoadingDocuments] = useState(false);
@@ -57,6 +62,56 @@ export function useDatasetManager() {
} }
}, [documentPageSize]); }, [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 * 加载知识库(获取第一个知识库,再获取详情以包含 retrieval_model
*/ */
@@ -64,23 +119,41 @@ export function useDatasetManager() {
setLoadingDataset(true); setLoadingDataset(true);
try { try {
console.log('[DatasetManager] 加载知识库...'); 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 等字段 if (userDatasets.length > 0) {
const fullDataset = await fetchDataset(firstDatasetId); // 找到默认知识库或第一个知识库
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); console.log('[DatasetManager] 知识库详情响应:', fullDataset);
setDataset(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); await loadDocuments(firstDatasetId, 1);
} else { } else {
setError('未找到知识库,请先在Dify中创建知识库'); setError('未找到知识库,请先在Dify中创建知识库');
} }
}
} catch (err: any) { } catch (err: any) {
console.error('[DatasetManager] 加载知识库失败:', err); console.error('[DatasetManager] 加载知识库失败:', err);
setError(err.message || '加载知识库失败'); setError(err.message || '加载知识库失败');
@@ -89,7 +162,19 @@ export function useDatasetManager() {
setLoadingDataset(false); setLoadingDataset(false);
setInited(true); 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, activeTab,
selectedDocument, selectedDocument,
// 知识库列表(基于权限)
availableDatasets,
loadingAvailableDatasets,
// 方法 // 方法
loadDataset, loadDataset,
loadDocuments, loadDocuments,
@@ -202,6 +291,7 @@ export function useDatasetManager() {
handleBackToDocuments, handleBackToDocuments,
handleTabChange, handleTabChange,
handleDatasetUpdated, handleDatasetUpdated,
handleDatasetChange,
}; };
} }
+77 -37
View File
@@ -4,7 +4,7 @@
* 提供地区-知识库绑定管理功能 * 提供地区-知识库绑定管理功能
*/ */
import { useState, useEffect, useCallback } from 'react'; import { useState, useEffect, useCallback, useMemo } from 'react';
import { import {
getMyDatasets, getMyDatasets,
getAllDatasets, getAllDatasets,
@@ -31,9 +31,9 @@ export interface UseAreaDatasetConfigReturn {
areas: string[]; areas: string[];
areasLoading: boolean; areasLoading: boolean;
// 筛选 // 筛选 - 支持多选
filterArea: string; filterAreas: string[];
setFilterArea: (area: string) => void; setFilterAreas: (areas: string[]) => void;
page: number; page: number;
setPage: (page: number) => void; setPage: (page: number) => void;
pageSize: number; pageSize: number;
@@ -56,23 +56,36 @@ export interface UseAreaDatasetConfigReturn {
canManageDataset: boolean; canManageDataset: boolean;
} }
// 角色名称映射
const ROLE_LABELS: Record<string, string> = {
common: '普通用户',
admin: '市级管理员',
provincial_admin: '省级管理员',
};
// ==================== Hook Implementation ==================== // ==================== Hook Implementation ====================
export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn { 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 [datasets, setDatasets] = useState<AreaDataset[]>([]);
const [allDatasets, setAllDatasets] = useState<AreaDataset[]>([]); // 保存所有数据用于提取地区
const [loading, setLoading] = useState<boolean>(false); const [loading, setLoading] = useState<boolean>(false);
const [total, setTotal] = useState<number>(0); const [total, setTotal] = useState<number>(0);
const [userArea, setUserArea] = useState<string>(''); const [userArea, setUserArea] = useState<string>('');
const [userRoleState, setUserRoleState] = useState<string>(''); const [apiAreas, setApiAreas] = useState<string[]>([]); // API 返回的地区列表
const [areas, setAreas] = useState<string[]>([]);
const [areasLoading, setAreasLoading] = useState<boolean>(false); const [areasLoading, setAreasLoading] = useState<boolean>(false);
// 筛选状态 // 筛选状态 - 支持多选
const [filterArea, setFilterArea] = useState<string>(''); const [filterAreas, setFilterAreas] = useState<string[]>([]);
const [page, setPage] = useState<number>(1); const [page, setPage] = useState<number>(1);
const pageSize = 20; const pageSize = 20;
@@ -81,6 +94,26 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
const [editingId, setEditingId] = useState<number | null>(null); const [editingId, setEditingId] = useState<number | null>(null);
const [submitLoading, setSubmitLoading] = useState<boolean>(false); 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 ==================== // ==================== Data Loading ====================
/** /**
@@ -98,9 +131,11 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
if (canManageDataset) { if (canManageDataset) {
// 省级管理员:获取所有知识库 // 省级管理员:获取所有知识库
// 如果有多个地区筛选,用逗号分隔传递
const areaFilter = filterAreas.length > 0 ? filterAreas.join(',') : undefined;
response = await getAllDatasets({ response = await getAllDatasets({
area: filterArea || undefined, area: areaFilter,
only_enabled: true, only_enabled: false, // 管理员可以看到所有状态
page, page,
page_size: pageSize, page_size: pageSize,
}); });
@@ -109,17 +144,25 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
response = await getMyDatasets(); response = await getMyDatasets();
} }
if (response.code === 0) { console.log('[AreaDatasetConfig] API响应:', response);
setDatasets(response.data.data);
setTotal(response.data.total); 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 接口,保存用户信息 // 如果是 my 接口,保存用户信息
if ('user_area' in response.data) { if ('user_area' in response.data) {
setUserArea((response.data as any).user_area); setUserArea((response.data as any).user_area || '');
setUserRoleState((response.data as any).user_role);
} }
} else { } else {
message.error(`加载失败: ${response.message}`); console.error('[AreaDatasetConfig] API响应格式错误:', response);
message.error(`加载失败: ${response?.message || '未知错误'}`);
} }
} catch (error: any) { } catch (error: any) {
console.error('加载知识库失败:', error); console.error('加载知识库失败:', error);
@@ -127,7 +170,7 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
} finally { } finally {
setLoading(false); setLoading(false);
} }
}, [canManageDataset, canViewDataset, filterArea, page, pageSize]); }, [canManageDataset, canViewDataset, filterAreas, page, pageSize]);
/** /**
* 加载地区列表(仅省级管理员) * 加载地区列表(仅省级管理员)
@@ -138,10 +181,16 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
setAreasLoading(true); setAreasLoading(true);
try { try {
const areasList = await getAvailableAreas(); 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) { } catch (error: any) {
console.error('加载地区列表失败:', error); console.error('加载地区列表失败:', error);
message.error('加载地区列表失败'); // 不显示错误提示,因为可以从数据中提取地区
} finally { } finally {
setAreasLoading(false); setAreasLoading(false);
} }
@@ -247,29 +296,20 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
// ==================== Effects ==================== // ==================== Effects ====================
// 加载数据 // 初始加载数据
useEffect(() => { useEffect(() => {
loadDatasets(); loadDatasets();
}, [loadDatasets]); }, []); // 只在挂载时加载一次
// 加载地区列表
useEffect(() => { useEffect(() => {
loadAreas(); loadAreas();
}, [loadAreas]); }, [loadAreas]);
// 监听筛选条件变化 // 监听筛选条件和页码变化
useEffect(() => { useEffect(() => {
if (canManageDataset) {
setPage(1); // 筛选条件变化时重置到第一页
loadDatasets(); loadDatasets();
} }, [filterAreas, page]);
}, [filterArea, canManageDataset, loadDatasets]);
// 监听页码变化
useEffect(() => {
if (canManageDataset) {
loadDatasets();
}
}, [page, canManageDataset, loadDatasets]);
return { return {
// 数据 // 数据
@@ -277,13 +317,13 @@ export function useAreaDatasetConfig(): UseAreaDatasetConfigReturn {
loading, loading,
total, total,
userArea, userArea,
userRole: userRoleState, userRole: userRoleLabel,
areas, areas,
areasLoading, areasLoading,
// 筛选 // 筛选
filterArea, filterAreas,
setFilterArea, setFilterAreas,
page, page,
setPage, setPage,
pageSize, 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 ( const handleSend = useCallback(async (
message: string, message: string,
conversationId: string | null, conversationId: string | null,
files?: VisionFile[], files?: VisionFile[],
inputs?: Record<string, any>, inputs?: Record<string, any>,
appId?: string,
) => { ) => {
if (!checkCanSend() || !message.trim()) { if (!checkCanSend() || !message.trim()) {
return; return;
@@ -186,6 +192,7 @@ export default function useChatMessage({
inputs: toServerInputs, inputs: toServerInputs,
query: message, query: message,
conversation_id: conversationId === '-1' ? null : conversationId, conversation_id: conversationId === '-1' ? null : conversationId,
app_id: appId, // 添加对话应用 ID
}; };
// 添加文件数据 // 添加文件数据
+2 -2
View File
@@ -288,8 +288,8 @@ export default function App() {
return ( return (
<html lang="zh-CN"> <html lang="zh-CN" suppressHydrationWarning>
<head> <head suppressHydrationWarning>
<meta charSet="utf-8" /> <meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" /> <meta name="viewport" content="width=device-width,initial-scale=1" />
<style dangerouslySetInnerHTML={{ <style dangerouslySetInnerHTML={{
+4 -1
View File
@@ -91,6 +91,7 @@ export async function action({ request }: ActionFunctionArgs) {
files, files,
conversation_id: conversationId, conversation_id: conversationId,
response_mode: responseMode, response_mode: responseMode,
app_id: appId, // 支持前端传递应用 ID
} = body; } = body;
console.log('客戶端調用remix路由_Chat Messages API - 收到请求:', { console.log('客戶端調用remix路由_Chat Messages API - 收到请求:', {
@@ -98,6 +99,7 @@ export async function action({ request }: ActionFunctionArgs) {
queryPreview: query?.substring(0, 100) + (query?.length > 100 ? '...' : ''), queryPreview: query?.substring(0, 100) + (query?.length > 100 ? '...' : ''),
conversationId, conversationId,
responseMode, responseMode,
appId, // 记录应用 ID
hasInputs: !!inputs, hasInputs: !!inputs,
hasFiles: !!files && files.length > 0, hasFiles: !!files && files.length > 0,
filesCount: files?.length || 0, filesCount: files?.length || 0,
@@ -110,7 +112,8 @@ export async function action({ request }: ActionFunctionArgs) {
responseMode, responseMode,
conversationId, conversationId,
files, 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, { return json(data, {
headers: { headers: {
+5 -5
View File
@@ -6,7 +6,6 @@
import { type LoaderFunctionArgs, json } from '@remix-run/node'; import { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config'; import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server'; import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
export async function loader({ request, params }: LoaderFunctionArgs) { export async function loader({ request, params }: LoaderFunctionArgs) {
return json({ error: 'Method not allowed' }, { status: 405 }); 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); console.log(`[API V3] Update Area Dataset: ${id}`, body);
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`; const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`;
const response = await backendRequest(apiUrl, { const response = await fetch(apiUrl, {
method: 'PUT', method: 'PUT',
headers: { headers: {
Authorization: `Bearer ${frontendJWT}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
}, },
body: JSON.stringify(body), body: JSON.stringify(body),
}); });
@@ -52,10 +51,11 @@ export async function action({ request, params }: LoaderFunctionArgs) {
console.log(`[API V3] Delete Area Dataset: ${id}`); console.log(`[API V3] Delete Area Dataset: ${id}`);
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`; const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/${id}`;
const response = await backendRequest(apiUrl, { const response = await fetch(apiUrl, {
method: 'DELETE', method: 'DELETE',
headers: { headers: {
Authorization: `Bearer ${frontendJWT}`, 'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
}, },
}); });
@@ -5,7 +5,6 @@
import { type LoaderFunctionArgs, json } from '@remix-run/node'; import { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config'; import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server'; import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
@@ -22,10 +21,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 转发请求到后端 // 转发请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/areas`; const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/areas`;
const response = await backendRequest(apiUrl, { const response = await fetch(apiUrl, {
method: 'GET', method: 'GET',
headers: { 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 { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config'; import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server'; import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
export async function loader({ request }: LoaderFunctionArgs) { export async function loader({ request }: LoaderFunctionArgs) {
try { try {
@@ -22,10 +21,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 转发请求到后端 // 转发请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/my`; const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets/my`;
const response = await backendRequest(apiUrl, { const response = await fetch(apiUrl, {
method: 'GET', method: 'GET',
headers: { 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 { type LoaderFunctionArgs, json } from '@remix-run/node';
import { API_BASE_URL } from '~/config/api-config'; import { API_BASE_URL } from '~/config/api-config';
import { getUserSession } from '~/api/login/auth.server'; import { getUserSession } from '~/api/login/auth.server';
import { request as backendRequest } from '~/api/axios-client';
/** /**
* GET - 获取所有知识库绑定列表 * GET - 获取所有知识库绑定列表
@@ -40,10 +39,11 @@ export async function loader({ request }: LoaderFunctionArgs) {
// 转发请求到后端 // 转发请求到后端
const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets?${params}`; const apiUrl = `${API_BASE_URL}/v3/dify/area-datasets?${params}`;
const response = await backendRequest(apiUrl, { const response = await fetch(apiUrl, {
method: 'GET', method: 'GET',
headers: { 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 apiUrl = `${API_BASE_URL}/v3/dify/area-datasets`;
const response = await backendRequest(apiUrl, { const response = await fetch(apiUrl, {
method: 'POST', method: 'POST',
headers: { headers: {
Authorization: `Bearer ${frontendJWT}`,
'Content-Type': 'application/json', 'Content-Type': 'application/json',
'Authorization': `Bearer ${frontendJWT}`,
}, },
body: JSON.stringify(body), 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 { Spin } from 'antd';
import DatasetManager from "~/components/dify-dataset-manager"; import { useState, useEffect } from 'react';
import AreaDatasetConfig from "~/components/dify-dataset-manager/area-dataset-config";
/** /**
* 知识库管理首页 - 带标签页导航 * 知识库管理页面
* 标签1: 知识库列表 - 进入单个知识库管理 * 动态加载 DatasetManager 组件避免 SSR 问题
* 标签2: 知识库配置管理 - 地区-知识库绑定管理
*/ */
export default function DatasetManagerIndex() { export default function DatasetManagerPage() {
const items = [ const [DatasetManager, setDatasetManager] = useState<React.ComponentType | null>(null);
{ const [mounted, setMounted] = useState(false);
key: 'dataset-list',
label: '知识库列表',
children: <DatasetManager />,
},
{
key: 'area-config',
label: '知识库配置管理',
children: <AreaDatasetConfig />,
},
];
// 使用Tabs作为顶层导航,默认选中第一个 useEffect(() => {
const defaultActiveTab = 'dataset-list'; setMounted(true);
// 只在客户端动态导入组件
import("~/components/dify-dataset-manager").then((mod) => {
setDatasetManager(() => mod.default);
}).catch(err => {
console.error('加载知识库管理组件失败:', err);
});
}, []);
// 服务端渲染时显示简单加载状态
if (!mounted) {
return ( return (
<div className="dataset-manager-container"> <div style={{ padding: '40px', textAlign: 'center' }}>
<Tabs <div>...</div>
defaultActiveKey={defaultActiveTab}
items={items}
className="dataset-manager-tabs"
/>
</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; 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 { .sidebar-stats {
padding: 0 16px 16px; padding: 0 16px 16px;
+8 -1
View File
@@ -1,10 +1,11 @@
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { Dataset } from '~/api/dify-dataset/type/datasetTypes'; 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; onBack?: () => void;
/** 子组件 */ /** 子组件 */
children: ReactNode; children: ReactNode;
/** 用户可访问的知识库列表(基于权限) */
availableDatasets?: AreaDataset[];
/** 加载知识库列表状态 */
loadingAvailableDatasets?: boolean;
/** 切换知识库回调 */
onDatasetChange?: (datasetId: string) => void;
} }