feat: 添加对话应用选择和知识库切换功能
- 新增对话应用管理模块(dify-chat-apps),支持获取和切换对话应用 - 优化对话应用切换后自动刷新会话列表功能 - 知识库管理页面新增下拉选择器,支持切换不同知识库 - API 层支持 app_id 参数传递,实现多应用会话隔离 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
/**
|
||||
* Dify 对话应用管理 API 模块
|
||||
*
|
||||
* 提供浏览器端调用对话应用管理 API 的函数
|
||||
* 注意:这些 API 调用的是前端 Remix 路由(/api/...),不需要后端 baseURL
|
||||
*
|
||||
* @module api/dify-chat-apps/chatAppsApi
|
||||
*/
|
||||
|
||||
import type {
|
||||
ChatApp,
|
||||
MyChatAppsResponse,
|
||||
DefaultChatAppResponse,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
* API 基础 URL(前端 Remix 路由)
|
||||
*/
|
||||
const API_URL = '/api/v3/dify/chat-apps';
|
||||
|
||||
/**
|
||||
* HTTP 状态码对应的友好错误信息
|
||||
*/
|
||||
const HTTP_ERROR_MESSAGES: Record<number, string> = {
|
||||
400: '请求参数错误',
|
||||
401: '登录已过期,请重新登录',
|
||||
403: '您没有权限执行此操作',
|
||||
404: '请求的资源不存在',
|
||||
409: '数据冲突,该记录可能已存在',
|
||||
500: '服务器内部错误,请稍后重试',
|
||||
502: '网关错误,请稍后重试',
|
||||
503: '服务暂时不可用,请稍后重试',
|
||||
};
|
||||
|
||||
/**
|
||||
* 封装 fetch 请求,自动处理 credentials
|
||||
*/
|
||||
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
credentials: 'include', // 包含 cookies
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
},
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json().catch(() => ({}));
|
||||
// 优先使用后端返回的错误信息,否则使用友好的默认信息
|
||||
const friendlyMessage = errorData.message
|
||||
|| errorData.error
|
||||
|| HTTP_ERROR_MESSAGES[response.status]
|
||||
|| '操作失败,请稍后重试';
|
||||
const error = new Error(friendlyMessage);
|
||||
(error as any).response = { status: response.status, data: errorData };
|
||||
throw error;
|
||||
}
|
||||
|
||||
return response.json();
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前实例配置的对话应用列表
|
||||
*
|
||||
* 根据用户角色自动返回对应数据范围:
|
||||
* - provincial_admin: 全部可用应用
|
||||
* - admin/common: 本地区可用应用
|
||||
*
|
||||
* @returns 用户可访问的对话应用列表
|
||||
*/
|
||||
export async function getMyChatApps(): Promise<MyChatAppsResponse> {
|
||||
const response = await request<any>(`${API_URL}/my`);
|
||||
|
||||
// 兼容嵌套格式 { data: { data: [], total: ... } }
|
||||
if (response?.data?.data) {
|
||||
return {
|
||||
data: response.data.data,
|
||||
total: response.data.total || 0,
|
||||
page: response.data.page || 1,
|
||||
page_size: response.data.page_size || 10,
|
||||
};
|
||||
}
|
||||
// 格式 { data: [], total: ... }
|
||||
if (response?.data && Array.isArray(response.data)) {
|
||||
return response as MyChatAppsResponse;
|
||||
}
|
||||
// 直接返回
|
||||
if (Array.isArray(response?.data)) {
|
||||
return {
|
||||
data: response.data,
|
||||
total: response.data.length,
|
||||
page: 1,
|
||||
page_size: response.data.length,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn('[API] getMyChatApps: 无效的响应格式', response);
|
||||
return { data: [], total: 0, page: 1, page_size: 10 };
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认对话应用
|
||||
*
|
||||
* 返回配置文件中的第一个应用作为默认应用
|
||||
*
|
||||
* @returns 默认对话应用
|
||||
*/
|
||||
export async function getDefaultChatApp(): Promise<DefaultChatAppResponse> {
|
||||
const response = await request<any>(`${API_URL}/default`);
|
||||
|
||||
// 兼容嵌套格式 { data: { data: {...} } }
|
||||
if (response?.data?.data) {
|
||||
return {
|
||||
data: response.data.data,
|
||||
};
|
||||
}
|
||||
// 格式 { data: {...} }
|
||||
if (response?.data) {
|
||||
return {
|
||||
data: response.data,
|
||||
};
|
||||
}
|
||||
|
||||
console.warn('[API] getDefaultChatApp: 无效的响应格式', response);
|
||||
throw new Error('获取默认对话应用失败');
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* 对话应用类型定义
|
||||
*/
|
||||
export interface ChatApp {
|
||||
/** 应用ID */
|
||||
app_id: string;
|
||||
/** 应用名称 */
|
||||
app_name: string;
|
||||
/** 应用描述 */
|
||||
description: string;
|
||||
/** 是否默认应用 */
|
||||
is_default: boolean;
|
||||
/** 应用类型 */
|
||||
type: string;
|
||||
/** 创建时间 */
|
||||
created_at: string;
|
||||
/** 更新时间 */
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取我的对话应用列表响应
|
||||
*/
|
||||
export interface MyChatAppsResponse {
|
||||
/** 应用列表 */
|
||||
data: ChatApp[];
|
||||
/** 总数 */
|
||||
total: number;
|
||||
/** 分页页码 */
|
||||
page: number;
|
||||
/** 每页数量 */
|
||||
page_size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认对话应用响应
|
||||
*/
|
||||
export interface DefaultChatAppResponse {
|
||||
/** 默认应用 */
|
||||
data: ChatApp;
|
||||
}
|
||||
@@ -32,8 +32,11 @@ export const difyClient = {
|
||||
|
||||
/**
|
||||
* 获取会话列表
|
||||
*
|
||||
* @param jwt - JWT 认证令牌
|
||||
* @param appId - 对话应用 ID(可选,用于获取特定应用的会话列表)
|
||||
*/
|
||||
async getConversations(jwt?: string): Promise<any> {
|
||||
async getConversations(jwt?: string, appId?: string): Promise<any> {
|
||||
const params = new URLSearchParams({
|
||||
limit: '100',
|
||||
first_id: '',
|
||||
@@ -41,6 +44,7 @@ export const difyClient = {
|
||||
|
||||
const response = await difyFetch(`conversations?${params}`, {
|
||||
method: 'GET',
|
||||
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
|
||||
}, jwt);
|
||||
return response.json();
|
||||
},
|
||||
@@ -70,6 +74,7 @@ export const difyClient = {
|
||||
* @param conversationId - 会话 ID
|
||||
* @param files - 附件文件
|
||||
* @param jwt - JWT 认证令牌
|
||||
* @param appId - 对话应用 ID(可选,用于切换不同的 Dify 应用)
|
||||
* @returns 对于流式响应返回 Response 对象,否则返回 JSON
|
||||
*/
|
||||
async createChatMessage(
|
||||
@@ -78,7 +83,8 @@ export const difyClient = {
|
||||
responseMode: string = 'streaming',
|
||||
conversationId?: string,
|
||||
files?: any[],
|
||||
jwt?: string
|
||||
jwt?: string,
|
||||
appId?: string
|
||||
): Promise<Response | any> {
|
||||
const body = {
|
||||
inputs,
|
||||
@@ -90,6 +96,7 @@ export const difyClient = {
|
||||
const response = await difyFetch('chat-messages', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify(body),
|
||||
appId, // 传递应用 ID,会在请求头中添加 X-Dify-App-Id
|
||||
}, jwt);
|
||||
|
||||
// 对于流式响应,直接返回 Response 对象
|
||||
|
||||
@@ -27,6 +27,14 @@ const DIFY_CHAT_API_URL = `${API_BASE_URL}/dify_chat`;
|
||||
// 基础请求函数
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Dify Fetch 请求选项
|
||||
*/
|
||||
export interface DifyFetchOptions extends RequestInit {
|
||||
/** 对话应用 ID,用于切换不同的 Dify 应用 */
|
||||
appId?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dify Chat API 基础请求函数
|
||||
*
|
||||
@@ -34,20 +42,21 @@ const DIFY_CHAT_API_URL = `${API_BASE_URL}/dify_chat`;
|
||||
* FastAPI 后端会验证 JWT 并添加 Dify API_KEY
|
||||
*
|
||||
* @param endpoint - API 端点路径
|
||||
* @param options - fetch 请求选项
|
||||
* @param options - fetch 请求选项(可包含 appId)
|
||||
* @param jwt - 用户 JWT 认证令牌
|
||||
* @returns Response 对象
|
||||
*/
|
||||
export async function difyFetch(
|
||||
endpoint: string,
|
||||
options: RequestInit = {},
|
||||
options: DifyFetchOptions = {},
|
||||
jwt?: string
|
||||
): Promise<Response> {
|
||||
const { appId, ...fetchOptions } = options;
|
||||
const url = `${DIFY_CHAT_API_URL}/${endpoint.replace(/^\//, '')}`;
|
||||
|
||||
const headers: HeadersInit = {
|
||||
'Content-Type': 'application/json',
|
||||
...options.headers,
|
||||
...fetchOptions.headers,
|
||||
};
|
||||
|
||||
if (jwt) {
|
||||
@@ -56,8 +65,14 @@ export async function difyFetch(
|
||||
console.warn('[Dify Chat] 没有提供 JWT,FastAPI 请求可能失败');
|
||||
}
|
||||
|
||||
// 如果指定了应用 ID,添加 X-Dify-App-Id 请求头
|
||||
if (appId) {
|
||||
(headers as Record<string, string>)['X-Dify-App-Id'] = appId;
|
||||
console.log('[Dify Chat] 使用应用 ID:', appId);
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
...options,
|
||||
...fetchOptions,
|
||||
headers,
|
||||
});
|
||||
|
||||
|
||||
@@ -51,6 +51,7 @@ const baseOptions: RequestInit = {
|
||||
/**
|
||||
* 获取用户的会话列表
|
||||
*
|
||||
* @param appId - 对话应用 ID(可选,用于获取特定应用的会话列表)
|
||||
* @returns 包含会话列表的响应对象
|
||||
* @throws {Error} 当获取会话列表失败时抛出错误
|
||||
*
|
||||
@@ -59,15 +60,23 @@ const baseOptions: RequestInit = {
|
||||
* const response = await fetchConversations();
|
||||
* const conversations = response.data;
|
||||
* console.log('会话数量:', conversations.length);
|
||||
*
|
||||
* // 获取指定应用的会话列表
|
||||
* const appConversations = await fetchConversations('app-123');
|
||||
* ```
|
||||
*/
|
||||
export async function fetchConversations(): Promise<ConversationsResponse> {
|
||||
export async function fetchConversations(appId?: string): Promise<ConversationsResponse> {
|
||||
const params = new URLSearchParams({
|
||||
limit: '100',
|
||||
});
|
||||
|
||||
// 如果指定了 appId,添加到查询参数中
|
||||
if (appId) {
|
||||
params.append('app_id', appId);
|
||||
}
|
||||
|
||||
const url = `${API_URL}/conversations?${params}`;
|
||||
console.log('📋 [Dify Client] 获取会话列表:', { url });
|
||||
console.log('📋 [Dify Client] 获取会话列表:', { url, appId });
|
||||
|
||||
try {
|
||||
const response = await axios.get<ConversationsResponse>(url, {
|
||||
|
||||
@@ -521,6 +521,8 @@ export interface SendMessageParams {
|
||||
conversation_id?: string | null;
|
||||
files?: VisionFile[];
|
||||
response_mode?: 'streaming' | 'blocking';
|
||||
/** 对话应用 ID,用于切换不同的 Dify 应用 */
|
||||
app_id?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* 提供地区-知识库绑定管理接口
|
||||
*/
|
||||
|
||||
import { request } from '~/api/axios-client';
|
||||
import { get, post, put, del } from '~/api/axios-client';
|
||||
|
||||
// ==================== Type Definitions ====================
|
||||
|
||||
@@ -87,8 +87,8 @@ const API_BASE = '/api/v3/dify/area-datasets';
|
||||
* 权限: dify:dataset:read
|
||||
*/
|
||||
export async function getMyDatasets(): Promise<MyDatasetsResponse> {
|
||||
const response = await request.get(`${API_BASE}/my`);
|
||||
return response.data;
|
||||
const response = await get<MyDatasetsResponse>(`${API_BASE}/my`);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -101,19 +101,15 @@ export async function getAllDatasets(params: {
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
}): Promise<AllDatasetsResponse> {
|
||||
const queryParams = new URLSearchParams();
|
||||
const queryParams: Record<string, string | number | boolean | undefined> = {};
|
||||
|
||||
if (params.area) queryParams.append('area', params.area);
|
||||
if (params.only_enabled !== undefined)
|
||||
queryParams.append('only_enabled', String(params.only_enabled));
|
||||
if (params.page) queryParams.append('page', String(params.page));
|
||||
if (params.page_size) queryParams.append('page_size', String(params.page_size));
|
||||
if (params.area) queryParams.area = params.area;
|
||||
if (params.only_enabled !== undefined) queryParams.only_enabled = params.only_enabled;
|
||||
if (params.page) queryParams.page = params.page;
|
||||
if (params.page_size) queryParams.page_size = params.page_size;
|
||||
|
||||
const response = await request.get(
|
||||
`${API_BASE}?${queryParams.toString()}`
|
||||
);
|
||||
|
||||
return response.data;
|
||||
const response = await get<AllDatasetsResponse>(API_BASE, queryParams);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -121,8 +117,8 @@ export async function getAllDatasets(params: {
|
||||
* 权限: dify:dataset:manage
|
||||
*/
|
||||
export async function getAvailableAreas(): Promise<string[]> {
|
||||
const response = await request.get(`${API_BASE}/areas`);
|
||||
return response.data.data;
|
||||
const response = await get<AreasResponse>(`${API_BASE}/areas`);
|
||||
return response.data?.data || [];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -132,8 +128,8 @@ export async function getAvailableAreas(): Promise<string[]> {
|
||||
export async function createDatasetBinding(
|
||||
data: CreateDatasetRequest
|
||||
): Promise<ApiResponse<{ data: AreaDataset }>> {
|
||||
const response = await request.post(`${API_BASE}`, data);
|
||||
return response.data;
|
||||
const response = await post<ApiResponse<{ data: AreaDataset }>>(API_BASE, data);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -144,8 +140,8 @@ export async function updateDatasetBinding(
|
||||
id: number,
|
||||
data: UpdateDatasetRequest
|
||||
): Promise<ApiResponse<{ data: AreaDataset }>> {
|
||||
const response = await request.put(`${API_BASE}/${id}`, data);
|
||||
return response.data;
|
||||
const response = await put<ApiResponse<{ data: AreaDataset }>>(`${API_BASE}/${id}`, data);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -153,6 +149,6 @@ export async function updateDatasetBinding(
|
||||
* 权限: dify:dataset:manage
|
||||
*/
|
||||
export async function deleteDatasetBinding(id: number): Promise<ApiResponse<{ message: string }>> {
|
||||
const response = await request.delete(`${API_BASE}/${id}`);
|
||||
return response.data;
|
||||
const response = await del<ApiResponse<{ message: string }>>(`${API_BASE}/${id}`);
|
||||
return response.data!;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user